import gradio as gr import threading import logging import os import difflib import re import subprocess from datasets import load_dataset from transformers import ( T5Tokenizer, T5ForConditionalGeneration, DataCollatorForSeq2Seq, Trainer, TrainingArguments, TrainerCallback, set_seed, ) from huggingface_hub import HfApi logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) TRAINED_MODEL = "Bayernator/horst-spelling-model" BASE_MODEL = "oliverguhr/spelling-correction-german-base" DATASET_NAME = "Bayernator/german-spelling-data" LOCAL_MODEL_DIR = "/data/horst_checkpoints" BUCKET_NAME = "Bayernator/HORST-Rechtschreibbot-storage" set_seed(42) logger.info("=== SUCHE CHECKPOINTS IN /data ===") if os.path.exists("/data"): for root, dirs, files in os.walk("/data"): logger.info(f"Pfad: {root} | Ordner: {dirs} | Dateien: {files[:5]}") else: logger.info("/data existiert nicht (nur im HF Space verfügbar)") logger.info("=== STARTE MANUELLEN BUCKET-SYNC ===") try: subprocess.run( ["hf", "sync", "/data", f"hf://buckets/{BUCKET_NAME}"], capture_output=True, text=True, check=True, timeout=300, ) logger.info("Bucket-Sync von /data abgeschlossen") except Exception as e: logger.info(f"Bucket-Sync fehlgeschlagen (normal ausserhalb HF Space): {e}") tokenizer = None model = None training_status = "Initialisiere..." training_active = False def try_load_model(source): try: tok = T5Tokenizer.from_pretrained(source) mod = T5ForConditionalGeneration.from_pretrained(source) return tok, mod except Exception: return None, None def model_exists_on_hub(repo_id): try: api = HfApi() api.model_info(repo_id, token=os.environ.get("HF_TOKEN")) return True except Exception: return False def sync_to_bucket(local_path, bucket_path): try: result = subprocess.run( ["hf", "sync", local_path, f"hf://buckets/{BUCKET_NAME}/{bucket_path}"], capture_output=True, text=True, timeout=300, ) if result.returncode == 0: logger.info(f"Synced {local_path} to bucket {bucket_path}") return True else: logger.warning(f"Bucket sync failed: {result.stderr[:200]}") return False except Exception as e: logger.warning(f"Bucket sync error: {e}") return False class BucketSyncCallback(TrainerCallback): def on_save(self, args, state, control, **kwargs): step = state.global_step logger.info(f"Checkpoint saved at step {step}. Syncing to bucket...") sync_to_bucket(args.output_dir, f"checkpoints/step_{step}") def word_diff(original, corrected): orig_words = re.findall(r'\S+|\s+', original) corr_words = re.findall(r'\S+|\s+', corrected) orig_tokens = [w for w in orig_words if w.strip()] corr_tokens = [w for w in corr_words if w.strip()] matcher = difflib.SequenceMatcher(None, orig_tokens, corr_tokens) changes = [] for tag, i1, i2, j1, j2 in matcher.get_opcodes(): if tag == "replace": for idx in range(max(i2 - i1, j2 - j1)): o = orig_tokens[i1 + idx] if i1 + idx < i2 else "" c = corr_tokens[j1 + idx] if j1 + idx < j2 else "" if o and c and o != c: changes.append((o, c)) elif tag == "delete": for idx in range(i1, i2): changes.append((orig_tokens[idx], "\u2205")) elif tag == "insert": for idx in range(j1, j2): changes.append(("\u2205", corr_tokens[idx])) return changes def build_changes_html(changes): if not changes: return '
\u2714 Keine \u00c4nderungen erforderlich.
' items = [] for old, new in changes: items.append( f'
' f'{old}' f'\u2192' f'{new}' f'
' ) return ( f'
' f'
' f'\u00c4nderungen ({len(changes)})
' f'{"".join(items)}' f'
' ) def do_training(): global model, tokenizer, training_status, training_active if training_active: return training_active = True try: training_status = "Lade Trainingsdaten..." logger.info("Loading dataset...") dataset = load_dataset(DATASET_NAME, split="train") dataset = dataset.train_test_split(test_size=0.1, seed=42) train_data = dataset["train"] eval_data = dataset["test"] logger.info(f"Dataset loaded: {len(train_data)} train, {len(eval_data)} eval") training_status = f"Tokenisiere {len(train_data)} Beispiele..." def tokenize_fn(batch): inputs = tokenizer( batch["input"], max_length=128, truncation=True, padding=False, ) targets = tokenizer( batch["target"], max_length=128, truncation=True, padding=False, ) inputs["labels"] = targets["input_ids"] return inputs train_data = train_data.map( tokenize_fn, batched=True, batch_size=50, remove_columns=["input", "target"], ) eval_data = eval_data.map( tokenize_fn, batched=True, batch_size=50, remove_columns=["input", "target"], ) train_data.set_format("torch", columns=["input_ids", "attention_mask", "labels"]) eval_data.set_format("torch", columns=["input_ids", "attention_mask", "labels"]) data_collator = DataCollatorForSeq2Seq(tokenizer, model=model, padding=True) training_args = TrainingArguments( output_dir=LOCAL_MODEL_DIR, num_train_epochs=3, per_device_train_batch_size=1, per_device_eval_batch_size=1, gradient_accumulation_steps=2, learning_rate=3e-5, warmup_steps=100, logging_steps=5, eval_strategy="steps", eval_steps=500, save_strategy="steps", save_steps=500, save_total_limit=2, load_best_model_at_end=True, metric_for_best_model="loss", greater_is_better=False, bf16=False, fp16=False, report_to="none", dataloader_num_workers=0, ) trainer = Trainer( model=model, args=training_args, data_collator=data_collator, train_dataset=train_data, eval_dataset=eval_data, processing_class=tokenizer, callbacks=[BucketSyncCallback], ) training_status = "Training läuft (ca. 3-5h auf CPU)..." logger.info("Starting training...") trainer.train() training_status = "Speichere Modell..." logger.info("Saving model...") trainer.save_model(LOCAL_MODEL_DIR) try: training_status = "Pushe Modell zu Hugging Face..." logger.info("Pushing to hub...") trainer.push_to_hub( commit_message="Trained via HORST Space on CPU", token=os.environ.get("HF_TOKEN"), ) except Exception as hub_error: logger.warning(f"Push to Hub failed: {hub_error}") try: training_status = "Sync Modell zu Bucket..." logger.info("Syncing to bucket...") sync_to_bucket(LOCAL_MODEL_DIR, "model_checkpoints") except Exception as bucket_error: logger.warning(f"Bucket sync failed: {bucket_error}") tokenizer = T5Tokenizer.from_pretrained(LOCAL_MODEL_DIR) model = T5ForConditionalGeneration.from_pretrained(LOCAL_MODEL_DIR) training_status = "Training abgeschlossen. Modell bereit." logger.info("Training completed.") except Exception as e: training_status = f"Training fehlgeschlagen: {e}" logger.error(f"Training failed: {e}", exc_info=True) finally: training_active = False if os.path.isdir(LOCAL_MODEL_DIR): tokenizer, model = try_load_model(LOCAL_MODEL_DIR) if tokenizer is not None: training_status = "Feinabgestimmtes Modell bereit (lokal)." if tokenizer is None and model_exists_on_hub(TRAINED_MODEL): tokenizer, model = try_load_model(TRAINED_MODEL) if tokenizer is not None: training_status = "Feinabgestimmtes Modell bereit (Hub)." if tokenizer is None: tokenizer = T5Tokenizer.from_pretrained(BASE_MODEL) model = T5ForConditionalGeneration.from_pretrained(BASE_MODEL) training_status = "Basis-Modell geladen. Training startet automatisch..." logger.info("Base model loaded. Auto-training will start.") def auto_train(): global training_status training_status = "Starte automatisches Training..." do_training() threading.Thread(target=auto_train, daemon=True).start() def start_training(): global training_status if training_active: return training_status lower = training_status.lower() if "lade" in lower or "läuft" in lower or "starte" in lower: return training_status training_status = "Training wird gestartet..." threading.Thread(target=do_training, daemon=True).start() return training_status def get_training_status(): return training_status def build_prefix(rs, gram, stil): if rs and gram and stil: return "Verbessere Rechtschreibung, Grammatik und Stil: " if rs and gram: return "Korrigiere Rechtschreibung und Grammatik: " if rs and stil: return "Korrigiere Rechtschreibung und verbessere den Stil: " if gram and stil: return "Korrigiere Grammatik und verbessere den Stil: " if gram: return "Korrigiere die Grammatik: " if stil: return "Verbessere den Stil: " return "Korrigiere die Rechtschreibung: " def format_info(text_len, prefix): if "Verbessere" in prefix and "Grammatik" in prefix and "Stil" in prefix: mode = "RS + Grammatik + Stil" elif "Grammatik" in prefix and "Stil" in prefix: mode = "Grammatik + Stil" elif "Rechtschreibung" in prefix and "Stil" in prefix: mode = "RS + Stil" elif "Grammatik" in prefix: mode = "Grammatik" elif "Stil" in prefix: mode = "Stil" else: mode = "Rechtschreibung" return f"{text_len} Zeichen \u00b7 {mode}" def korrigieren(text, rs, gram, stil): if not text or not text.strip(): return "", "", "", "Bitte gib zuerst einen Text ein." try: prefix = build_prefix(rs, gram, stil) prompt = prefix + text.strip() original = text.strip() inputs = tokenizer( prompt, return_tensors="pt", max_length=512, truncation=True, ) outputs = model.generate( **inputs, max_length=512, num_beams=4, early_stopping=True, ) result = tokenizer.decode(outputs[0], skip_special_tokens=True) info = format_info(len(text), prefix) changes = word_diff(original, result) changes_html = build_changes_html(changes) return result, changes_html, "", info except Exception as e: return "", "", "", f"Fehler: {e}" CSS = """ footer { display: none !important; } header { display: none !important; } .gradio-container { max-width: 960px !important; margin: auto !important; } .gradio-container textarea { border-radius: 12px !important; font-size: 15px !important; line-height: 1.6 !important; padding: 14px !important; } .panel-label { font-size: 12px; font-weight: 600; color: #6b7280; letter-spacing: 0.5px; margin-bottom: 6px; } """ with gr.Blocks() as demo: gr.HTML("""
HORST
KI-Schreibassistent f\u00fcr die deutsche Sprache
""") with gr.Row(equal_height=True): with gr.Column(scale=1, min_width=300): gr.HTML('
EINGABE
') text_input = gr.Textbox( label=None, placeholder="Deutschen Text hier eingeben...", lines=16, max_lines=30, container=False, ) with gr.Column(scale=1, min_width=300): gr.HTML('
AUSGABE
') text_output = gr.Textbox( label=None, lines=16, max_lines=30, interactive=False, container=False, ) with gr.Row(): rs_cb = gr.Checkbox(label="Rechtschreibung", value=True) gram_cb = gr.Checkbox(label="Grammatik", value=False) stil_cb = gr.Checkbox(label="Stil", value=False) beispiel_btn = gr.Button("\u2b50 Beispiel", variant="secondary", size="sm") leeren_btn = gr.Button("\u21ba Leeren", variant="secondary", size="sm") korrigieren_btn = gr.Button("\u2192 Korrigieren", variant="primary", size="sm") changes_box = gr.HTML() def on_correction(text, rs, gram, stil): res, changes_html, _, info = korrigieren(text, rs, gram, stil) return res, changes_html def on_beispiel(): return "Ich habe gestern ein interesanten Artikel gelessen und fand ihn zimlich gut." def on_leeren(): return "", "", "" korrigieren_btn.click( fn=on_correction, inputs=[text_input, rs_cb, gram_cb, stil_cb], outputs=[text_output, changes_box], ) beispiel_btn.click(fn=on_beispiel, inputs=None, outputs=text_input) leeren_btn.click(fn=on_leeren, inputs=None, outputs=[text_input, text_output, changes_box]) gr.HTML("
") with gr.Accordion("\u2699 Training & Status", open=False): training_status_box = gr.Textbox( label="Status", value=training_status, interactive=False, lines=2, ) with gr.Row(): refresh_status_btn = gr.Button("Aktualisieren", size="sm") train_btn = gr.Button("Training starten", variant="secondary", size="sm") train_btn.click(fn=start_training, inputs=None, outputs=training_status_box) refresh_status_btn.click(fn=get_training_status, inputs=None, outputs=training_status_box) demo.launch( css=CSS, theme=gr.themes.Base( primary_hue="blue", neutral_hue="gray", font=gr.themes.GoogleFont("Inter"), ), )