# -*- coding: utf-8 -*- """ translate: Diktat mit Übersetzung und Text-to-Speech Funktionen: - Diktieren in gewählter Eingabesprache (Standard: Deutsch) - Übersetzung in Ausgabesprache (zwei Texte nebeneinander) - Umkehr-Button: Eingabe- und Ausgabesprache tauschen - Sound-Button: Übersetzung laut vorlesen (Text-to-Speech) - Neu-Button: Beide Textbereiche leeren Voraussetzungen: py -3.11 -m pip install openai python-dotenv sounddevice Für Vorlesen direkt in der App (kein externes Programm): py -3.11 -m pip install pygame Start: py -3.11 translate.py """ import html as html_module import json import os import re import sys import textwrap import tempfile import threading import wave import tkinter as tk import tkinter.font as tkfont from tkinter import ttk, messagebox, simpledialog, filedialog from tkinter.scrolledtext import ScrolledText from datetime import datetime from dotenv import load_dotenv from openai import OpenAI def _clipboard_set_persistent(text: str) -> bool: """Text in Zwischenablage schreiben, bleibt nach Schließen der App erhalten (Windows).""" if sys.platform != "win32": try: root = tk._default_root if root: root.clipboard_clear() root.clipboard_append(text) return True except Exception: return False try: import ctypes from ctypes import wintypes CF_UNICODETEXT = 13 GMEM_DDESHARE = 0x2000 kernel32 = ctypes.WinDLL("kernel32") user32 = ctypes.WinDLL("user32") user32.OpenClipboard.argtypes = [wintypes.HWND] user32.OpenClipboard.restype = wintypes.BOOL user32.EmptyClipboard.argtypes = [] user32.SetClipboardData.argtypes = [wintypes.UINT, wintypes.HANDLE] user32.SetClipboardData.restype = wintypes.HANDLE kernel32.GlobalAlloc.argtypes = [wintypes.UINT, ctypes.c_size_t] kernel32.GlobalAlloc.restype = wintypes.HGLOBAL kernel32.GlobalLock.argtypes = [wintypes.HGLOBAL] kernel32.GlobalLock.restype = ctypes.c_void_p kernel32.GlobalUnlock.argtypes = [wintypes.HGLOBAL] for _ in range(5): if user32.OpenClipboard(None): break import time time.sleep(0.03) else: return False try: user32.EmptyClipboard() data = (text + "\0").encode("utf-16-le") h = kernel32.GlobalAlloc(GMEM_DDESHARE, len(data)) if not h: return False ptr = kernel32.GlobalLock(h) if ptr: ctypes.memmove(ptr, data, len(data)) kernel32.GlobalUnlock(h) user32.SetClipboardData(CF_UNICODETEXT, h) return True finally: user32.CloseClipboard() except Exception: return False # ----------------------------- # Audio Recorder # ----------------------------- class AudioRecorder: """Recorder mit sounddevice. Speichert als 16kHz mono WAV.""" def __init__(self, samplerate=16000, channels=1): self.samplerate = samplerate self.channels = channels self._stream = None self._frames = [] self._recording = False def start(self): try: import sounddevice as sd except ImportError as e: raise RuntimeError( "Paket 'sounddevice' fehlt. Installieren: py -3.11 -m pip install sounddevice" ) self._frames = [] self._recording = True def callback(indata, frames, time_info, status): if self._recording: self._frames.append(indata.copy()) self._stream = sd.InputStream( samplerate=self.samplerate, channels=self.channels, callback=callback, dtype="float32", blocksize=0, ) self._stream.start() def get_recent_audio_seconds(self, n: float): """Gibt die letzten n Sekunden Audio als float32-Numpy-Array zurück (für Stille-Erkennung).""" if not self._frames: return None import numpy as np audio = np.concatenate(self._frames, axis=0) samples_needed = int(n * self.samplerate) if len(audio) < samples_needed: return None return audio[-samples_needed:] def stop_and_save_wav(self) -> str: if not self._stream: raise RuntimeError("Recorder nicht gestartet.") self._recording = False self._stream.stop() self._stream.close() self._stream = None if not self._frames: raise RuntimeError("Keine Audio-Daten.") import numpy as np audio = np.concatenate(self._frames, axis=0) audio = np.clip(audio, -1.0, 1.0) pcm16 = (audio * 32767.0).astype(np.int16) fd, path = tempfile.mkstemp(suffix=".wav", prefix="LindengutAG_") os.close(fd) with wave.open(path, "wb") as wf: wf.setnchannels(self.channels) wf.setsampwidth(2) wf.setframerate(self.samplerate) wf.writeframes(pcm16.tobytes()) return path # ----------------------------- # Sprachen # ----------------------------- LANGUAGES = { "de": "Deutsch", "en": "Englisch", "fr": "Französisch", "es": "Spanisch", "it": "Italienisch", "pt": "Portugiesisch", "nl": "Niederländisch", "pl": "Polnisch", "ru": "Russisch", "ja": "Japanisch", "zh": "Chinesisch", "ar": "Arabisch", "tr": "Türkisch", "el": "Griechisch", "sv": "Schwedisch", "sk": "Slowakisch", "uk": "Ukrainisch", "sq": "Albanisch", } # Sortiert nach Sprachname (A–Z) LANGUAGES_SORTED = sorted(LANGUAGES.items(), key=lambda x: x[1]) # Konfigurationsdatei für letzte Einstellungen CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "translate_config.json") VORLAGEN_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "gespraech_vorlagen.json") def load_gesp_config(): """Lädt letzte Sprache, Stille-Sekunden, Sprechgeschwindigkeit und Fenster-Geometrie aus Config.""" try: if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, "r", encoding="utf-8") as f: data = json.load(f) lang = data.get("gesp_lang", "de") silence = max(15, data.get("silence_sec", 20)) speed = data.get("tts_speed", 1.0) geom = data.get("gesp_geometry", "") return lang, silence, speed, geom if geom and "x" in geom else "" except Exception: pass return "de", 20, 1.0, "" def save_gesp_config(gesp_lang: str, silence_sec: int, tts_speed: float = 1.0, gesp_geometry: str = None): """Speichert Sprache, Stille-Sekunden, Sprechgeschwindigkeit und optional Fenster-Geometrie.""" try: data = {} if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, "r", encoding="utf-8") as f: data = json.load(f) data["gesp_lang"] = gesp_lang data["silence_sec"] = int(max(15, min(30, silence_sec))) data["tts_speed"] = float(tts_speed) if gesp_geometry is not None: data["gesp_geometry"] = gesp_geometry with open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) except Exception: pass def _clamp_geometry(geom: str, min_w: int, min_h: int) -> str: """Begrenzt gespeicherte Geometrie auf Mindestgröße (alle Buttons sichtbar).""" if not geom or "x" not in geom: return f"{min_w}x{min_h}" parts = geom.replace("+", "x").split("x") try: w = max(min_w, int(parts[0].strip())) h = max(min_h, int(parts[1].strip())) if len(parts) >= 4: return f"{w}x{h}+{parts[2].strip()}+{parts[3].strip()}" return f"{w}x{h}" except (ValueError, IndexError): return f"{min_w}x{min_h}" def load_main_geometry(): """Lädt gespeicherte Hauptfenster-Geometrie (Größe + Position), mindestens Mindestgröße.""" MAIN_MIN_W, MAIN_MIN_H = 600, 450 try: if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, "r", encoding="utf-8") as f: g = json.load(f).get("main_geometry", "") if g and "x" in g: return _clamp_geometry(g, MAIN_MIN_W, MAIN_MIN_H) except Exception: pass return f"{MAIN_MIN_W}x{MAIN_MIN_H}" def save_main_geometry(geom: str): try: data = {} if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, "r", encoding="utf-8") as f: data = json.load(f) data["main_geometry"] = geom with open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) except Exception: pass def load_main_languages(): """Lädt gespeicherte Eingabe- und Ausgabesprache aus Config (beim Start).""" try: if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, "r", encoding="utf-8") as f: data = json.load(f) lang_in = (data.get("lang_in") or "de").strip() lang_out = (data.get("lang_out") or "en").strip() if lang_in in LANGUAGES and lang_out in LANGUAGES: return lang_in, lang_out except Exception: pass return "de", "en" def load_lang_out_usage(): """Lädt die Nutzungshäufigkeit der Ausgabesprachen (für Sortierung: häufigste zuoberst).""" try: if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, "r", encoding="utf-8") as f: data = json.load(f) return data.get("lang_out_usage") or {} except Exception: pass return {} def save_lang_out_usage(usage: dict): """Speichert die Nutzungshäufigkeit der Ausgabesprachen.""" try: data = {} if os.path.exists(CONFIG_PATH): try: with open(CONFIG_PATH, "r", encoding="utf-8") as f: data = json.load(f) except Exception: pass data["lang_out_usage"] = {k: int(v) for k, v in usage.items() if k in LANGUAGES and isinstance(v, (int, float))} with open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) f.flush() if hasattr(f, "fileno"): try: os.fsync(f.fileno()) except Exception: pass except Exception: pass def increment_lang_out_usage(lang_out: str): """Erhöht die Nutzungszahl der gewählten Ausgabesprache um 1.""" if lang_out not in LANGUAGES: return usage = load_lang_out_usage() usage[lang_out] = usage.get(lang_out, 0) + 1 save_lang_out_usage(usage) def get_output_languages_ordered(): """Reihenfolge für Ausgabesprache: zuerst die drei vom Benutzer am häufigsten genutzten, danach alphabetisch.""" usage = load_lang_out_usage() # Sortieren: absteigend nach Nutzung, bei gleicher Nutzung alphabetisch nach Sprachname return sorted( LANGUAGES.items(), key=lambda x: (-usage.get(x[0], 0), x[1]), ) def save_main_languages(lang_in: str, lang_out: str): """Speichert Eingabe- und Ausgabesprache in Config und zählt Ausgabesprache für „häufigste“ mit.""" try: data = {} if os.path.exists(CONFIG_PATH): try: with open(CONFIG_PATH, "r", encoding="utf-8") as f: data = json.load(f) except Exception: pass data["lang_in"] = lang_in if lang_in in LANGUAGES else "de" data["lang_out"] = lang_out if lang_out in LANGUAGES else "en" with open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) f.flush() if hasattr(f, "fileno"): try: os.fsync(f.fileno()) except Exception: pass except Exception: pass def main(): load_dotenv() api_key = os.getenv("OPENAI_API_KEY", "").strip() if not api_key: messagebox.showerror("Fehler", "OPENAI_API_KEY fehlt. Lege eine .env Datei an.") return client = OpenAI(api_key=api_key) try: def_font = tkfont.nametofont("TkDefaultFont") text_font = (def_font.actual()["family"], def_font.actual()["size"]) except Exception: text_font = ("Segoe UI", 10) MAIN_MIN_W, MAIN_MIN_H = 600, 450 root = tk.Tk() root.title("AZA von Arzt zu Arzt") root.minsize(MAIN_MIN_W, MAIN_MIN_H) root.geometry(load_main_geometry()) root.configure(bg="#E8F4F8") def on_main_close(): try: save_main_geometry(root.geometry()) in_val = (lang_in_var.get() or "").strip() out_val = (lang_out_var.get() or "").strip() lin = in_val.split(" – ")[0].strip() if " – " in in_val else ("de" if in_val not in LANGUAGES else in_val) lou = out_val.split(" – ")[0].strip() if " – " in out_val else ("en" if out_val not in LANGUAGES else out_val) if lin not in LANGUAGES: lin = "de" if lou not in LANGUAGES: lou = "en" save_main_languages(lin, lou) except Exception: pass root.destroy() root.protocol("WM_DELETE_WINDOW", on_main_close) recorder = AudioRecorder() is_recording = [False] diktat_recorder = [None] # Sprachauswahl lang_in_saved, lang_out_saved = load_main_languages() lang_container = ttk.Frame(root, padding=12) lang_container.pack(fill="x") LABEL_WIDTH = 30 ttk.Label(lang_container, text="Eingabesprache (diktiert):", width=LABEL_WIDTH, anchor="w").grid(row=0, column=0, padx=(0, 8), sticky="w") lang_in_var = tk.StringVar(value=lang_in_saved) combo_in = ttk.Combobox( lang_container, textvariable=lang_in_var, values=[f"{k} – {v}" for k, v in LANGUAGES_SORTED], state="readonly", width=28, ) combo_in.grid(row=0, column=1, sticky="w", padx=(0, 24)) combo_in.set(f"{lang_in_saved} – {LANGUAGES.get(lang_in_saved, lang_in_saved)}") ttk.Label(lang_container, text="Ausgabesprache (Übersetzung):", width=LABEL_WIDTH, anchor="w").grid(row=1, column=0, padx=(0, 8), pady=(4, 0), sticky="w") lang_out_var = tk.StringVar(value=lang_out_saved) output_ordered = get_output_languages_ordered() combo_out = ttk.Combobox( lang_container, textvariable=lang_out_var, values=[f"{k} – {v}" for k, v in output_ordered], state="readonly", width=28, ) combo_out.grid(row=1, column=1, sticky="w", padx=(0, 12), pady=(4, 0)) combo_out.set(f"{lang_out_saved} – {LANGUAGES.get(lang_out_saved, lang_out_saved)}") def save_main_languages_now(): try: in_val = (lang_in_var.get() or "").strip() out_val = (lang_out_var.get() or "").strip() lin = in_val.split(" – ")[0].strip() if " – " in in_val else ("de" if in_val not in LANGUAGES else in_val) lou = out_val.split(" – ")[0].strip() if " – " in out_val else ("en" if out_val not in LANGUAGES else out_val) if lin not in LANGUAGES: lin = "de" if lou not in LANGUAGES: lou = "en" save_main_languages(lin, lou) except Exception: pass lang_in_var.trace_add("write", lambda *a: save_main_languages_now()) lang_out_var.trace_add("write", lambda *a: save_main_languages_now()) def _on_lang_selected(event): """Bei Auswahl in der Combobox: Wert aus dem Widget übernehmen und sofort speichern (readonly-Combobox setzt StringVar teils nicht).""" w = event.widget try: val = w.get().strip() if val: if w == combo_in: lang_in_var.set(val) else: lang_out_var.set(val) save_main_languages_now() except Exception: pass combo_in.bind("<>", _on_lang_selected) combo_out.bind("<>", _on_lang_selected) def get_lang_codes(): in_val = lang_in_var.get() out_val = lang_out_var.get() lang_in = in_val.split(" – ")[0] if " – " in in_val else "de" lang_out = out_val.split(" – ")[0] if " – " in out_val else "en" return lang_in, lang_out def swap_languages(): in_val = lang_in_var.get() out_val = lang_out_var.get() lang_in_var.set(out_val) lang_out_var.set(in_val) umkehr_row = ttk.Frame(root, padding=(12, 4, 12, 4)) umkehr_row.pack(fill="x") btn_swap = ttk.Button(umkehr_row, text="⇄ Umkehren", command=swap_languages) btn_swap.pack(anchor="center") # Zwei Textbereiche paned = ttk.PanedWindow(root, orient="horizontal") paned.pack(fill="both", expand=True, padx=12, pady=(0, 12)) left_f = ttk.Frame(paned, padding=8) right_f = ttk.Frame(paned, padding=8) paned.add(left_f, weight=1) paned.add(right_f, weight=1) ttk.Label(left_f, text="Diktiert / Original:").pack(anchor="w") txt_left = tk.Text(left_f, wrap="word", font=text_font, bg="#E1EDF5", height=8) txt_left.pack(fill="both", expand=True, pady=(4, 0)) copy_left_f = ttk.Frame(left_f) copy_left_f.pack(fill="x", pady=(4, 0)) ttk.Button(copy_left_f, text="Kopiere Original", command=lambda: _clipboard_set_persistent(txt_left.get("1.0", "end").strip()) or None).pack(anchor="center") ttk.Label(right_f, text="Übersetzung:").pack(anchor="w") txt_right = tk.Text(right_f, wrap="word", font=text_font, bg="#EBF3FA", height=8) txt_right.pack(fill="both", expand=True, pady=(4, 0)) copy_right_f = ttk.Frame(right_f) copy_right_f.pack(fill="x", pady=(4, 0)) ttk.Button(copy_right_f, text="Kopiere Übersetzung", command=lambda: _clipboard_set_persistent(txt_right.get("1.0", "end").strip()) or None).pack(anchor="center") # Buttons btn_frame = ttk.Frame(root, padding=(12, 0, 12, 6)) btn_frame.pack(fill="x") status_var = tk.StringVar(value="Bereit.") status_row = ttk.Frame(btn_frame) status_row.pack(fill="x", pady=(0, 4)) status_style = ttk.Style() status_style.configure("Status.TLabel", foreground="#5A6C7D", font=("Segoe UI", 10)) status_label = ttk.Label(status_row, textvariable=status_var, width=45, anchor="w", style="Status.TLabel") status_label.pack(side="left") btn_row1 = ttk.Frame(btn_frame) btn_row1.pack(fill="x", pady=(0, 4)) btn_row2 = ttk.Frame(btn_frame) btn_row2.pack(fill="x", pady=(0, 4)) main_speed_var = tk.StringVar(value="Normal") try: if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, encoding="utf-8") as f: main_speed_var.set(json.load(f).get("main_tts_speed", "Normal")) except Exception: pass def save_main_speed(): try: data = {} if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, "r", encoding="utf-8") as f: data = json.load(f) data["main_tts_speed"] = main_speed_var.get() if main_speed_var.get() in ("Normal", "Langsam") else "Normal" with open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) except Exception: pass main_speed_var.trace_add("write", lambda *a: save_main_speed()) def transcribe_wav(wav_path: str, lang: str) -> str: with open(wav_path, "rb") as f: resp = client.audio.transcriptions.create( model="gpt-4o-mini-transcribe", file=f, language=lang, ) return getattr(resp, "text", "") or str(resp) def translate_text(text: str, from_lang: str, to_lang: str) -> str: if not text or not text.strip(): return "" from_name = LANGUAGES.get(from_lang, from_lang) to_name = LANGUAGES.get(to_lang, to_lang) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": f"Übersetze den folgenden Text von {from_name} nach {to_name}. Nur die Übersetzung ausgeben, keine Erklärungen."}, {"role": "user", "content": text.strip()}, ], ) return (resp.choices[0].message.content or "").strip() _pygame_initialized = [False] def _warmup_tts(): """Initialisiert Pygame beim Start – vermeidet Anlaufprobleme bei erster Wiedergabe.""" try: import pygame pygame.mixer.init() _pygame_initialized[0] = True except Exception: pass root.after(800, _warmup_tts) def _play_audio_in_app(path: str): """Spielt Audio direkt in dieser Anwendung ab – kein externes Programm.""" try: import pygame import time if not _pygame_initialized[0]: pygame.mixer.init() _pygame_initialized[0] = True time.sleep(0.15) pygame.mixer.music.load(path) time.sleep(0.05) pygame.mixer.music.play() while pygame.mixer.music.get_busy(): time.sleep(0.1) except ImportError: messagebox.showerror("Audio", "Für Wiedergabe in der App: py -3.11 -m pip install pygame") finally: root.after(1000, lambda p=path: _try_remove(p)) def speak_text(text: str, lang: str, speed: float = 1.0): """KI-TTS: Spricht Text natürlich in der richtigen Sprache aus.""" if not text or not text.strip(): return text = text.strip()[:4096] try: fd, path = tempfile.mkstemp(suffix=".mp3", prefix="tts_") os.close(fd) with client.audio.speech.with_streaming_response.create( model="tts-1-hd", voice="nova", input=text, speed=speed, ) as resp: resp.stream_to_file(path) import time time.sleep(0.1) _play_audio_in_app(path) return except Exception: pass # 2. gTTS – spricht in der gewählten Sprache (z.B. Italienisch) try: from gtts import gTTS fd, path = tempfile.mkstemp(suffix=".mp3", prefix="tts_") os.close(fd) gTTS(text=text[:4000], lang=lang).save(path) _play_audio_in_app(path) return except ImportError: messagebox.showerror("TTS", "Für Fallback: py -3.11 -m pip install gTTS pygame") except Exception as e: messagebox.showerror("TTS Fehler", str(e)) def _try_remove(p): try: if os.path.exists(p): os.remove(p) except Exception: pass def toggle_diktat(): if is_recording[0]: is_recording[0] = False btn_diktat.configure(text="⏺ Diktat starten") status_var.set("Transkribiere…") lang_in, _ = get_lang_codes() def worker(): try: rec = diktat_recorder[0] wav_path = rec.stop_and_save_wav() diktat_recorder[0] = None text = transcribe_wav(wav_path, lang_in) try: if os.path.exists(wav_path): os.remove(wav_path) except Exception: pass lang_in2, lang_out = get_lang_codes() increment_lang_out_usage(lang_out) translation = translate_text(text, lang_in2, lang_out) root.after(0, lambda: _done(text, translation)) except Exception as e: root.after(0, lambda: messagebox.showerror("Fehler", str(e))) root.after(0, lambda: status_var.set("Fehler.")) def _done(orig, trans): if orig: txt_left.insert("end", ("\n\n" if txt_left.get("1.0", "end").strip() else "") + orig) if trans: txt_right.insert("end", ("\n\n" if txt_right.get("1.0", "end").strip() else "") + trans) status_var.set("Fertig.") threading.Thread(target=worker, daemon=True).start() else: try: if not diktat_recorder[0]: diktat_recorder[0] = AudioRecorder() diktat_recorder[0].start() is_recording[0] = True btn_diktat.configure(text="⏹ Diktat stoppen") status_var.set("Aufnahme läuft…") except Exception as e: messagebox.showerror("Aufnahme-Fehler", str(e)) diktat_style = ttk.Style() diktat_style.configure("Diktat.TButton", font=("Segoe UI", 11), foreground="#3D6B6B", padding=(14, 8)) btn_diktat = ttk.Button(btn_row1, text="⏺ Diktat starten", command=toggle_diktat, style="Diktat.TButton") btn_diktat.pack(side="left", padx=(0, 8)) def do_speak(): text = txt_right.get("1.0", "end").strip() if not text: messagebox.showinfo("Hinweis", "Kein übersetzter Text zum Vorlesen.") return _, lang_out = get_lang_codes() sp = 0.85 if main_speed_var.get() == "Langsam" else 1.0 status_var.set("Spricht…") def w(): try: speak_text(text, lang_out, sp) root.after(0, lambda: status_var.set("Fertig.")) except Exception as e: root.after(0, lambda: messagebox.showerror("TTS Fehler", str(e))) root.after(0, lambda: status_var.set("Fehler.")) threading.Thread(target=w, daemon=True).start() speak_style = ttk.Style() speak_style.configure("Vorlesen.TButton", font=("Segoe UI", 11), foreground="#4A6B8A", padding=(14, 8)) btn_speak = ttk.Button(btn_row2, text="🔊 Vorlesen", command=do_speak, style="Vorlesen.TButton") btn_speak.pack(side="left", padx=(0, 8)) neu_style = ttk.Style() neu_style.configure("Neu.TButton", font=("Segoe UI", 11), padding=(14, 8)) def do_translate_manual(): """Manuelle Übersetzung: linker Text → rechter Text.""" text = txt_left.get("1.0", "end").strip() if not text: messagebox.showinfo("Hinweis", "Kein Text zum Übersetzen.") return lang_in, lang_out = get_lang_codes() increment_lang_out_usage(lang_out) status_var.set("Übersetze…") def w(): try: trans = translate_text(text, lang_in, lang_out) root.after(0, lambda: _upd(trans)) except Exception as e: root.after(0, lambda: messagebox.showerror("Fehler", str(e))) root.after(0, lambda: status_var.set("Fertig.")) def _upd(t): txt_right.delete("1.0", "end") txt_right.insert("1.0", t or "") threading.Thread(target=w, daemon=True).start() ttk.Button(btn_row1, text="Übersetzen", command=do_translate_manual).pack(side="left", padx=(0, 8)) def do_neu(): """Beide Textbereiche leeren für neue Runde.""" if is_recording[0] and diktat_recorder[0]: try: diktat_recorder[0].stop_and_save_wav() except Exception: pass diktat_recorder[0] = None is_recording[0] = False btn_diktat.configure(text="⏺ Diktat starten") txt_left.delete("1.0", "end") txt_right.delete("1.0", "end") status_var.set("Bereit.") btn_row3 = ttk.Frame(btn_frame, padding=(0, 4, 0, 0)) btn_row3.pack(fill="x") ttk.Button(btn_row3, text="Neu", command=do_neu, style="Neu.TButton").pack(side="left", padx=(2, 8)) LERNKARTEN_ZIELZEILELLAENGE = 70 LERNKARTEN_HTML_BG_WEISS = "#FFFFFF" LERNKARTEN_HTML_BG_HELLBLAU = "#E0F2F7" def _write_lernkarten_html(html_path, txt_content): """Schreibt aus dem Lernkarten-Text eine HTML-Datei: Original weiss, Übersetzung hellblau.""" blocks = txt_content.split("\n\n") body_parts = ['Lernkarten Sätze'] for blk in blocks: blk = blk.strip() if not blk: continue if "\n---\n" in blk: g, _, i = blk.partition("\n---\n") g, i = g.strip(), i.strip() g_esc = html_module.escape(g).replace("\n", "
\n") i_esc = html_module.escape(i).replace("\n", "
\n") body_parts.append(f'
{g_esc}
') body_parts.append(f'
{i_esc}
') else: esc = html_module.escape(blk).replace("\n", "
\n") body_parts.append(f'

{esc}

') full = "" + "\n".join(body_parts) + "" try: with open(html_path, "w", encoding="utf-8") as f: f.write(full) except Exception: pass def _split_into_sentences(text): """Teilt Text in Sätze (an . ! ? gefolgt von Leerzeichen/Zeilenumbruch).""" if not text or not text.strip(): return [] t = " ".join(text.split()) parts = re.split(r"(?<=[.!?])\s+", t) return [p.strip() for p in parts if p.strip()] def do_save_to_lernkarten(): """Diktiert/Original und Übersetzung satzweise in Lernkarten speichern. Pro Satz: Originalzeile, nächste Zeile Übersetzung, dann Absatz. Zeilen max. 70 Zeichen, Neuestes zuoberst.""" left_txt = txt_left.get("1.0", "end").strip() right_txt = txt_right.get("1.0", "end").strip() if not left_txt and not right_txt: status_var.set("Nichts zum Speichern.") return base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Lernmodus_Export") os.makedirs(base_dir, exist_ok=True) path = os.path.join(base_dir, "Diktat_Lernkarten.txt") try: lin, lou = get_lang_codes() lang_in_name = LANGUAGES.get(lin, lin).strip() lang_out_name = LANGUAGES.get(lou, lou).strip() if lang_in_name and lang_out_name: lang_in_name = lang_in_name[0].upper() + lang_in_name[1:].lower() lang_out_name = lang_out_name[0].upper() + lang_out_name[1:].lower() header = f"{lang_in_name} / {lang_out_name}" left_sentences = _split_into_sentences(left_txt) right_sentences = _split_into_sentences(right_txt) if not left_sentences and not right_sentences: left_sentences = [" ".join(left_txt.split())] if left_txt else [] right_sentences = [" ".join(right_txt.split())] if right_txt else [] n = max(len(left_sentences), len(right_sentences), 1) parts = [] for i in range(n): left_s = left_sentences[i].strip() if i < len(left_sentences) else "" right_s = right_sentences[i].strip() if i < len(right_sentences) else "" if not left_s and not right_s: continue left_wrapped = "\n".join(textwrap.wrap(left_s, width=LERNKARTEN_ZIELZEILELLAENGE, break_long_words=False)) if left_s else "" right_wrapped = "\n".join(textwrap.wrap(right_s, width=LERNKARTEN_ZIELZEILELLAENGE, break_long_words=False)) if right_s else "" block = (left_wrapped + "\n---\n" + right_wrapped).strip() if (left_wrapped or right_wrapped) else "" if block: parts.append((left_wrapped, right_wrapped)) block_txt = "\n\n".join(left_w + "\n---\n" + right_w for left_w, right_w in parts) new_block = "Lernkarten Sätze\n\n" + header + "\n\n" + block_txt content = "" if os.path.exists(path): with open(path, "r", encoding="utf-8") as f: content = f.read().rstrip() out = (new_block + "\n\n" + content) if content else new_block with open(path, "w", encoding="utf-8") as f: f.write(out) path_html = os.path.join(base_dir, "Diktat_Lernkarten.html") _write_lernkarten_html(path_html, out) status_var.set("In Lernkarten gespeichert.") except Exception as e: messagebox.showerror("Fehler", str(e)) status_var.set("Fehler beim Speichern.") ttk.Button(btn_row3, text="In Lernkarten speichern", command=do_save_to_lernkarten).pack(side="left", padx=(12, 8)) def open_lernkarten_fenster(): """Eigenes Fenster für Lernkarten wie Diskussion mit KI: Anzeige mit weiss (Deutsch) und hellblau (Übersetzung), Speichern/Laden als JSON.""" win = tk.Toplevel(root) win.title("Lernkarten – Sätze") win.transient(root) win.minsize(520, 400) win.geometry("700x500") win.configure(bg="#E8F4F8") base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Lernmodus_Export") lernkarten_dir = os.path.join(base_dir, "Gespeicherte_Lernkarten") os.makedirs(lernkarten_dir, exist_ok=True) entries = [] # [{"de": str, "it": str}, ...] current_lernkarten_path = [None] current_lernkarten_titel = [None] status_lern = tk.StringVar(value="Bereit. Laden oder aus Übersetzung hinzufügen.") chat_frame = ttk.Frame(win, padding=(12, 8)) chat_frame.pack(fill="both", expand=True) display = ScrolledText(chat_frame, wrap="word", font=("Segoe UI", 11), bg="#F5FCFF", state="disabled", height=20) display.tag_configure("lern_weiss", background="#FFFFFF") display.tag_configure("lern_hellblau", background="#E0F2F7") display.pack(fill="both", expand=True) def refresh_display(): display.configure(state="normal") display.delete("1.0", "end") for e in reversed(entries): de, it = e.get("de", "").strip(), e.get("it", "").strip() if not de and not it: continue start = display.index("end") display.insert("end", (de or "") + "\n", "lern_weiss") display.insert("end", (it or "") + "\n\n", "lern_hellblau") display.configure(state="disabled") status_lern.set(f"{len(entries)} Einträge." if entries else "Keine Einträge.") def do_lernkarten_speichern(): if not entries: messagebox.showinfo("Lernkarten speichern", "Keine Einträge zum Speichern.") return status_lern.set("Speichere…") now = datetime.now() datum = now.strftime("%d.%m.%Y") uhrzeit = now.strftime("%H:%M") path = current_lernkarten_path[0] titel = current_lernkarten_titel[0] if path and titel: data = {"titel": titel, "datum": datum, "uhrzeit": uhrzeit, "eintraege": entries} try: with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) status_lern.set(f"Aktualisiert: {titel}") except Exception as e: messagebox.showerror("Fehler", str(e)) status_lern.set("Fehler.") return try: r = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "Gib nur eine kurze deutsche Überschrift (3–8 Wörter) für diese Lernkarten-Sammlung. Keine Anführungszeichen, eine Zeile."}, {"role": "user", "content": (entries[0].get("de", "")[:500] + " …") if entries else "Lernkarten"}, ], ) titel = (r.choices[0].message.content or "Lernkarten").strip().strip('"\'') if not titel: titel = "Lernkarten" safe_titel = "".join(c for c in titel[:50] if c.isalnum() or c in " _-") or "Lernkarten" fname = f"Lernkarten_{now.strftime('%Y-%m-%d_%H-%M')}_{safe_titel}.json" path = os.path.join(lernkarten_dir, fname) data = {"titel": titel, "datum": datum, "uhrzeit": uhrzeit, "eintraege": entries} with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) current_lernkarten_path[0] = path current_lernkarten_titel[0] = titel status_lern.set(f"Gespeichert: {titel}") except Exception as e: messagebox.showerror("Fehler", str(e)) status_lern.set("Fehler.") def do_lernkarten_laden(): path = filedialog.askopenfilename( title="Lernkarten laden", initialdir=lernkarten_dir, filetypes=[("JSON", "*.json"), ("Alle", "*.*")], ) if not path: return try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) entries.clear() entries.extend(data.get("eintraege", [])) current_lernkarten_path[0] = path current_lernkarten_titel[0] = data.get("titel", "–") refresh_display() status_lern.set(f"Geladen: {data.get('titel', '–')} ({data.get('datum', '')} {data.get('uhrzeit', '')})") except Exception as e: messagebox.showerror("Lernkarten laden", str(e)) def do_aus_txt_laden(): path_txt = os.path.join(base_dir, "Diktat_Lernkarten.txt") if not os.path.isfile(path_txt): messagebox.showinfo("Lernkarten", "Diktat_Lernkarten.txt nicht gefunden.") return try: with open(path_txt, "r", encoding="utf-8") as f: content = f.read() except Exception as e: messagebox.showerror("Fehler", str(e)) return blocks = content.replace("\r", "").split("\n\n") new_entries = [] for blk in blocks: blk = blk.strip() if "\n---\n" not in blk: continue g, _, i = blk.partition("\n---\n") g, i = g.strip(), i.strip() if g or i: new_entries.append({"de": g, "it": i}) entries.clear() entries.extend(new_entries) current_lernkarten_path[0] = None current_lernkarten_titel[0] = None refresh_display() status_lern.set(f"Aus Diktat_Lernkarten.txt geladen: {len(entries)} Einträge.") def do_aus_uebersetzung(): left_txt = txt_left.get("1.0", "end").strip() right_txt = txt_right.get("1.0", "end").strip() if not left_txt and not right_txt: messagebox.showinfo("Lernkarten", "Kein Text in den Übersetzungsfeldern.") return left_s = _split_into_sentences(left_txt) right_s = _split_into_sentences(right_txt) if not left_s and not right_s: left_s = [" ".join(left_txt.split())] if left_txt else [] right_s = [" ".join(right_txt.split())] if right_txt else [] n = max(len(left_s), len(right_s), 1) for i in range(n): de = left_s[i].strip() if i < len(left_s) else "" it = right_s[i].strip() if i < len(right_s) else "" if de or it: entries.append({"de": de, "it": it}) refresh_display() status_lern.set(f"{len(entries)} Einträge (aus Übersetzung hinzugefügt).") def do_verlauf_loeschen(): entries.clear() current_lernkarten_path[0] = None current_lernkarten_titel[0] = None refresh_display() status_lern.set("Verlauf gelöscht.") btn_row = ttk.Frame(win, padding=(12, 8)) btn_row.pack(fill="x") ttk.Button(btn_row, text="Lernkarten speichern", command=do_lernkarten_speichern).pack(side="left", padx=(0, 8)) ttk.Button(btn_row, text="Lernkarten laden", command=do_lernkarten_laden).pack(side="left", padx=(0, 8)) ttk.Button(btn_row, text="Aus Diktat_Lernkarten.txt laden", command=do_aus_txt_laden).pack(side="left", padx=(0, 8)) ttk.Button(btn_row, text="Aus Übersetzung hinzufügen", command=do_aus_uebersetzung).pack(side="left", padx=(0, 8)) ttk.Button(btn_row, text="Verlauf löschen", command=do_verlauf_loeschen).pack(side="left", padx=(0, 8)) ttk.Label(win, textvariable=status_lern, font=("Segoe UI", 10)).pack(anchor="w", padx=12, pady=(0, 8)) refresh_display() ttk.Button(btn_row3, text="Lernkarten (Fenster)", command=open_lernkarten_fenster).pack(side="left", padx=(0, 8)) def open_lernkarten_ordner(): """Öffnet den Ordner Lernmodus_Export (Lernkarten aus Gespräch und Diktat).""" base_dir = os.path.dirname(os.path.abspath(__file__)) lernmodus_dir = os.path.join(base_dir, "Lernmodus_Export") os.makedirs(lernmodus_dir, exist_ok=True) try: if os.name == "nt": os.startfile(lernmodus_dir) else: import subprocess subprocess.run(["xdg-open", lernmodus_dir], check=False) except Exception: import subprocess subprocess.run(["explorer", lernmodus_dir] if os.name == "nt" else ["xdg-open", lernmodus_dir], check=False) ttk.Button(btn_row3, text="Ordner Lernkarten", command=open_lernkarten_ordner).pack(side="left", padx=(0, 8)) def start_lernkarten_abfrage(): """Startet das Lernkarten-Abfrage-Programm.""" import subprocess script_dir = os.path.dirname(os.path.abspath(__file__)) abfrage_path = os.path.join(script_dir, "lernkarten_abfrage.py") if os.path.exists(abfrage_path): subprocess.Popen([sys.executable, abfrage_path], cwd=script_dir) else: messagebox.showinfo("Lernkarten-Abfrage", "lernkarten_abfrage.py nicht gefunden.") ttk.Button(btn_row3, text="📝 Lernkarten üben", command=start_lernkarten_abfrage).pack(side="left", padx=(12, 8)) vorlesen_row = ttk.Frame(btn_frame, padding=(0, 8, 0, 0)) vorlesen_row.pack(fill="x") ttk.Label(vorlesen_row, text="Vorlesen:").pack(side="left", padx=(0, 4)) combo_main_speed = ttk.Combobox(vorlesen_row, textvariable=main_speed_var, values=["Normal", "Langsam"], state="readonly", width=8) combo_main_speed.pack(side="left", padx=(0, 8)) combo_main_speed.set(main_speed_var.get() if main_speed_var.get() in ("Normal", "Langsam") else "Normal") ttk.Button(btn_row2, text="📚 Gespräch / Lernmodus", command=lambda: open_gespraech_window(lehrer_default=True)).pack(side="left", padx=(0, 8)) def open_gespraech_window(lehrer_default=True): """Öffnet Gesprächs-Fenster: KI diskutiert mit dir, auto Vorlesen, optional Sprachen lernen.""" GESP_MIN_W, GESP_MIN_H = 680, 540 win = tk.Toplevel(root) win.title("Gespräch – KI diskutiert mit dir") gesp_lang_saved, silence_saved, speed_saved, gesp_geom_saved = load_gesp_config() win.minsize(GESP_MIN_W, GESP_MIN_H) win.geometry(_clamp_geometry(gesp_geom_saved, GESP_MIN_W, GESP_MIN_H) if gesp_geom_saved else "820x680") win.configure(bg="#E8F4F8") win.transient(root) # —— Einstellungen (kompakt, übersichtlich) —— opts_frame = ttk.LabelFrame(win, text=" Einstellungen ", padding=(10, 8)) opts_frame.pack(fill="x", padx=10, pady=(8, 4)) row1 = ttk.Frame(opts_frame) row1.pack(fill="x", pady=(0, 6)) ttk.Label(row1, text="Spreche/lerne in dieser Sprache:").pack(side="left", padx=(0, 6)) gesp_lang_var = tk.StringVar(value=gesp_lang_saved) combo = ttk.Combobox( row1, textvariable=gesp_lang_var, values=[f"{k} – {v}" for k, v in LANGUAGES_SORTED], state="readonly", width=20, ) combo.pack(side="left", padx=(0, 16)) combo_str = f"{gesp_lang_saved} – {LANGUAGES.get(gesp_lang_saved, gesp_lang_saved)}" combo.set(combo_str if gesp_lang_saved in LANGUAGES else "de – Deutsch") ttk.Label(row1, text=" Geschwindigkeit:").pack(side="left", padx=(16, 6)) speed_map = {"Normal": 1.0, "Langsam": 0.85, "Sehr langsam": 0.7} def speed_to_label(v): if v <= 0.75: return "Sehr langsam" if v < 0.95: return "Langsam" return "Normal" speed_var = tk.StringVar(value=speed_to_label(speed_saved)) combo_speed = ttk.Combobox( row1, textvariable=speed_var, values=["Normal", "Langsam", "Sehr langsam"], state="readonly", width=12, ) combo_speed.pack(side="left", padx=(0, 4)) combo_speed.set(speed_var.get() if speed_var.get() in speed_map else "Normal") # Toggle: Weitere Einstellungen (Sprachen lernen, Vorlage, Thema, Eigenes Thema) lehrer_var = tk.BooleanVar(value=lehrer_default) extra_opts_visible = [False] extra_opts_frame = ttk.Frame(opts_frame) def toggle_extra_opts(): if extra_opts_visible[0]: extra_opts_frame.pack_forget() btn_toggle_extra.configure(text="▼ Weitere Einstellungen anzeigen") extra_opts_visible[0] = False else: extra_opts_frame.pack(fill="x", pady=(6, 0)) btn_toggle_extra.configure(text="▲ Weitere Einstellungen einklappen") extra_opts_visible[0] = True btn_toggle_extra = ttk.Button(opts_frame, text="▼ Weitere Einstellungen anzeigen", command=toggle_extra_opts) btn_toggle_extra.pack(anchor="w", pady=(2, 0)) def load_saved_vorlagen(): try: if os.path.exists(VORLAGEN_PATH): with open(VORLAGEN_PATH, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass return [] def save_vorlagen(vorlagen_list): try: with open(VORLAGEN_PATH, "w", encoding="utf-8") as f: json.dump(vorlagen_list, f, indent=2, ensure_ascii=False) except Exception: pass LERN_VORLAGEN_DEFAULT = [ "Standard (Korrektur + Antwort)", "Ich möchte diese Sprache erlernen. KI stellt viele Fragen, damit das Gespräch ständig weiter fließt – nicht nur mein Input.", ] def get_all_vorlagen(): saved = load_saved_vorlagen() texts = [v.get("text", "") for v in saved if isinstance(v, dict) and v.get("text")] return LERN_VORLAGEN_DEFAULT + (texts if texts else []) row2 = ttk.Frame(extra_opts_frame) row2.pack(fill="x", pady=(0, 6)) ttk.Checkbutton(row2, text="Sprachen lernen (Lehrer-Modus)", variable=lehrer_var).pack(side="left", padx=(0, 16)) ttk.Label(row2, text="Vorlage (wie KI antwortet):").pack(side="left", padx=(0, 6)) lern_vorlage_var = tk.StringVar(value=LERN_VORLAGEN_DEFAULT[1]) combo_vorlage = ttk.Combobox( row2, textvariable=lern_vorlage_var, values=get_all_vorlagen(), width=52, ) combo_vorlage.pack(side="left", padx=(0, 4), fill="x", expand=True) combo_vorlage.set(lern_vorlage_var.get()) def open_vorlagen_dialog(): vwin = tk.Toplevel(win) vwin.title("Vorlagen verwalten") vwin.geometry("520x340") vwin.transient(win) ttk.Label(vwin, text="Gespeicherte Vorlagen (wie KI antworten soll):").pack(anchor="w", padx=10, pady=(10, 4)) list_f = ttk.Frame(vwin) list_f.pack(fill="both", expand=True, padx=10, pady=4) lb = tk.Listbox(list_f, height=8, font=("Segoe UI", 10)) lb.pack(side="left", fill="both", expand=True) sb = ttk.Scrollbar(list_f, command=lb.yview) sb.pack(side="right", fill="y") lb.configure(yscrollcommand=sb.set) saved = load_saved_vorlagen() names = [v.get("name", v.get("text", "")[:40]) for v in saved if isinstance(v, dict)] for n in names: lb.insert("end", n) btn_vf = ttk.Frame(vwin, padding=10) btn_vf.pack(fill="x") def vorlage_speichern(): txt = lern_vorlage_var.get().strip() if not txt: messagebox.showinfo("Vorlage", "Bitte zuerst eine Vorlage eingeben oder auswählen.", parent=vwin) return name = simpledialog.askstring("Name", "Name für diese Vorlage (z.B. „Stelle viele Fragen“):", parent=vwin) if not name or not name.strip(): return name = name.strip() lst = load_saved_vorlagen() lst.append({"name": name, "text": txt}) save_vorlagen(lst) lb.insert("end", name) combo_vorlage["values"] = get_all_vorlagen() messagebox.showinfo("Gespeichert", f"Vorlage „{name}“ gespeichert.", parent=vwin) def vorlage_laden(): sel = lb.curselection() if not sel: messagebox.showinfo("Vorlage", "Bitte eine Vorlage auswählen.", parent=vwin) return idx = sel[0] lst = load_saved_vorlagen() if 0 <= idx < len(lst) and isinstance(lst[idx], dict): lern_vorlage_var.set(lst[idx].get("text", "")) combo_vorlage.set(lern_vorlage_var.get()) vwin.destroy() def vorlage_loeschen(): sel = lb.curselection() if not sel: return idx = sel[0] lst = load_saved_vorlagen() if 0 <= idx < len(lst): del lst[idx] save_vorlagen(lst) lb.delete(idx) combo_vorlage["values"] = get_all_vorlagen() ttk.Button(btn_vf, text="Aktuelle Vorlage speichern", command=vorlage_speichern).pack(side="left", padx=(0, 8)) ttk.Button(btn_vf, text="Vorlage laden", command=vorlage_laden).pack(side="left", padx=(0, 8)) ttk.Button(btn_vf, text="Vorlage löschen", command=vorlage_loeschen).pack(side="left", padx=(0, 8)) ttk.Button(row2, text="Vorlagen", command=open_vorlagen_dialog).pack(side="left", padx=(4, 0)) THEMEN_VORGABEN = [ "Allgemein (kein bestimmtes Thema)", "Gespräch mit Patienten (Verhalten: liegen, stehen, Anweisungen)", "Gespräch in den Ferien / Urlaub", "Gespräch beim Einkaufen", "Am Arzt / beim Arzt", "Small Talk / Kennenlernen", "Restaurant / Bestellen", "Wegbeschreibungen / unterwegs", "Familie und Alltag", "Beruf / Arbeit", ] row3 = ttk.Frame(extra_opts_frame) row3.pack(fill="x", pady=(0, 4)) ttk.Label(row3, text="Thema:").pack(side="left", padx=(0, 6)) thema_var = tk.StringVar(value=THEMEN_VORGABEN[0]) combo_thema = ttk.Combobox( row3, textvariable=thema_var, values=THEMEN_VORGABEN, width=48, ) combo_thema.pack(side="left", padx=(0, 4), fill="x", expand=True) combo_thema.set(thema_var.get()) row4 = ttk.Frame(extra_opts_frame) row4.pack(fill="x", pady=(0, 0)) ttk.Label(row4, text="Eigenes Thema (optional):").pack(side="left", padx=(0, 6)) entry_thema_custom = tk.Entry(row4, font=("Segoe UI", 10), bg="#FFF8E8", width=50) entry_thema_custom.pack(side="left", padx=(0, 4), fill="x", expand=True) def get_tts_speed() -> float: return speed_map.get(speed_var.get(), 1.0) chat_f = ttk.Frame(win, padding=10) chat_f.pack(fill="both", expand=True) ttk.Label(chat_f, text="Gespräch:").pack(anchor="w") txt_chat = ScrolledText(chat_f, wrap="word", font=text_font, bg="#F5FCFF", height=8) txt_chat.pack(fill="both", expand=True, pady=(4, 8)) ttk.Label(chat_f, text="Deine Nachricht (oder per Diktat):").pack(anchor="w") entry_msg = tk.Text(chat_f, wrap="word", font=text_font, bg="#FFF8E8", height=2) entry_msg.pack(fill="x", pady=(4, 0)) save_trans_var = tk.BooleanVar(value=True) send_row = ttk.Frame(chat_f, padding=(0, 8, 0, 0)) send_row.pack(fill="x") status_row_gesp = ttk.Frame(win, padding=(10, 8, 10, 0)) status_row_gesp.pack(fill="x") status_gesp = tk.StringVar(value="Bereit.") status_style_gesp = ttk.Style() status_style_gesp.configure("Orange.TLabel", foreground="#E65100", font=("Segoe UI", 10, "bold")) ttk.Label(status_row_gesp, textvariable=status_gesp, width=50, anchor="w", style="Orange.TLabel").pack(side="left") btn_f = ttk.Frame(win, padding=10) btn_f.pack(fill="x") gesp_recorder = [None] gesp_recording = [False] gesp_stop_requested = [False] gesp_playing = [False] gesp_manual_stop = [False] last_ai_message = [""] def auto_start_recording(): """Startet Diktat automatisch nach KI-Vorlesen – nur wenn nicht manuell pausiert.""" try: if (not gesp_stop_requested[0] and not gesp_manual_stop[0] and not gesp_recording[0] and win.winfo_exists()): toggle_gesp_diktat() except Exception: pass def get_gesp_lang(): try: v = gesp_lang_var.get() or "" code = v.split(" – ")[0].strip() if " – " in str(v) else "de" return code if code in LANGUAGES else "de" except Exception: return "de" def save_gesp_config_now(include_geometry=False): try: geom = win.geometry() if include_geometry and win.winfo_exists() else None save_gesp_config(get_gesp_lang(), 20, get_tts_speed(), geom) except Exception: pass gesp_lang_var.trace_add("write", lambda *a: save_gesp_config_now()) speed_var.trace_add("write", lambda *a: save_gesp_config_now()) def on_gesp_close(): save_gesp_config_now(include_geometry=True) win.destroy() win.protocol("WM_DELETE_WINDOW", on_gesp_close) def transcribe_gesp(path: str, lang: str) -> str: with open(path, "rb") as f: r = client.audio.transcriptions.create( model="gpt-4o-mini-transcribe", file=f, language=lang, ) return getattr(r, "text", "") or str(r) def chat_with_ai(user_msg: str, lang: str, is_lehrer: bool, lern_vorlage: str = "", thema: str = "") -> str: lang = lang if lang in LANGUAGES else "de" lang_name = LANGUAGES.get(lang, lang) thema_add = "" if thema and thema.strip() and "Allgemein" not in thema: thema_add = ( f" PFLICHT-THEMA: {thema.strip()}. " "Du MUSST das Gespräch ZWINGEND in diesem Bereich führen. Leite und steuere das Gespräch aktiv auf dieses Thema. " "Bring dem Lernenden Vokabeln und Redewendungen zu genau diesem Bereich bei. Verlasse das Thema nicht." ) lang_rule = ( f"RIGID RULE: You are a native {lang_name} speaker. You MUST respond ONLY in {lang_name}. " f"NEVER use English, German, or any other language – only {lang_name}. " f"You NEVER switch to a German perspective or persona. You are NOT 'a German trying to speak {lang_name}' – you ARE {lang_name}. " f"Every single word you write MUST be in {lang_name}. No exceptions." ) no_repeat = ( "FORBIDDEN: NEVER repeat, echo, or write the user's sentence. It is already displayed. " "Do NOT quote the user. Do NOT say 'You said' or paraphrase their words. " "Write ONLY your response – never the user's words." ) if is_lehrer: vorlage_add = "" if lern_vorlage and "Standard" not in lern_vorlage: vorlage_add = f" ZUSÄTZLICH (Nutzerwunsch): {lern_vorlage.strip()}" sys = ( lang_rule + " " + no_repeat + thema_add + " " f"Du bist ein geduldiger {lang_name}-Lehrer (muttersprachlich). " "WICHTIG – Reihenfolge deiner Antwort: " "(1) ZUERST: Wenn der Nutzer sprachliche Fehler gemacht hat, korrigiere sie – gib NUR die richtige Form, wiederhole seinen Satz NICHT. " "(2) DANN: Gib deine inhaltliche Antwort auf das Gesagte. " "Beides kurz halten. Niemals den Satz des Nutzers schreiben oder zitieren." + vorlage_add ) else: sys = ( lang_rule + " " + no_repeat + thema_add + " " f"Du bist ein freundlicher Gesprächspartner. " "Halte deine Antworten ähnlich lang wie die des Nutzers. " "Diskutiere lebendig über verschiedene Themen." ) msgs = [] try: hist = txt_chat.get("1.0", "end").strip() except Exception: hist = "" if hist: for block in hist.split("\n\n---\n\n"): if "Du: " in block and "KI: " in block: parts = block.split("KI: ", 1) if len(parts) == 2: u = parts[0].replace("Du: ", "").strip() a = parts[1].strip() if u: msgs.append({"role": "user", "content": u}) if a: msgs.append({"role": "assistant", "content": a}) reminder = f"[Schreibe den Satz des Nutzers NIEMALS. Nur deine Antwort. Erst Fehlerkorrektur (falls nötig), dann inhaltliche Antwort. Alles auf {lang_name}.]" msgs.append({"role": "user", "content": f"{reminder}\n\n{user_msg}"}) r = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "system", "content": sys}] + msgs[-20:], ) return (r.choices[0].message.content or "").strip() def _play_gesp_audio(path: str): """Spielt MP3 ab (pygame).""" try: import pygame import time pygame.mixer.init() pygame.mixer.music.load(path) pygame.mixer.music.play() gesp_playing[0] = True try: while pygame.mixer.music.get_busy() and not gesp_stop_requested[0]: time.sleep(0.1) finally: gesp_playing[0] = False try: pygame.mixer.music.stop() pygame.mixer.quit() except Exception: pass except ImportError: pass try: if path and os.path.exists(path): os.remove(path) except Exception: pass def speak_gesp(text: str, lang: str, speed: float = None): """Spricht Text vor – in der GEWÄHLTEN Sprache (nicht Englisch), damit der „Native Speaker“ korrekt spricht.""" if not text or not text.strip() or gesp_stop_requested[0]: return t = text.strip()[:4096] sp = get_tts_speed() if speed is None else float(speed) path = None # gTTS hat expliziten lang=-Parameter → garantiert korrekte Sprache (z.B. Italienisch statt Englisch) if lang and lang != "en": try: from gtts import gTTS fd, path = tempfile.mkstemp(suffix=".mp3", prefix="tts_") os.close(fd) gTTS(text=t[:4000], lang=lang).save(path) if gesp_stop_requested[0]: return _play_gesp_audio(path) return except Exception: pass # Fallback: OpenAI TTS (kein lang-Parameter; spricht Textsprache) try: fd, path = tempfile.mkstemp(suffix=".mp3", prefix="tts_") os.close(fd) with client.audio.speech.with_streaming_response.create( model="tts-1-hd", voice="nova", input=t, speed=sp ) as resp: resp.stream_to_file(path) if gesp_stop_requested[0]: return _play_gesp_audio(path) except Exception: try: from gtts import gTTS fd, path = tempfile.mkstemp(suffix=".mp3", prefix="tts_") os.close(fd) gTTS(text=t[:4000], lang=lang if lang else "it").save(path) if not gesp_stop_requested[0]: _play_gesp_audio(path) except Exception: pass def safe_after(callback): """Führt Callback auf Main-Thread aus, nur wenn Fenster noch existiert.""" def _run(): try: if win.winfo_exists(): callback() except Exception: pass win.after(0, _run) def auto_save_vokabeln(user_msg: str, ai_msg: str, lang: str, thema: str): """Speichert automatisch 2–5 interessante Vokabeln/Kurzsätze aus dem letzten Austausch in Lernkarten.""" if not user_msg.strip() and not ai_msg.strip(): return try: lang_name = LANGUAGES.get(lang, lang) sys_extract = ( f"Extrahiere 2–5 besonders interessante, nützliche Vokabeln oder kurze Sätze aus diesem Austausch. " f"Format: Deutsch – {lang_name} (z.B. 'Guten Tag – buongiorno'). " f"Nur Zeilen im Format 'Deutsch – {lang_name}', eine pro Zeile. Kein anderer Text." ) r = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": sys_extract}, {"role": "user", "content": f"Du: {user_msg}\n\nKI: {ai_msg}"}, ], ) extracted = (r.choices[0].message.content or "").strip() if not extracted: return base_dir = os.path.dirname(os.path.abspath(__file__)) lernmodus_dir = os.path.join(base_dir, "Lernmodus_Export") os.makedirs(lernmodus_dir, exist_ok=True) safe_name = "".join(c for c in (thema or "Lernmodus")[:40] if c.isalnum() or c in " _-") or "Lernmodus" lang_out_name = lang_name.strip() if lang_out_name: lang_out_name = lang_out_name[0].upper() + lang_out_name[1:].lower() gesp_header = f"Deutsch / {lang_out_name}" new_entries = [] for line in extracted.replace("\r", "").split("\n"): line = line.strip() if " – " in line: a, b = line.split(" – ", 1) a, b = a.strip(), b.strip() if a and b: new_entries.append(f"{a} = {b}") if not new_entries: return kumulativ_path = os.path.join(lernmodus_dir, f"{safe_name}_Gesamtliste_Lernkarten.txt") content = "" if os.path.exists(kumulativ_path): with open(kumulativ_path, "r", encoding="utf-8") as f: content = f.read() lines_f = content.split("\n") out_f = [] i = 0 found_gesp = False while i < len(lines_f): line = lines_f[i] st = line.strip() is_h = (" / " in st and " = " not in st and st and not st.startswith("===") and not st.startswith("###")) if is_h and st.lower() == gesp_header.lower(): out_f.append(line) i += 1 while i < len(lines_f) and lines_f[i].strip() and " = " in lines_f[i]: out_f.append(lines_f[i]) i += 1 for e in new_entries: out_f.append(e) found_gesp = True continue out_f.append(line) i += 1 if not found_gesp: if out_f and out_f[-1].strip(): out_f.append("") out_f.append(gesp_header) out_f.extend(new_entries) with open(kumulativ_path, "w", encoding="utf-8") as f: f.write("\n".join(out_f)) except Exception: pass def append_chat(user: str, ai: str): try: last_ai_message[0] = (ai or "").strip() prev = txt_chat.get("1.0", "end").strip() sep = "\n\n---\n\n" if prev else "" txt_chat.insert("end", sep + f"Du: {user}\n\nKI: {ai}") txt_chat.see("end") entry_msg.delete("1.0", "end") except Exception: pass def do_send(): user_txt = entry_msg.get("1.0", "end").strip() if not user_txt: status_gesp.set("Bitte Nachricht eingeben oder diktieren.") return lang = get_gesp_lang() is_lehrer = lehrer_var.get() status_gesp.set("KI antwortet…") entry_msg.delete("1.0", "end") def worker(): try: if gesp_stop_requested[0]: return thema_txt = entry_thema_custom.get().strip() or thema_var.get() ai_text = chat_with_ai(user_txt, lang, is_lehrer, lern_vorlage_var.get() if is_lehrer else "", thema_txt) if gesp_stop_requested[0]: return safe_after(lambda: append_chat(user_txt, ai_text)) safe_after(lambda: status_gesp.set("Vorlesen…")) auto_save_vokabeln(user_txt, ai_text, lang, thema_txt) speak_gesp(ai_text, lang) if not gesp_stop_requested[0] and not gesp_manual_stop[0]: safe_after(lambda: status_gesp.set("Fertig. Diktat startet gleich…")) win.after(1200, auto_start_recording) elif gesp_manual_stop[0]: safe_after(lambda: status_gesp.set("Pausiert. Start drücken zum Fortsetzen.")) else: safe_after(lambda: status_gesp.set("Gestoppt.")) except Exception as e: if not gesp_stop_requested[0]: safe_after(lambda: messagebox.showerror("Fehler", str(e))) safe_after(lambda: status_gesp.set("Fehler.")) threading.Thread(target=worker, daemon=True).start() ttk.Button(send_row, text="Senden", command=do_send).pack(side="left", padx=(0, 8)) def on_gespraech_return(event): """Enter = Senden, Shift+Enter = neue Zeile.""" if event.state & 0x1: # Shift gedrückt → Zeilenumbruch wie üblich return do_send() return "break" entry_msg.bind("", on_gespraech_return) def toggle_gesp_diktat(): if gesp_recording[0]: gesp_manual_stop[0] = True gesp_recording[0] = False btn_gesp_diktat.configure(text="⏺ Diktat") status_gesp.set("Transkribiere…" if not gesp_manual_stop[0] else "Pausiert.") lang = get_gesp_lang() is_lehrer = lehrer_var.get() def worker(): try: if gesp_stop_requested[0]: return rec = gesp_recorder[0] if not rec: return wav_path = rec.stop_and_save_wav() gesp_recorder[0] = None if gesp_stop_requested[0]: return text = transcribe_gesp(wav_path, lang) try: if os.path.exists(wav_path): os.remove(wav_path) except Exception: pass if gesp_stop_requested[0]: return safe_after(lambda: status_gesp.set("KI antwortet…")) thema_txt = entry_thema_custom.get().strip() or thema_var.get() ai_text = chat_with_ai(text, lang, is_lehrer, lern_vorlage_var.get() if is_lehrer else "", thema_txt) if gesp_stop_requested[0]: return safe_after(lambda: append_chat(text, ai_text)) safe_after(lambda: status_gesp.set("Vorlesen…")) auto_save_vokabeln(text, ai_text, lang, thema_txt) speak_gesp(ai_text, lang) if not gesp_stop_requested[0] and not gesp_manual_stop[0]: safe_after(lambda: status_gesp.set("Fertig. Diktat startet gleich…")) win.after(1200, auto_start_recording) elif gesp_manual_stop[0]: safe_after(lambda: status_gesp.set("Pausiert. Start drücken zum Fortsetzen.")) else: safe_after(lambda: status_gesp.set("Gestoppt.")) except Exception as e: if not gesp_stop_requested[0]: safe_after(lambda: messagebox.showerror("Fehler", str(e))) safe_after(lambda: status_gesp.set("Fehler.")) threading.Thread(target=worker, daemon=True).start() else: try: gesp_manual_stop[0] = False if not gesp_recorder[0]: gesp_recorder[0] = AudioRecorder() gesp_recorder[0].start() gesp_recording[0] = True btn_gesp_diktat.configure(text="⏹ Stopp") status_gesp.set("Aufnahme läuft… (Stopp zum Beenden)") except Exception as e: messagebox.showerror("Aufnahme-Fehler", str(e)) btn_gesp_diktat = ttk.Button(btn_f, text="⏺ Diktat", command=toggle_gesp_diktat) btn_gesp_diktat.pack(side="left", padx=(0, 8)) def do_stop_all(): """Stoppt alles sofort: Aufnahme, Wiedergabe, pausiert Gespräch.""" gesp_stop_requested[0] = True gesp_manual_stop[0] = True if gesp_recording[0] and gesp_recorder[0]: try: gesp_recorder[0].stop_and_save_wav() except Exception: pass gesp_recorder[0] = None gesp_recording[0] = False btn_gesp_diktat.configure(text="⏺ Diktat") try: import pygame pygame.mixer.music.stop() pygame.mixer.quit() except Exception: pass gesp_playing[0] = False status_gesp.set("Gestoppt.") win.after(800, lambda: gesp_stop_requested.__setitem__(0, False)) ttk.Button(btn_f, text="⏹ Stopp alles", command=do_stop_all).pack(side="left", padx=(0, 8)) def do_gesp_neu(): gesp_stop_requested[0] = True if gesp_recording[0] and gesp_recorder[0]: try: gesp_recorder[0].stop_and_save_wav() except Exception: pass gesp_recorder[0] = None gesp_recording[0] = False btn_gesp_diktat.configure(text="⏺ Diktat") try: import pygame pygame.mixer.music.stop() pygame.mixer.quit() except Exception: pass gesp_stop_requested[0] = False try: txt_chat.delete("1.0", "end") entry_msg.delete("1.0", "end") except Exception: pass status_gesp.set("Bereit.") ttk.Button(btn_f, text="Neu", command=do_gesp_neu).pack(side="left", padx=(0, 8)) def do_nochmal_vorlesen(): """Lässt die letzte KI-Antwort noch einmal vorlesen.""" txt = last_ai_message[0] if not txt: status_gesp.set("Nichts zum Vorlesen – zuerst eine KI-Antwort abwarten.") return status_gesp.set("Nochmal vorlesen…") def worker(): speak_gesp(txt, get_gesp_lang()) safe_after(lambda: status_gesp.set("Fertig.")) threading.Thread(target=worker, daemon=True).start() ttk.Button(btn_f, text="Nochmal vorlesen", command=do_nochmal_vorlesen).pack(side="left", padx=(0, 8)) current_gespraech_path = [None] current_gespraech_titel = [None] def do_gespraech_speichern(): """Gespräch als JSON speichern. Wenn geladen oder schon gespeichert: gleiche Datei aktualisieren, sonst neue Datei.""" hist = txt_chat.get("1.0", "end").strip() if not hist: messagebox.showinfo("Gespräch speichern", "Kein Gespräch zum Speichern.") return status_gesp.set("Speichere Gespräch…") lang = get_gesp_lang() lang_name = LANGUAGES.get(lang, lang) def worker(): try: now = datetime.now() datum = now.strftime("%d.%m.%Y") uhrzeit = now.strftime("%H:%M") path = current_gespraech_path[0] titel = current_gespraech_titel[0] if path and titel: # Weitergeführtes Gespräch: bestehende Datei aktualisieren data = { "titel": titel, "datum": datum, "uhrzeit": uhrzeit, "sprache": lang_name, "sprache_code": lang, "chat": hist, } with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) safe_after(lambda: status_gesp.set(f"Gespräch aktualisiert: {titel}")) return # Neue Gespräch: KI für Überschrift, neue Datei r = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "Gib nur eine kurze deutsche Überschrift (3–8 Wörter) für dieses Gespräch. Keine Anführungszeichen, nur die Überschrift, eine Zeile."}, {"role": "user", "content": hist[:3000]}, ], ) titel = (r.choices[0].message.content or "Gespräch").strip().strip('"\'') if not titel: titel = "Gespräch" base_dir = os.path.dirname(os.path.abspath(__file__)) gesp_dir = os.path.join(base_dir, "Lernmodus_Export", "Gespeicherte_Gespraeche") os.makedirs(gesp_dir, exist_ok=True) safe_titel = "".join(c for c in titel[:50] if c.isalnum() or c in " _-") or "Gespraech" fname = f"Gespraech_{now.strftime('%Y-%m-%d_%H-%M')}_{safe_titel}.json" path = os.path.join(gesp_dir, fname) data = { "titel": titel, "datum": datum, "uhrzeit": uhrzeit, "sprache": lang_name, "sprache_code": lang, "chat": hist, } with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) current_gespraech_path[0] = path current_gespraech_titel[0] = titel safe_after(lambda: status_gesp.set(f"Gespräch gespeichert: {titel}")) except Exception as e: safe_after(lambda: messagebox.showerror("Fehler", str(e))) safe_after(lambda: status_gesp.set("Fehler beim Speichern.")) threading.Thread(target=worker, daemon=True).start() def do_gespraech_laden(): """Gespräch aus JSON-Datei laden.""" base_dir = os.path.dirname(os.path.abspath(__file__)) gesp_dir = os.path.join(base_dir, "Lernmodus_Export", "Gespeicherte_Gespraeche") if not os.path.isdir(gesp_dir): os.makedirs(gesp_dir, exist_ok=True) path = filedialog.askopenfilename( title="Gespräch laden", initialdir=gesp_dir, filetypes=[("JSON", "*.json"), ("Alle", "*.*")], ) if not path: return try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) chat_text = data.get("chat", "") if not chat_text: messagebox.showwarning("Gespräch laden", "In der Datei ist kein Gesprächsinhalt.") return txt_chat.delete("1.0", "end") txt_chat.insert("1.0", chat_text) titel = data.get("titel", "–") datum = data.get("datum", "") uhrzeit = data.get("uhrzeit", "") current_gespraech_path[0] = path current_gespraech_titel[0] = titel status_gesp.set(f"Geladen: {titel} ({datum} {uhrzeit})") except Exception as e: messagebox.showerror("Gespräch laden", str(e)) ttk.Button(btn_f, text="Gespräch speichern", command=do_gespraech_speichern).pack(side="left", padx=(0, 8)) ttk.Button(btn_f, text="Gespräch laden", command=do_gespraech_laden).pack(side="left", padx=(0, 8)) def open_export_ordner(): """Öffnet den Lernmodus-Export-Ordner im Explorer.""" base_dir = os.path.dirname(os.path.abspath(__file__)) lernmodus_dir = os.path.join(base_dir, "Lernmodus_Export") os.makedirs(lernmodus_dir, exist_ok=True) try: if os.name == "nt": os.startfile(lernmodus_dir) else: import subprocess subprocess.run(["xdg-open", lernmodus_dir], check=False) except Exception: import subprocess subprocess.run(["explorer", lernmodus_dir] if os.name == "nt" else ["xdg-open", lernmodus_dir], check=False) def do_speichern(): """Speichert Wortschatz und Korrekturen – als Text oder JSON in Lernmodus-Ordner.""" hist = txt_chat.get("1.0", "end").strip() if not hist: messagebox.showinfo("Speichern", "Kein Gespräch zum Speichern.") return thema = thema_var.get().strip() or "Lernmodus" mit_uebersetzung = save_trans_var.get() lang = get_gesp_lang() lang_name = LANGUAGES.get(lang, lang) status_gesp.set("Speichere…") def worker(): try: sys_extract = ( f"Du bist ein Sprachlehrer. Extrahiere aus dem Gespräch:\n" f"1. KORREKTUREN (Priorität!): Jede sprachliche Korrektur als 'falsch → richtig – Übersetzung'.\n" f"2. VOKABELN: 5–15 wichtige Wörter/Phrasen.\n\n" ) if mit_uebersetzung: sys_extract += ( "WICHTIG – Reihenfolge: Zuerst MUTTERSPRACHE (Deutsch, links), dann Fremdsprache (rechts). " "Format: Deutsch – " + lang_name + " (z.B. 'Guten Tag – buongiorno', nicht umgekehrt).\n\n" ) sys_extract += "Antworte auf " + lang_name + ". Struktur:\n" sys_extract += "=== KORREKTUREN ===\nFalsch → Richtig – Deutsche Übersetzung (je Zeile)\n=== VOKABELN ===\nDeutsch – " + lang_name + " (je Zeile, Muttersprache links)\n" r = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": sys_extract}, {"role": "user", "content": hist}, ], ) extracted = (r.choices[0].message.content or "").strip() if not extracted: safe_after(lambda: messagebox.showwarning("Speichern", "Nichts zum Speichern extrahiert.")) return now = datetime.now() datum = now.strftime("%Y-%m-%d") uhrzeit = now.strftime("%H:%M") base_dir = os.path.dirname(os.path.abspath(__file__)) lernmodus_dir = os.path.join(base_dir, "Lernmodus_Export") os.makedirs(lernmodus_dir, exist_ok=True) safe_name = "".join(c for c in thema[:40] if c.isalnum() or c in " _-") or "Lernmodus" header_txt = f"{thema}\nDatum: {datum} Uhrzeit: {uhrzeit}\nSprache: {lang_name}\n{'=' * 50}\n\n" base_name_heute = f"{safe_name}_{datum}_{now.strftime('%H%M')}" fmt = "Lernkarten" if fmt == "json": data = { "thema": thema, "datum": datum, "uhrzeit": uhrzeit, "sprache": lang_name, "vokabeln": [], "korrekturen": [], "volltext": extracted, } lines = extracted.replace("\r", "").split("\n") section = None for line in lines: if "KORREKTUR" in line.upper(): section = "korrekturen" continue if "VOKABEL" in line.upper() or "WORT" in line.upper(): section = "vokabeln" continue line = line.strip() if line and section and not line.startswith("==="): data[section].append(line) save_path = os.path.join(lernmodus_dir, base_name_heute + ".json") with open(save_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) kumulativ_path = os.path.join(lernmodus_dir, f"{safe_name}_Gesamtliste.json") try: old_data = json.load(open(kumulativ_path, encoding="utf-8")) if os.path.exists(kumulativ_path) else {"eintraege": []} except Exception: old_data = {"eintraege": []} old_data.setdefault("eintraege", []).append({"datum": datum, "uhrzeit": uhrzeit, "korrekturen": data["korrekturen"], "vokabeln": data["vokabeln"]}) with open(kumulativ_path, "w", encoding="utf-8") as f: json.dump(old_data, f, indent=2, ensure_ascii=False) elif fmt == "Lernkarten": lines = extracted.replace("\r", "").split("\n") vokabeln, korrekturen = [], [] section = None for line in lines: if "KORREKTUR" in line.upper(): section = "k" continue if "VOKABEL" in line.upper() or "WORT" in line.upper(): section = "v" continue line = line.strip() if line and section and not line.startswith("==="): if section == "v": vokabeln.append(line) else: korrekturen.append(line) save_path = os.path.join(lernmodus_dir, base_name_heute + "_Lernkarten.txt") with open(save_path, "w", encoding="utf-8") as f: f.write(header_txt) f.write("=== VOKABELN (Vorderseite | Rückseite) ===\n\n") for v in vokabeln: if " – " in v: a, b = v.split(" – ", 1) # Muttersprache (links) = Vorderseite, Fremdsprache (rechts) = Rückseite f.write(f"Vorderseite: {a.strip()}\nRückseite: {b.strip()}\n\n") else: f.write(f"Vorderseite: {v}\nRückseite: (Übersetzung eintragen)\n\n") f.write("=== KORREKTUREN (Falsch | Richtig) ===\n\n") for k in korrekturen: if "→" in k: a, b = k.split("→", 1) f.write(f"Falsch: {a.strip()}\nRichtig: {b.strip()}\n\n") else: f.write(f"{k}\n\n") kumulativ_path = os.path.join(lernmodus_dir, f"{safe_name}_Gesamtliste_Lernkarten.txt") lang_out_name = LANGUAGES.get(lang, lang).strip() if lang_out_name: lang_out_name = lang_out_name[0].upper() + lang_out_name[1:].lower() gesp_header = f"Deutsch / {lang_out_name}" # Format: Muttersprache (links) – Fremdsprache (rechts), Zeilen max. 100 Zeichen, Neuestes zuoberst new_entries_raw = [f"{a.strip()} = {b.strip()}" for v in vokabeln if " – " in v for a, b in [v.split(" – ", 1)]] new_entries_wrapped = [] for e in new_entries_raw: new_entries_wrapped.extend(textwrap.wrap(e, width=LERNKARTEN_ZIELZEILELLAENGE, break_long_words=False)) content = "" if os.path.exists(kumulativ_path): with open(kumulativ_path, "r", encoding="utf-8") as f: content = f.read() lines_f = content.split("\n") out_f = [] i = 0 found_gesp = False while i < len(lines_f): line = lines_f[i] st = line.strip() is_h = (" / " in st and " = " not in st and st and not st.startswith("===") and not st.startswith("###")) if is_h and st.lower() == gesp_header.lower(): out_f.append(line) i += 1 old_section_lines = [] while i < len(lines_f) and lines_f[i].strip() and " = " in lines_f[i]: old_section_lines.append(lines_f[i]) i += 1 for e in new_entries_wrapped: out_f.append(e) out_f.extend(old_section_lines) found_gesp = True continue out_f.append(line) i += 1 if not found_gesp: if out_f and out_f[-1].strip(): out_f.append("") out_f.append(gesp_header) out_f.extend(new_entries_wrapped) with open(kumulativ_path, "w", encoding="utf-8") as f: f.write("\n".join(out_f)) else: save_path_heute = os.path.join(lernmodus_dir, base_name_heute + ".txt") with open(save_path_heute, "w", encoding="utf-8") as f: f.write(header_txt) f.write(extracted) kumulativ_path = os.path.join(lernmodus_dir, f"{safe_name}_Gesamtliste.txt") with open(kumulativ_path, "a", encoding="utf-8") as f: f.write(f"\n\n### {datum} {uhrzeit} ###\n\n") f.write(extracted) save_path = save_path_heute safe_after(lambda: status_gesp.set("Gespeichert.")) except Exception as e: safe_after(lambda: messagebox.showerror("Fehler", str(e))) safe_after(lambda: status_gesp.set("Fehler.")) threading.Thread(target=worker, daemon=True).start() export_row = ttk.Frame(win, padding=(10, 0, 10, 10)) export_row.pack(fill="x") ttk.Button(export_row, text="📁 Ordner Lernkarten", command=open_export_ordner).pack(side="left", padx=(0, 8)) ttk.Button(export_row, text="💾 Gespräch für Lernkarten speichern", command=do_speichern).pack(side="left", padx=(0, 8)) ttk.Button(export_row, text="📝 Lernkarten üben", command=start_lernkarten_abfrage).pack(side="left", padx=(0, 8)) root.mainloop() if __name__ == "__main__": # Gleiche Keygen-Lizenz wie basis14 (ein Schlüssel für KG-Diktat + Translate) load_dotenv() try: from keygen_license import show_license_dialog_and_exit_if_invalid show_license_dialog_and_exit_if_invalid("KG-Diktat / Translate") except ImportError: pass main()