2026-03-25 22:03:39 +01:00
# -*- 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
import numpy as _np_module
try :
import sounddevice as _sd_module
except Exception :
_sd_module = None
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
# ========== Textfeld-Schriftgröße mit ▲▼-Pfeilen ==========
_FONT_SIZE_SETTINGS_FILE = os . path . join ( os . path . dirname ( os . path . abspath ( __file__ ) ) , " translate_font_sizes.json " )
def _load_font_sizes ( ) :
try :
if os . path . isfile ( _FONT_SIZE_SETTINGS_FILE ) :
with open ( _FONT_SIZE_SETTINGS_FILE , " r " , encoding = " utf-8 " ) as f :
return json . load ( f )
except Exception :
pass
return { }
def _save_font_size ( key , size ) :
data = _load_font_sizes ( )
data [ key ] = size
try :
with open ( _FONT_SIZE_SETTINGS_FILE , " w " , encoding = " utf-8 " ) as f :
json . dump ( data , f , indent = 2 )
except Exception :
pass
def add_text_font_size_control ( parent_frame , text_widget , initial_size = 10 , label = " Aa " , bg_color = " #F5FCFF " , save_key = None ) :
""" ▲▼-Pfeile für Textfeld-Schriftgröße (5– 20pt), unauffällig im Hintergrund. """
if save_key :
saved = _load_font_sizes ( ) . get ( save_key )
if saved is not None :
initial_size = int ( saved )
_size = [ max ( 5 , min ( 20 , initial_size ) ) ]
_fg = " #8AAFC0 "
_fg_hover = " #1a4d6d "
cf = tk . Frame ( parent_frame , bg = bg_color , highlightthickness = 0 , bd = 0 )
cf . pack ( side = " right " , padx = 4 )
tk . Label ( cf , text = label , font = ( " Segoe UI " , 8 ) , bg = bg_color , fg = _fg ) . pack ( side = " left " , padx = ( 0 , 1 ) )
size_lbl = tk . Label ( cf , text = str ( _size [ 0 ] ) , font = ( " Segoe UI " , 8 ) , bg = bg_color , fg = _fg , width = 2 , anchor = " center " )
size_lbl . pack ( side = " left " )
def _apply ( ns ) :
ns = max ( 5 , min ( 20 , ns ) )
_size [ 0 ] = ns
size_lbl . configure ( text = str ( ns ) )
text_widget . configure ( font = ( " Segoe UI " , ns ) )
if save_key :
_save_font_size ( save_key , ns )
text_widget . configure ( font = ( " Segoe UI " , _size [ 0 ] ) )
btn_up = tk . Label ( cf , text = " \u25B2 " , font = ( " Segoe UI " , 7 ) , bg = bg_color , fg = _fg , cursor = " hand2 " , bd = 0 , highlightthickness = 0 )
btn_up . pack ( side = " left " , padx = ( 2 , 0 ) )
btn_down = tk . Label ( cf , text = " \u25BC " , font = ( " Segoe UI " , 7 ) , bg = bg_color , fg = _fg , cursor = " hand2 " , bd = 0 , highlightthickness = 0 )
btn_down . pack ( side = " left " )
btn_up . bind ( " <Button-1> " , lambda e : _apply ( _size [ 0 ] + 1 ) )
btn_down . bind ( " <Button-1> " , lambda e : _apply ( _size [ 0 ] - 1 ) )
for w in ( btn_up , btn_down ) :
w . bind ( " <Enter> " , lambda e , ww = w : ww . configure ( fg = _fg_hover ) )
w . bind ( " <Leave> " , lambda e , ww = w : ww . configure ( fg = _fg ) )
return _size
# ==========================================================
# -----------------------------
# 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 ) :
if _sd_module is None :
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_module . 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
audio = _np_module . 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. " )
audio = _np_module . concatenate ( self . _frames , axis = 0 )
audio = _np_module . clip ( audio , - 1.0 , 1.0 )
pcm16 = ( audio * 32767.0 ) . astype ( _np_module . 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
# -----------------------------
_BASE_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 " ,
" ro " : " Rumänisch " ,
" hu " : " Ungarisch " ,
}
ALL_GOOGLE_LANGUAGES = {
" af " : " Afrikaans " , " am " : " Amharisch " , " ar " : " Arabisch " , " az " : " Aserbaidschanisch " ,
" be " : " Belarussisch " , " bg " : " Bulgarisch " , " bn " : " Bengalisch " , " bs " : " Bosnisch " ,
" ca " : " Katalanisch " , " ceb " : " Cebuano " , " co " : " Korsisch " , " cs " : " Tschechisch " ,
" cy " : " Walisisch " , " da " : " Dänisch " , " de " : " Deutsch " , " el " : " Griechisch " ,
" en " : " Englisch " , " eo " : " Esperanto " , " es " : " Spanisch " , " et " : " Estnisch " ,
" eu " : " Baskisch " , " fa " : " Persisch " , " fi " : " Finnisch " , " fr " : " Französisch " ,
" fy " : " Friesisch " , " ga " : " Irisch " , " gd " : " Schottisch-Gälisch " , " gl " : " Galicisch " ,
" gu " : " Gujarati " , " ha " : " Hausa " , " haw " : " Hawaiisch " , " he " : " Hebräisch " ,
" hi " : " Hindi " , " hmn " : " Hmong " , " hr " : " Kroatisch " , " ht " : " Haitianisch " ,
" hu " : " Ungarisch " , " hy " : " Armenisch " , " id " : " Indonesisch " , " ig " : " Igbo " ,
" is " : " Isländisch " , " it " : " Italienisch " , " ja " : " Japanisch " , " jw " : " Javanisch " ,
" ka " : " Georgisch " , " kk " : " Kasachisch " , " km " : " Khmer " , " kn " : " Kannada " ,
" ko " : " Koreanisch " , " ku " : " Kurdisch " , " ky " : " Kirgisisch " , " la " : " Latein " ,
" lb " : " Luxemburgisch " , " lo " : " Laotisch " , " lt " : " Litauisch " , " lv " : " Lettisch " ,
" mg " : " Madagassisch " , " mi " : " Maori " , " mk " : " Mazedonisch " , " ml " : " Malayalam " ,
" mn " : " Mongolisch " , " mr " : " Marathi " , " ms " : " Malaiisch " , " mt " : " Maltesisch " ,
" my " : " Birmanisch " , " ne " : " Nepalesisch " , " nl " : " Niederländisch " , " no " : " Norwegisch " ,
" ny " : " Chichewa " , " or " : " Odia " , " pa " : " Punjabi " , " pl " : " Polnisch " ,
" ps " : " Paschtu " , " pt " : " Portugiesisch " , " ro " : " Rumänisch " , " ru " : " Russisch " ,
" rw " : " Kinyarwanda " , " sd " : " Sindhi " , " si " : " Singhalesisch " , " sk " : " Slowakisch " ,
" sl " : " Slowenisch " , " sm " : " Samoanisch " , " sn " : " Shona " , " so " : " Somali " ,
" sq " : " Albanisch " , " sr " : " Serbisch " , " st " : " Sesotho " , " su " : " Sundanesisch " ,
" sv " : " Schwedisch " , " sw " : " Suaheli " , " ta " : " Tamil " , " te " : " Telugu " ,
" tg " : " Tadschikisch " , " th " : " Thailändisch " , " tk " : " Turkmenisch " , " tl " : " Filipino " ,
" tr " : " Türkisch " , " tt " : " Tatarisch " , " ug " : " Uigurisch " , " uk " : " Ukrainisch " ,
" ur " : " Urdu " , " uz " : " Usbekisch " , " vi " : " Vietnamesisch " , " xh " : " Xhosa " ,
" yi " : " Jiddisch " , " yo " : " Yoruba " , " zh " : " Chinesisch " , " zu " : " Zulu " ,
}
def _extra_langs_path ( ) - > str :
appdata = os . environ . get ( " APPDATA " )
if appdata :
d = os . path . join ( appdata , " AZA Desktop " , " data " )
else :
d = os . path . dirname ( os . path . abspath ( __file__ ) )
os . makedirs ( d , exist_ok = True )
return os . path . join ( d , " extra_languages.json " )
def load_extra_languages ( ) - > list :
try :
p = _extra_langs_path ( )
if os . path . isfile ( p ) :
with open ( p , " r " , encoding = " utf-8 " ) as f :
codes = json . load ( f )
return [ c for c in codes if c in ALL_GOOGLE_LANGUAGES ]
except Exception :
pass
return [ ]
def save_extra_languages ( codes : list ) :
try :
p = _extra_langs_path ( )
with open ( p , " w " , encoding = " utf-8 " ) as f :
json . dump ( sorted ( set ( codes ) ) , f , ensure_ascii = False )
except Exception :
pass
def _build_languages ( ) :
merged = dict ( _BASE_LANGUAGES )
for code in load_extra_languages ( ) :
if code in ALL_GOOGLE_LANGUAGES :
merged [ code ] = ALL_GOOGLE_LANGUAGES [ code ]
return merged
LANGUAGES = _build_languages ( )
LANGUAGES_SORTED = sorted ( LANGUAGES . items ( ) , key = lambda x : x [ 1 ] )
def _refresh_languages ( ) :
global LANGUAGES , LANGUAGES_SORTED
LANGUAGES = _build_languages ( )
LANGUAGES_SORTED = sorted ( LANGUAGES . items ( ) , key = lambda x : x [ 1 ] )
# Konfigurationsdatei für letzte Einstellungen
def _config_dir ( ) - > str :
appdata = os . environ . get ( " APPDATA " )
if appdata :
d = os . path . join ( appdata , " AZA Desktop " , " data " )
else :
d = os . path . dirname ( os . path . abspath ( __file__ ) )
os . makedirs ( d , exist_ok = True )
return d
_SCRIPT_DIR = os . path . dirname ( os . path . abspath ( __file__ ) )
CONFIG_PATH = os . path . join ( _config_dir ( ) , " translate_config.json " )
VORLAGEN_PATH = os . path . join ( _config_dir ( ) , " gespraech_vorlagen.json " )
# Einmalige Migration: alte Config aus Installationsverzeichnis übernehmen
def _migrate_old_config ( ) :
old = os . path . join ( _SCRIPT_DIR , " translate_config.json " )
if old != CONFIG_PATH and os . path . isfile ( old ) and not os . path . isfile ( CONFIG_PATH ) :
try :
import shutil
shutil . copy2 ( old , CONFIG_PATH )
except Exception :
pass
old_v = os . path . join ( _SCRIPT_DIR , " gespraech_vorlagen.json " )
if old_v != VORLAGEN_PATH and os . path . isfile ( old_v ) and not os . path . isfile ( VORLAGEN_PATH ) :
try :
import shutil
shutil . copy2 ( old_v , VORLAGEN_PATH )
except Exception :
pass
_migrate_old_config ( )
_MAX_HISTORY_TERMS = 10
def _language_history_path ( ) - > str :
appdata = os . environ . get ( " APPDATA " )
if appdata :
d = os . path . join ( appdata , " AZA Desktop " , " data " )
else :
d = os . path . dirname ( os . path . abspath ( __file__ ) )
os . makedirs ( d , exist_ok = True )
return os . path . join ( d , " language_history.json " )
def load_language_history ( ) - > dict :
try :
p = _language_history_path ( )
if os . path . isfile ( p ) :
with open ( p , " r " , encoding = " utf-8 " ) as f :
return json . load ( f )
except Exception :
pass
return { " terms " : [ ] , " last_context " : " " }
def save_language_history ( terms : list [ str ] , context : str = " " ) :
data = {
" terms " : terms [ - _MAX_HISTORY_TERMS : ] ,
" last_context " : ( context or " " ) [ : 2000 ] ,
" updated " : datetime . now ( ) . isoformat ( timespec = " seconds " ) ,
}
try :
with open ( _language_history_path ( ) , " w " , encoding = " utf-8 " ) as f :
json . dump ( data , f , ensure_ascii = False , indent = 2 )
except Exception :
pass
def build_history_prompt_extension ( ) - > str :
hist = load_language_history ( )
terms = hist . get ( " terms " , [ ] )
ctx = hist . get ( " last_context " , " " )
if not terms and not ctx :
return " "
parts = [ ]
if terms :
parts . append ( f " Erinnere dich an die zuletzt gelernten Begriffe: { ' , ' . join ( terms ) } . " )
parts . append ( " Knuepfe wenn passend daran an und verwende sie im Gespraech. " )
if ctx :
parts . append ( f " Letzter Diskussionskontext: { ctx } " )
return " " . join ( parts )
def extract_terms_from_exchange ( user_msg : str , ai_msg : str ) - > list [ str ] :
terms = [ ]
combined = f " { user_msg } \n { ai_msg } "
for line in combined . replace ( " \r " , " " ) . split ( " \n " ) :
line = line . strip ( )
if " – " in line :
_ , term = line . split ( " – " , 1 )
term = term . strip ( )
if term and len ( term ) < 60 :
terms . append ( term )
elif " = " in line :
_ , term = line . split ( " = " , 1 )
term = term . strip ( )
if term and len ( term ) < 60 :
terms . append ( term )
return terms
2026-04-20 14:38:16 +02:00
def load_text_font_size ( key : str , default : int = 9 ) - > int :
2026-03-25 22:03:39 +01:00
""" 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 ALL_GOOGLE_LANGUAGES and lang_out in ALL_GOOGLE_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 ALL_GOOGLE_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 ALL_GOOGLE_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 ALL_GOOGLE_LANGUAGES else " de "
data [ " lang_out " ] = lang_out if lang_out in ALL_GOOGLE_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 ( parent = None ) :
""" Startet das Übersetzer-Modul.
parent : Wenn angegeben ( Tk - Window ) , wird ein Toplevel statt eigenem Tk ( ) erstellt
und kein eigener mainloop ( ) aufgerufen ( Embedded - Modus ) . """
load_dotenv ( )
2026-04-16 13:32:32 +02:00
try :
from aza_ai_client import get_ai_client
client = get_ai_client ( )
except Exception :
api_key = os . getenv ( " OPENAI_API_KEY " , " " ) . strip ( )
if not api_key :
try :
from openai_runtime_config import get_openai_api_key
api_key = get_openai_api_key ( ) or " "
except Exception :
pass
if not api_key :
messagebox . showerror ( " Fehler " , " KI-Verbindung nicht verfügbar. \n \n Weder Backend noch lokaler API-Key konfiguriert. " )
return
client = OpenAI ( api_key = api_key )
2026-03-25 22:03:39 +01:00
try :
def_font = tkfont . nametofont ( " TkDefaultFont " )
text_font = ( def_font . actual ( ) [ " family " ] , def_font . actual ( ) [ " size " ] )
except Exception :
text_font = ( " Segoe UI " , 10 )
_embedded = parent is not None
MAIN_MIN_W , MAIN_MIN_H = 720 , 560
if _embedded :
root = tk . Toplevel ( parent )
else :
root = tk . Tk ( )
root . title ( " AZA von Arzt zu Arzt " )
root . minsize ( MAIN_MIN_W , MAIN_MIN_H )
_saved_g = load_main_geometry ( )
root . geometry ( _saved_g )
if " + " not in _saved_g :
root . update_idletasks ( )
sw = root . winfo_screenwidth ( )
sh = root . winfo_screenheight ( )
root . geometry ( f " + { ( sw - MAIN_MIN_W ) / / 2 } + { ( sh - MAIN_MIN_H ) / / 2 } " )
root . configure ( bg = " #C5E8F2 " )
root . attributes ( " -topmost " , True )
# 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 ( )
2026-04-20 14:38:16 +02:00
lin = in_val . split ( " \u2013 " ) [ 0 ] . strip ( ) if " \u2013 " in in_val else in_val
lou = out_val . split ( " \u2013 " ) [ 0 ] . strip ( ) if " \u2013 " in out_val else out_val
2026-03-25 22:03:39 +01:00
if lin not in ALL_GOOGLE_LANGUAGES :
lin = " de "
if lou not in ALL_GOOGLE_LANGUAGES :
2026-04-20 14:38:16 +02:00
lou = load_main_languages ( ) [ 1 ]
2026-03-25 22:03:39 +01:00
save_main_languages ( lin , lou )
except Exception :
pass
root . destroy ( )
root . protocol ( " WM_DELETE_WINDOW " , on_main_close )
try :
from aza_global_paste import start_global_right_click_paste_listener
start_global_right_click_paste_listener ( )
except Exception :
pass
def _make_feedback_btn ( parent , text , command , bg = " #5B8DB3 " , fg = " white " ,
active_bg = " #3A6D93 " , font_spec = ( " Segoe UI " , 11 , " bold " ) ,
padx = 16 , pady = 6 , * * kw ) :
""" Erzeugt einen tk.Button mit visuellem Doppelfeedback (press/release Farbwechsel). """
btn = tk . Button ( parent , text = text , font = font_spec ,
bg = bg , fg = fg , activebackground = active_bg , activeforeground = fg ,
relief = " flat " , bd = 0 , padx = padx , pady = pady ,
cursor = " hand2 " , command = command , * * kw )
orig_bg = bg
def _on_press ( e ) :
btn . configure ( bg = active_bg )
def _on_release ( e ) :
btn . after ( 120 , lambda : btn . configure ( bg = orig_bg ) )
def _on_enter ( e ) :
btn . configure ( bg = active_bg )
def _on_leave ( e ) :
btn . configure ( bg = orig_bg )
btn . bind ( " <ButtonPress-1> " , _on_press )
btn . bind ( " <ButtonRelease-1> " , _on_release )
btn . bind ( " <Enter> " , _on_enter )
btn . bind ( " <Leave> " , _on_leave )
return btn
recorder = AudioRecorder ( )
is_recording = [ False ]
diktat_recorder = [ None ]
# Schliessen-Button (dezent, oben)
_close_bar = tk . Frame ( root , bg = " #C5E8F2 " )
_close_bar . pack ( fill = " x " , padx = 12 , pady = ( 6 , 0 ) )
_btn_close = tk . Button (
_close_bar , text = " \u2190 Schliessen " , font = ( " Segoe UI " , 9 ) ,
bg = " #EBEDF0 " , fg = " #6B7280 " , activebackground = " #D1D5DB " ,
relief = " flat " , bd = 0 , padx = 10 , pady = 3 , cursor = " hand2 " ,
command = on_main_close ,
)
_btn_close . pack ( side = " left " )
_btn_close . bind ( " <Enter> " , lambda e : _btn_close . configure ( bg = " #D1D5DB " ) )
_btn_close . bind ( " <Leave> " , lambda e : _btn_close . configure ( bg = " #EBEDF0 " ) )
# 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 , 4 ) , pady = ( 4 , 0 ) )
combo_out . set ( f " { lang_out_saved } – { LANGUAGES . get ( lang_out_saved , lang_out_saved ) } " )
def _refresh_combos ( ) :
_refresh_languages ( )
new_sorted = sorted ( LANGUAGES . items ( ) , key = lambda x : x [ 1 ] )
combo_in . configure ( values = [ f " { k } – { v } " for k , v in new_sorted ] )
new_ordered = get_output_languages_ordered ( )
combo_out . configure ( values = [ f " { k } – { v } " for k , v in new_ordered ] )
cur_in = ( lang_in_var . get ( ) or " " ) . split ( " \u2013 " ) [ 0 ] . strip ( )
cur_out = ( lang_out_var . get ( ) or " " ) . split ( " \u2013 " ) [ 0 ] . strip ( )
if cur_in in LANGUAGES :
combo_in . set ( f " { cur_in } \u2013 { LANGUAGES [ cur_in ] } " )
if cur_out in LANGUAGES :
combo_out . set ( f " { cur_out } \u2013 { LANGUAGES [ cur_out ] } " )
def _open_language_settings ( ) :
dlg = tk . Toplevel ( root )
dlg . title ( " Sprachen verwalten " )
dlg . configure ( bg = " #F0F4F8 " )
dlg . attributes ( " -topmost " , True )
dw , dh = 420 , 520
dsw , dsh = dlg . winfo_screenwidth ( ) , dlg . winfo_screenheight ( )
dlg . geometry ( f " { dw } x { dh } + { ( dsw - dw ) / / 2 } + { ( dsh - dh ) / / 2 } " )
dlg . minsize ( 380 , 400 )
tk . Label ( dlg , text = " Zusaetzliche Sprachen aktivieren " ,
font = ( " Segoe UI Semibold " , 11 , " bold " ) , bg = " #F0F4F8 " , fg = " #1a4d6d "
) . pack ( fill = " x " , padx = 10 , pady = ( 10 , 2 ) )
tk . Label ( dlg , text = " Angekreuzte Sprachen erscheinen in der Sprachauswahl. " ,
font = ( " Segoe UI " , 8 ) , bg = " #F0F4F8 " , fg = " #777 "
) . pack ( fill = " x " , padx = 10 , pady = ( 0 , 6 ) )
search_var = tk . StringVar ( )
tk . Entry ( dlg , textvariable = search_var , font = ( " Segoe UI " , 10 ) ,
relief = " solid " , bd = 1 ) . pack ( fill = " x " , padx = 10 , pady = ( 0 , 4 ) )
canvas_frame = tk . Frame ( dlg , bg = " #FFFFFF " , bd = 1 , relief = " solid " )
canvas_frame . pack ( fill = " both " , expand = True , padx = 10 , pady = ( 0 , 6 ) )
canvas = tk . Canvas ( canvas_frame , bg = " #FFFFFF " , highlightthickness = 0 )
scrollbar = ttk . Scrollbar ( canvas_frame , orient = " vertical " , command = canvas . yview )
inner = tk . Frame ( canvas , bg = " #FFFFFF " )
inner . bind ( " <Configure> " , lambda e : canvas . configure ( scrollregion = canvas . bbox ( " all " ) ) )
canvas . create_window ( ( 0 , 0 ) , window = inner , anchor = " nw " )
canvas . configure ( yscrollcommand = scrollbar . set )
scrollbar . pack ( side = " right " , fill = " y " )
canvas . pack ( side = " left " , fill = " both " , expand = True )
canvas . bind_all ( " <MouseWheel> " , lambda e : canvas . yview_scroll ( - 1 * ( e . delta / / 120 ) , " units " ) )
current_extra = set ( load_extra_languages ( ) )
base_codes = set ( _BASE_LANGUAGES . keys ( ) )
check_vars = { }
all_extra = sorted (
[ ( k , v ) for k , v in ALL_GOOGLE_LANGUAGES . items ( ) if k not in base_codes ] ,
key = lambda x : x [ 1 ]
)
cb_widgets = [ ]
def _build_list ( filter_text = " " ) :
for w in cb_widgets :
w . destroy ( )
cb_widgets . clear ( )
ft = filter_text . lower ( )
row = 0
for code , name in all_extra :
if ft and ft not in name . lower ( ) and ft not in code . lower ( ) :
continue
if code not in check_vars :
check_vars [ code ] = tk . BooleanVar ( value = code in current_extra )
cb = tk . Checkbutton ( inner , text = f " { name } ( { code } ) " , variable = check_vars [ code ] ,
font = ( " Segoe UI " , 9 ) , bg = " #FFFFFF " , anchor = " w " ,
activebackground = " #E8F0FE " )
cb . grid ( row = row , column = 0 , sticky = " w " , padx = 6 , pady = 1 )
cb_widgets . append ( cb )
row + = 1
_build_list ( )
search_var . trace_add ( " write " , lambda * _ : _build_list ( search_var . get ( ) ) )
def _save_and_close ( ) :
selected = [ code for code , var in check_vars . items ( ) if var . get ( ) ]
save_extra_languages ( selected )
_refresh_combos ( )
dlg . destroy ( )
btn_f = tk . Frame ( dlg , bg = " #F0F4F8 " )
btn_f . pack ( fill = " x " , padx = 10 , pady = ( 0 , 10 ) )
tk . Button ( btn_f , text = " Uebernehmen " , font = ( " Segoe UI " , 10 , " bold " ) ,
bg = " #5B8DB3 " , fg = " white " , relief = " flat " , padx = 16 , pady = 4 ,
cursor = " hand2 " , command = _save_and_close ) . pack ( side = " left " , padx = ( 0 , 8 ) )
tk . Button ( btn_f , text = " Abbrechen " , font = ( " Segoe UI " , 10 ) ,
bg = " #EBEDF0 " , fg = " #333 " , relief = " flat " , padx = 12 , pady = 4 ,
command = dlg . destroy ) . pack ( side = " left " )
btn_gear = tk . Button ( lang_container , text = " \u2699 " , font = ( " Segoe UI " , 14 ) ,
bg = " #C5E8F2 " , fg = " #5B8DB3 " , relief = " flat " , bd = 0 ,
cursor = " hand2 " , command = _open_language_settings )
btn_gear . grid ( row = 0 , column = 2 , rowspan = 2 , padx = ( 4 , 0 ) , sticky = " ns " )
def save_main_languages_now ( ) :
try :
in_val = ( lang_in_var . get ( ) or " " ) . strip ( )
out_val = ( lang_out_var . get ( ) or " " ) . strip ( )
2026-04-20 14:38:16 +02:00
lin = in_val . split ( " \u2013 " ) [ 0 ] . strip ( ) if " \u2013 " in in_val else in_val
lou = out_val . split ( " \u2013 " ) [ 0 ] . strip ( ) if " \u2013 " in out_val else out_val
2026-03-25 22:03:39 +01:00
if lin not in ALL_GOOGLE_LANGUAGES :
lin = " de "
if lou not in ALL_GOOGLE_LANGUAGES :
2026-04-20 14:38:16 +02:00
lou = load_main_languages ( ) [ 1 ]
2026-03-25 22:03:39 +01:00
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 ( ) :
2026-04-20 14:38:16 +02:00
in_val = ( lang_in_var . get ( ) or " " ) . strip ( )
out_val = ( lang_out_var . get ( ) or " " ) . strip ( )
lang_in = in_val . split ( " \u2013 " ) [ 0 ] . strip ( ) if " \u2013 " in in_val else in_val
lang_out = out_val . split ( " \u2013 " ) [ 0 ] . strip ( ) if " \u2013 " in out_val else out_val
if lang_in not in ALL_GOOGLE_LANGUAGES :
lang_in = " de "
if lang_out not in ALL_GOOGLE_LANGUAGES :
lang_out = load_main_languages ( ) [ 1 ]
2026-03-25 22:03:39 +01:00
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 = _make_feedback_btn ( umkehr_row , text = " \u21C4 Umkehren " , command = swap_languages ,
bg = " #7E57C2 " , active_bg = " #5E35A0 " , font_spec = ( " Segoe UI " , 10 , " bold " ) ,
padx = 12 , pady = 3 )
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 ) )
add_text_font_size_control ( left_header , txt_left , initial_size = 10 , bg_color = " #E1EDF5 " , save_key = " translate_left " )
copy_left_f = ttk . Frame ( left_f )
copy_left_f . pack ( fill = " x " , pady = ( 4 , 0 ) )
_make_feedback_btn ( copy_left_f , text = " Kopiere Original " ,
command = lambda : _clipboard_set_persistent ( txt_left . get ( " 1.0 " , " end " ) . strip ( ) ) or None ,
bg = " #6A8FA8 " , active_bg = " #4A6F88 " , font_spec = ( " Segoe UI " , 9 , " bold " ) ,
padx = 10 , pady = 3 ) . 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_font_size_control ( right_header , txt_right , initial_size = 10 , bg_color = " #EBF3FA " , save_key = " translate_right " )
copy_right_f = ttk . Frame ( right_f )
copy_right_f . pack ( fill = " x " , pady = ( 4 , 0 ) )
_make_feedback_btn ( copy_right_f , text = " Kopiere Uebersetzung " ,
command = lambda : _clipboard_set_persistent ( txt_right . get ( " 1.0 " , " end " ) . strip ( ) ) or None ,
bg = " #6A8FA8 " , active_bg = " #4A6F88 " , font_spec = ( " Segoe UI " , 9 , " bold " ) ,
padx = 10 , pady = 3 ) . 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 " )
def _load_autocopy_pref ( ) :
try :
from aza_persistence import is_autocopy_after_diktat_enabled
return is_autocopy_after_diktat_enabled ( )
except Exception :
return True
def _save_autocopy_pref ( val ) :
try :
from aza_persistence import save_autocopy_prefs
save_autocopy_prefs ( autocopy = bool ( val ) )
except Exception :
pass
autocopy_var = tk . BooleanVar ( value = _load_autocopy_pref ( ) )
cb_autocopy = ttk . Checkbutton ( btn_frame , text = " Autocopy nach Diktat/Übersetzung " , variable = autocopy_var )
cb_autocopy . pack ( anchor = " w " , pady = ( 0 , 4 ) )
autocopy_var . trace_add ( " write " , lambda * a : _save_autocopy_pref ( autocopy_var . get ( ) ) )
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 = " \u23FA Diktat starten " , bg = " #3D8B8B " )
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 )
if autocopy_var . get ( ) and trans :
_clipboard_set_persistent ( 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 = " \u23F9 Diktat stoppen " , bg = " #C62828 " )
status_var . set ( " Aufnahme läuft… " )
except Exception as e :
messagebox . showerror ( " Aufnahme-Fehler " , str ( e ) )
btn_diktat = _make_feedback_btn ( btn_row1 , text = " \u23FA Diktat starten " , command = toggle_diktat ,
bg = " #3D8B8B " , active_bg = " #2A6868 " )
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 ( )
btn_speak = _make_feedback_btn ( btn_row2 , text = " \U0001F50A Vorlesen " , command = do_speak ,
bg = " #4A6B8A " , active_bg = " #35506A " )
btn_speak . pack ( side = " left " , padx = ( 0 , 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 ( )
_make_feedback_btn ( btn_row1 , text = " Uebersetzen " , command = do_translate_manual ,
bg = " #2E7D32 " , active_bg = " #1B5E20 " ) . 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 = " \u23FA Diktat starten " , bg = " #3D8B8B " )
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 " )
_make_feedback_btn ( btn_row3 , text = " Neu " , command = do_neu ,
bg = " #8B5E3C " , active_bg = " #6B4226 " ) . 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 )
lern_header = ttk . Frame ( chat_frame )
lern_header . pack ( fill = " x " , anchor = " w " )
ttk . Label ( lern_header , text = " Lernkarten: " ) . pack ( side = " left " )
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 )
add_text_font_size_control ( lern_header , display , initial_size = 11 , bg_color = " #F0F0F0 " , save_key = " translate_lernkarten " )
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 ( )
tk . Button ( btn_row3 , text = " Lernkarten " , command = open_lernkarten_fenster ,
font = ( " Segoe UI " , 10 , " bold " ) , bg = " #b8e8d8 " , fg = " #1a4d6d " ,
activebackground = " #98d8c8 " , activeforeground = " #1a4d6d " ,
relief = " flat " , bd = 0 , padx = 14 , pady = 4 , cursor = " hand2 "
) . 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 )
gespraech_header = ttk . Frame ( chat_f )
gespraech_header . pack ( fill = " x " , anchor = " w " )
ttk . Label ( gespraech_header , text = " Gespräch: " ) . pack ( side = " left " )
txt_chat = ScrolledText ( chat_f , wrap = " word " , font = text_font , bg = " #F5FCFF " , height = 8 )
txt_chat . pack ( fill = " both " , expand = True , pady = ( 4 , 8 ) )
add_text_font_size_control ( gespraech_header , txt_chat , initial_size = 11 , bg_color = " #F0F0F0 " , save_key = " translate_gespraech " )
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. "
)
history_ext = build_history_prompt_extension ( )
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. "
)
if history_ext :
sys + = " " + history_ext
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 _update_language_history ( user_msg : str , ai_msg : str , thema : str = " " ) :
""" Aktualisiert das Lern-Gedaechtnis mit neuen Begriffen aus dem Austausch. """
try :
hist = load_language_history ( )
existing = hist . get ( " terms " , [ ] )
new_terms = extract_terms_from_exchange ( user_msg , ai_msg )
combined = existing + new_terms
seen = set ( )
unique = [ ]
for t in reversed ( combined ) :
key = t . lower ( ) . strip ( )
if key not in seen :
seen . add ( key )
unique . append ( t )
unique . reverse ( )
context = ( thema or " " ) [ : 200 ]
save_language_history ( unique , context )
except Exception :
pass
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 \n KI: { 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 \n KI: { 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 )
_update_language_history ( user_txt , ai_text , 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 )
_update_language_history ( text , ai_text , 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 === \n Falsch → Richtig – Deutsche Übersetzung (je Zeile) \n === VOKABELN === \n Deutsch – " + 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 } \n Datum: { datum } Uhrzeit: { uhrzeit } \n Sprache: { 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 ( ) } \n Rückseite: { b . strip ( ) } \n \n " )
else :
f . write ( f " Vorderseite: { v } \n Rü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 ( ) } \n Richtig: { 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 ) )
ttk . Sizegrip ( root ) . place ( relx = 1.0 , rely = 1.0 , anchor = " se " )
if not _embedded :
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 ( )