Files
aza/AzA march 2026/translate - Kopie (6).py
2026-03-25 22:03:39 +01:00

2109 lines
94 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 (AZ)
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_text_font_size(key: str, default: int = 10) -> int:
"""Lädt gespeicherte Schriftgröße aus translate_config.json."""
try:
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
font_sizes = data.get("font_sizes", {})
return int(font_sizes.get(key, default))
except Exception:
pass
return default
def save_text_font_size(key: str, size: int):
"""Speichert Schriftgröße in translate_config.json."""
try:
data = {}
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
if "font_sizes" not in data:
data["font_sizes"] = {}
data["font_sizes"][key] = int(size)
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception:
pass
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="#C5E8F2")
# Style für hellblauen Hintergrund
style = ttk.Style()
style.configure("TFrame", background="#C5E8F2")
style.configure("TLabel", background="#C5E8F2")
style.configure("TButton", background="#7EC8E3")
# PanedWindow-Sash (Trennbalken) unsichtbar machen
style.configure("TPanedwindow", background="#C5E8F2")
style.map("TPanedwindow", background=[("active", "#C5E8F2"), ("!active", "#C5E8F2")])
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("<<ComboboxSelected>>", _on_lang_selected)
combo_out.bind("<<ComboboxSelected>>", _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)
# Linkes Textfeld mit Schriftgrößen-Spinbox
left_header = ttk.Frame(left_f)
left_header.pack(fill="x", anchor="w")
ttk.Label(left_header, text="Diktiert / Original:").pack(side="left")
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))
# Schriftgrößen-Spinbox für linkes Textfeld
def add_text_size_spinbox(parent, txt_widget, initial=10, bg="#E1EDF5", save_key=None):
# Lade gespeicherte Größe
if save_key:
initial = load_text_font_size(save_key, initial)
cf = tk.Frame(parent, bg=bg)
cf.pack(side="right", padx=4)
tk.Label(cf, text="Aa", font=("Segoe UI", 9), bg=bg).pack(side="left", padx=2)
sv = tk.IntVar(value=initial)
def upd():
s = sv.get()
txt_widget.configure(font=("Segoe UI", max(5, min(12, s))))
# Automatisch speichern
if save_key:
save_text_font_size(save_key, s)
# Initiale Schriftgröße setzen
txt_widget.configure(font=("Segoe UI", initial))
sp = tk.Spinbox(cf, from_=5, to=12, textvariable=sv, width=4, command=upd,
bg=bg, readonlybackground=bg, buttonbackground=bg, relief="solid", bd=1)
sp.pack(side="left")
sp.bind("<Return>", lambda e: upd())
sp.bind("<FocusOut>", lambda e: upd())
add_text_size_spinbox(left_header, txt_left, 10, "#E1EDF5", save_key="translate_left")
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")
# Rechtes Textfeld mit Schriftgrößen-Spinbox
right_header = ttk.Frame(right_f)
right_header.pack(fill="x", anchor="w")
ttk.Label(right_header, text="Übersetzung:").pack(side="left")
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))
add_text_size_spinbox(right_header, txt_right, 10, "#EBF3FA", save_key="translate_right")
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()
# Statusbalken Hintergrund anpassen
status_style.configure("Status.TLabel", foreground="#5A6C7D", font=("Segoe UI", 10), background="#C5E8F2")
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", 12), 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", 12), 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", 12), 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 = ['<meta charset="utf-8"><title>Lernkarten Sätze</title>']
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", "<br>\n")
i_esc = html_module.escape(i).replace("\n", "<br>\n")
body_parts.append(f'<div style="background:{LERNKARTEN_HTML_BG_WEISS}; padding:8px; margin:6px 0;">{g_esc}</div>')
body_parts.append(f'<div style="background:{LERNKARTEN_HTML_BG_HELLBLAU}; padding:8px; margin:6px 0;">{i_esc}</div>')
else:
esc = html_module.escape(blk).replace("\n", "<br>\n")
body_parts.append(f'<p style="margin:8px 0; font-weight:bold;">{esc}</p>')
full = "<!DOCTYPE html><html><head></head><body>" + "\n".join(body_parts) + "</body></html>"
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 (38 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 25 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 25 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("<Return>", 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 (38 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: 515 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()