diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b7e39cc..6d4abb0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -71,7 +71,28 @@ "Bash(.venv-linux/bin/pyinstaller whisper-dictation.spec --clean)", "Bash(.venv-linux/bin/pyinstaller whisper-dictation.spec --clean -y)", "Bash(pactl --version)", - "Bash(pactl list:*)" + "Bash(pactl list:*)", + "Bash(grep -r \"input\\\\|permission\\\\|portal\\\\|access\" /mnt/ventoy/projects/chrka/whisper-dictation/whisper_app/*.py)", + "Read(//home/chk/.config/**)", + "Bash(ls ~/.config/xdg-desktop-portal*)", + "Read(//home/chk/.local/share/flatpak/**)", + "Read(//home/chk/.local/share/**)", + "Bash(find ~/.config -name *permission* -o -name *remote* -o -name *fakeinput* -o -name *access*control*)", + "Bash(qdbus org.freedesktop.impl.portal.desktop.kde /org/freedesktop/impl/portal/desktop/kde)", + "Bash(find ~/.local/share -name *permission*)", + "Bash(kwriteconfig6 --help)", + "Bash(plasma-browser-integration-host --version)", + "Read(//usr/share/xdg-desktop-portal/portals/**)", + "Bash(grep -E \"\\(Makefile|setup|build|spec|\\\\.sh$|\\\\.py$\\)\")", + "Bash(systemctl --user status ydotool)", + "Bash(ydotool key:*)", + "Bash(echo \"exit: $?\")", + "Bash(wl-copy)", + "Bash(wl-paste --no-newline)", + "Bash(ydotool type:*)", + "Bash(wl-copy -- \"XDOTOOL_TEST\")", + "Bash(xdotool key:*)", + "Bash(find /mnt/ventoy/projects/chrka/whisper-dictation/shared_data/ -name *.bin -o -name *.pt -o -name model*)" ] } } diff --git a/config.json b/config.json index 571aec1..a36cfc7 100644 --- a/config.json +++ b/config.json @@ -2,6 +2,6 @@ "hotkey": "ctrl+shift+space", "language": "de", "sample_rate": 16000, - "vocab_path": "/run/media/chk/Ventoy/projects/chrka/whisper-dictation/shared_data/vocabulary.json", - "model_dir": "/run/media/chk/Ventoy/projects/chrka/whisper-dictation/shared_data/" + "vocab_path": "shared_data/vocabulary.json", + "model_dir": "shared_data/" } \ No newline at end of file diff --git a/whisper_app/config.py b/whisper_app/config.py index 24cfe21..e509cd8 100644 --- a/whisper_app/config.py +++ b/whisper_app/config.py @@ -52,14 +52,19 @@ config: dict = {} vocab: dict = {"words": [], "replacements": []} +def _resolve_path(value: str, fallback_name: str = "") -> str: + """Resolve a config path: absolute stays absolute, relative is joined with DATA_DIR.""" + if value: + return value if os.path.isabs(value) else os.path.join(DATA_DIR, value) + if fallback_name: + return os.path.join(DATA_DIR, fallback_name) + return "" + + def _resolve_vocab_file() -> None: """Set VOCAB_FILE from config['vocab_path'], falling back to DATA_DIR.""" global VOCAB_FILE - vp = config.get("vocab_path", "") - if vp: - VOCAB_FILE = vp if os.path.isabs(vp) else os.path.join(DATA_DIR, vp) - else: - VOCAB_FILE = os.path.join(DATA_DIR, "vocabulary.json") + VOCAB_FILE = _resolve_path(config.get("vocab_path", ""), "vocabulary.json") def load_config() -> None: diff --git a/whisper_app/transcriber.py b/whisper_app/transcriber.py index 69ed968..d35465a 100644 --- a/whisper_app/transcriber.py +++ b/whisper_app/transcriber.py @@ -7,17 +7,20 @@ from whisper_app import app, config, grammar, media_duck, typer def load_model() -> None: - app.log(f"Loading {config.config['model']} on {config.config['device']}...") - model_dir = config.config.get("model_dir") or None - app.model = WhisperModel( - config.config["model"], - device=config.config["device"], - compute_type=config.config["compute_type"], - 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) + try: + app.log(f"Loading {config.config['model']} on {config.config['device']}...") + model_dir = config._resolve_path(config.config.get("model_dir", "")) or None + app.model = WhisperModel( + config.config["model"], + device=config.config["device"], + compute_type=config.config["compute_type"], + 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) + except Exception as e: + app.log(f"Model load FAILED: {e}") def stop_and_transcribe() -> None: diff --git a/whisper_app/typer.py b/whisper_app/typer.py index c8e1891..8884876 100644 --- a/whisper_app/typer.py +++ b/whisper_app/typer.py @@ -3,7 +3,7 @@ import shutil import subprocess import time -from whisper_app import config +from whisper_app import app, config def _pynput_type(text): @@ -36,6 +36,31 @@ def _wl_copy_bytes(data): pass +def _paste_via_ydotool(): + """Simulate Ctrl+V using ydotool (kernel-level, works on all compositors).""" + subprocess.run(["ydotool", "key", "29:1", "47:1", "47:0", "29:0"], check=False) + + +def _paste_via_wtype(): + """Simulate Ctrl+V using wtype (native Wayland, wlroots only).""" + subprocess.run(["wtype", "-M", "ctrl", "-P", "v", "-p", "v", "-m", "ctrl"], + check=False) + + +def _paste_via_xdotool(): + """Simulate Ctrl+V using xdotool (XWayland fallback).""" + subprocess.run(["xdotool", "key", "ctrl+v"], check=False) + + +def _pick_paste_cmd(): + """Select best available paste tool for the current Wayland session.""" + if shutil.which("ydotool"): + return _paste_via_ydotool + if shutil.which("wtype"): + return _paste_via_wtype + return _paste_via_xdotool + + def type_text(text): """Type text into the active window, cross-platform.""" if os.name == "nt": @@ -47,7 +72,9 @@ def type_text(text): old_clipboard = _wl_paste() subprocess.run(["wl-copy", "--", text], check=False) time.sleep(0.05) - subprocess.run(["xdotool", "key", "ctrl+v"], check=False) + paste_fn = _pick_paste_cmd() + app.log(f"typer: using {paste_fn.__name__}, session={session}") + paste_fn() time.sleep(delay) if old_clipboard is not None: _wl_copy_bytes(old_clipboard)