Compare commits

...

2 Commits

Author SHA1 Message Date
Christian Kauer 4a9a1ec799 fix dict path 2026-03-25 16:24:18 +01:00
Christian Kauer 79f5bfabe5 upd typer permissions, and relative path 2026-03-25 09:04:53 +01:00
7 changed files with 95 additions and 27 deletions

View File

@ -71,7 +71,31 @@
"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*)",
"Bash(python3 -c \":*)",
"Read(//usr/bin/**)",
"Bash(/run/media/chk/Ventoy/projects/chrka/whisper-dictation/.venv-linux/bin/python build.py)"
]
}
}

View File

@ -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/"
}

View File

@ -66,6 +66,10 @@
{
"from": "SHP",
"to": "SAP"
},
{
"from": "GRA App",
"to": "KRAH-App"
}
]
}

View File

@ -6,7 +6,12 @@ import sys
def _app_dir() -> str:
"""Root dir for config.json and vocabulary.json."""
if getattr(sys, "frozen", False):
return os.path.dirname(sys.executable)
exe_dir = os.path.dirname(sys.executable)
# Built exe lives at dist/<platform>/; project root is two levels up.
project_root = os.path.normpath(os.path.join(exe_dir, "..", ".."))
if os.path.isfile(os.path.join(project_root, "config.json")):
return project_root
return exe_dir
# config.py lives at whisper_app/config.py → parent of parent = repo root
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -52,14 +57,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:

View File

@ -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:

View File

@ -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)

View File

@ -31,8 +31,8 @@ def _open_main(root: tk.Tk) -> None:
win.title("Vokabular")
win.configure(bg=BG)
win.attributes("-topmost", True)
win.resizable(False, False)
win.minsize(600, 0)
win.resizable(True, True)
win.minsize(600, 480)
win.option_add("*Menu.background", BG3)
win.option_add("*Menu.foreground", FG)
win.option_add("*Menu.activeBackground", AMBER)
@ -192,6 +192,6 @@ def _open_main(root: tk.Tk) -> None:
win.update_idletasks()
sw = win.winfo_screenwidth()
sh = win.winfo_screenheight()
w = win.winfo_reqwidth()
h = win.winfo_reqheight()
win.geometry(f"+{(sw-w)//2}+{(sh-h)//2}")
w = max(win.winfo_reqwidth(), 600)
h = min(max(win.winfo_reqheight(), 480), sh - 100)
win.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")