diff --git a/shared_data/vocabulary.json b/shared_data/vocabulary.json index 7f20204..912b68e 100644 --- a/shared_data/vocabulary.json +++ b/shared_data/vocabulary.json @@ -62,6 +62,10 @@ { "from": "Kashi", "to": "Cachy" + }, + { + "from": "SHP", + "to": "SAP" } ] } \ No newline at end of file diff --git a/whisper-dictation.spec b/whisper-dictation.spec index 9342827..8b966cc 100644 --- a/whisper-dictation.spec +++ b/whisper-dictation.spec @@ -34,6 +34,7 @@ _hiddenimports = [ 'ctranslate2', 'faster_whisper', 'sounddevice', + 'language_tool_python', ] if _is_windows: _hiddenimports.append('pynput.keyboard._win32') diff --git a/whisper_app/config.py b/whisper_app/config.py index 4173da9..9b4aa30 100644 --- a/whisper_app/config.py +++ b/whisper_app/config.py @@ -35,6 +35,7 @@ DEFAULT_CONFIG = { "sample_rate": 16000, "vocab_path": "", "model_dir": "", + "grammar_check": True, } MODELS = ["tiny", "base", "small", "medium", "large-v2", "large-v3"] @@ -115,6 +116,23 @@ def apply_vocab(text: str) -> str: return text +_STYLE_HINTS = { + "de": "Hallo, wie geht es Ihnen? Ich arbeite an einem wichtigen Projekt. " + "Die Ergebnisse der Analyse zeigen deutliche Verbesserungen.", + "en": "Hello, how are you? I am working on an important project. " + "The analysis results show clear improvements.", + "fr": "Bonjour, comment allez-vous ? Je travaille sur un projet important. " + "Les résultats de l'analyse montrent des améliorations nettes.", +} + + def get_initial_prompt() -> str: + parts = [] + lang = config.get("language") + hint = _STYLE_HINTS.get(lang) + if hint: + parts.append(hint) words = vocab.get("words", []) - return ", ".join(words) if words else "" + if words: + parts.append(", ".join(words)) + return " ".join(parts) if parts else "" diff --git a/whisper_app/grammar.py b/whisper_app/grammar.py new file mode 100644 index 0000000..f846d68 --- /dev/null +++ b/whisper_app/grammar.py @@ -0,0 +1,41 @@ +"""Optional grammar correction using LanguageTool.""" + +_tool = None +_lang = None + +_LANG_MAP = { + "de": "de-DE", + "en": "en-US", + "fr": "fr-FR", + "es": "es", + "it": "it", +} + + +def init(lang, log=print): + """Pre-initialize LanguageTool. Call once at startup.""" + global _tool, _lang + if lang == _lang and _tool is not None: + return + _lang = lang + try: + import language_tool_python + lt_lang = _LANG_MAP.get(lang, lang or "de-DE") + _tool = language_tool_python.LanguageTool(lt_lang) + log("Grammar checker ready.") + except ImportError: + _tool = None + log("language_tool_python not installed — grammar check disabled.") + except Exception as e: + _tool = None + log(f"Grammar checker init failed: {e}") + + +def correct(text): + """Correct grammar, capitalization, and punctuation.""" + if _tool is None: + return text + try: + return _tool.correct(text) + except Exception: + return text diff --git a/whisper_app/settings_window.py b/whisper_app/settings_window.py index c58db58..e301ca9 100644 --- a/whisper_app/settings_window.py +++ b/whisper_app/settings_window.py @@ -158,6 +158,15 @@ def _open_main(root: tk.Tk, on_reload) -> None: f = row("Sprache") dd(f, lang_var, list(cfg.LANGUAGES.keys()), 14).pack(side="left") + # ── TEXTVERARBEITUNG ── + section("TEXTVERARBEITUNG") + grammar_var = tk.BooleanVar(value=cfg.config.get("grammar_check", True)) + f_gc = row("Grammatikkorrektur", hint="pip install language_tool_python") + tk.Checkbutton(f_gc, variable=grammar_var, text="Aktiviert", + bg=BG, fg=FG, selectcolor=BG3, activebackground=BG, + activeforeground=FG, font=FONT_UI, + highlightthickness=0, bd=0).pack(side="left") + # ── LEISTUNG ── section("LEISTUNG") device_var = tk.StringVar(value=cfg.config["device"]) @@ -232,6 +241,7 @@ def _open_main(root: tk.Tk, on_reload) -> None: cfg.config["hotkey"] = hotkey_var.get() cfg.config["vocab_path"] = vocab_path_var.get() cfg.config["model_dir"] = model_dir_var.get() + cfg.config["grammar_check"] = grammar_var.get() cfg.save_config() win.destroy() threading.Thread(target=on_reload, daemon=True).start() diff --git a/whisper_app/transcriber.py b/whisper_app/transcriber.py index a2dda20..26f101e 100644 --- a/whisper_app/transcriber.py +++ b/whisper_app/transcriber.py @@ -3,7 +3,7 @@ import time import numpy as np from faster_whisper import WhisperModel -from whisper_app import app, config, typer +from whisper_app import app, config, grammar, typer def load_model() -> None: @@ -16,6 +16,8 @@ def load_model() -> None: download_root=model_dir, ) app.log("Model ready.") + if config.config.get("grammar_check"): + grammar.init(config.config.get("language") or "de", log=app.log) def stop_and_transcribe() -> None: @@ -62,6 +64,8 @@ def _do_transcribe() -> None: ) text = " ".join(s.text for s in segments).strip() text = config.apply_vocab(text) + if config.config.get("grammar_check"): + text = grammar.correct(text) app.log(f"Result: {repr(text)}") if text: diff --git a/whisper_app/typer.py b/whisper_app/typer.py index ad1ff58..aa211fb 100644 --- a/whisper_app/typer.py +++ b/whisper_app/typer.py @@ -9,6 +9,31 @@ def _pynput_type(text): KeyboardController().type(text) +def _wl_paste(): + """Read current clipboard contents, returns None on failure.""" + try: + result = subprocess.run( + ["wl-paste", "--no-newline"], + capture_output=True, timeout=2, + ) + if result.returncode == 0: + return result.stdout + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + return None + + +def _wl_copy_bytes(data): + """Restore clipboard from raw bytes.""" + try: + subprocess.run( + ["wl-copy"], + input=data, check=False, timeout=2, + ) + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + def type_text(text): """Type text into the active window, cross-platform.""" if os.name == "nt": @@ -16,9 +41,13 @@ def type_text(text): return session = os.environ.get("XDG_SESSION_TYPE", "") if session == "wayland" and shutil.which("wl-copy"): + old_clipboard = _wl_paste() subprocess.run(["wl-copy", "--", text], check=False) time.sleep(0.05) subprocess.run(["xdotool", "key", "ctrl+v"], check=False) + time.sleep(0.05) + if old_clipboard is not None: + _wl_copy_bytes(old_clipboard) elif shutil.which("xdotool"): subprocess.run(["xdotool", "type", "--clearmodifiers", "--", text], check=False) else: