Compare commits

...

2 Commits

Author SHA1 Message Date
Christian Kauer e802ab2b3c fix linux 2026-03-22 13:12:23 +01:00
Christian Kauer 6af21dd21a fix linux version 2026-03-22 12:57:17 +01:00
6 changed files with 319 additions and 158 deletions

View File

@ -63,7 +63,11 @@
"Bash(echo \"Desktop: $XDG_CURRENT_DESKTOP\")", "Bash(echo \"Desktop: $XDG_CURRENT_DESKTOP\")",
"Bash(nvidia-smi)", "Bash(nvidia-smi)",
"Bash(lspci)", "Bash(lspci)",
"Bash(pacman -Q)" "Bash(pacman -Q)",
"Bash(/run/media/chk/Ventoy/projects/chrka/whisper-dictation/.venv-linux/bin/pip install:*)",
"Bash(.venv-linux/bin/python build.py)",
"Bash(.venv-linux/bin/pip list:*)",
"Bash(.venv-linux/bin/python -c \":*)"
] ]
} }
} }

View File

@ -41,6 +41,16 @@ sudo apt install python3-tk libayatana-appindicator3-1 wl-clipboard xdotool
| `wl-clipboard` | Text injection on Wayland (`wl-copy`) | | `wl-clipboard` | Text injection on Wayland (`wl-copy`) |
| `xdotool` | Simulates Ctrl+V paste on Wayland, text typing on X11 | | `xdotool` | Simulates Ctrl+V paste on Wayland, text typing on X11 |
**Low-latency hotkey (recommended):**
For fast hotkey response via evdev (instead of the slower XWayland fallback), add your user to the `input` group:
```bash
sudo usermod -aG input $USER
```
Log out and back in for the change to take effect.
**Optional (for GPU acceleration):** **Optional (for GPU acceleration):**
Arch/CachyOS: Arch/CachyOS:
@ -48,7 +58,9 @@ Arch/CachyOS:
sudo pacman -S nvidia cuda sudo pacman -S nvidia cuda
``` ```
Without CUDA, the app runs on CPU. Use `int8` compute type and a smaller model (`small` or `base`) for acceptable speed on CPU. Note: The system CUDA package may install a newer version (e.g. CUDA 13) than what faster-whisper/ctranslate2 requires (CUDA 12). The CUDA 12 runtime libraries (`nvidia-cublas-cu12`, `nvidia-cudnn-cu12`) are installed via pip in the virtual environment and bundled into the PyInstaller build, so the system CUDA version does not matter for the app itself. The system `nvidia` + `cuda` packages are only needed for the GPU driver and kernel module.
Without an NVIDIA GPU, the app runs on CPU. Use `int8` compute type and a smaller model (`small` or `base`) for acceptable speed on CPU.
**Python:** **Python:**
- Python 3.10+ - Python 3.10+

View File

@ -8,8 +8,8 @@ python3 -m venv --system-site-packages .venv-linux
echo "Installing dependencies..." echo "Installing dependencies..."
.venv-linux/bin/pip install --upgrade pip .venv-linux/bin/pip install --upgrade pip
.venv-linux/bin/pip install -r requirements.txt .venv-linux/bin/pip install -r requirements.txt
.venv-linux/bin/pip install -r requirements-cuda.txt
.venv-linux/bin/pip install pyinstaller .venv-linux/bin/pip install pyinstaller
# CUDA: uses system-installed CUDA libs (no pip CUDA packages needed)
echo "" echo ""
echo "Done. Run ./start.sh to launch, or ./build-linux.sh to create a standalone build." echo "Done. Run ./start.sh to launch, or ./build-linux.sh to create a standalone build."

View File

@ -1,63 +1,67 @@
{ {
"words": [ "words": [
"test" "test"
], ],
"replacements": [ "replacements": [
{ {
"from": "KRA", "from": "KRA",
"to": "KRAH" "to": "KRAH"
}, },
{ {
"from": "Atos", "from": "Atos",
"to": "ATHOS" "to": "ATHOS"
}, },
{ {
"from": "Resistec", "from": "Resistec",
"to": "RESISTEC" "to": "RESISTEC"
}, },
{ {
"from": "Resistek", "from": "Resistek",
"to": "RESISTEC" "to": "RESISTEC"
}, },
{ {
"from": "HES", "from": "HES",
"to": "HEES" "to": "HEES"
}, },
{ {
"from": "Ackerschot", "from": "Ackerschot",
"to": "Ackerschott" "to": "Ackerschott"
}, },
{ {
"from": "Carrois", "from": "Carrois",
"to": "Kauer" "to": "Kauer"
}, },
{ {
"from": "Jouer fixe", "from": "Jouer fixe",
"to": "Jour-Fixe" "to": "Jour-Fixe"
}, },
{ {
"from": "Docuware", "from": "Docuware",
"to": "DocuWare" "to": "DocuWare"
}, },
{ {
"from": "Nates", "from": "Nates",
"to": "Nejc" "to": "Nejc"
}, },
{ {
"from": "Bittzeit", "from": "Bittzeit",
"to": "BitSight" "to": "BitSight"
}, },
{ {
"from": "Kalmikow", "from": "Kalmikow",
"to": "Kalmykov" "to": "Kalmykov"
}, },
{ {
"from": "Leifert", "from": "Leifert",
"to": "Leifer" "to": "Leifer"
}, },
{ {
"from": "Kiyosa", "from": "Kiyosa",
"to": "Key-User" "to": "Key-User"
} },
] {
"from": "Kashi",
"to": "Cachy"
}
]
} }

View File

@ -16,13 +16,18 @@ _platform_tag = 'windows' if _is_windows else 'linux'
# ── Platform-specific binaries ──────────────────────────────────────────────── # ── Platform-specific binaries ────────────────────────────────────────────────
_binaries = [] _binaries = []
_sp = next(p for p in site.getsitepackages() if p.endswith('site-packages'))
_nvidia = os.path.join(_sp, 'nvidia')
if _is_windows: if _is_windows:
_sp = next(p for p in site.getsitepackages() if p.endswith('site-packages'))
_nvidia = os.path.join(_sp, 'nvidia')
_binaries = [ _binaries = [
(os.path.join(_nvidia, 'cublas', 'bin', '*.dll'), '.'), (os.path.join(_nvidia, 'cublas', 'bin', '*.dll'), '.'),
(os.path.join(_nvidia, 'cudnn', 'bin', '*.dll'), '.'), (os.path.join(_nvidia, 'cudnn', 'bin', '*.dll'), '.'),
] ]
else:
_binaries = [
(os.path.join(_nvidia, 'cublas', 'lib', 'libcublas.so*'), '.'),
(os.path.join(_nvidia, 'cudnn', 'lib', 'libcudnn.so*'), '.'),
]
# ── Platform-specific hidden imports ────────────────────────────────────────── # ── Platform-specific hidden imports ──────────────────────────────────────────
_hiddenimports = [ _hiddenimports = [

View File

@ -1,91 +1,227 @@
_pynput_loaded = False import os
Key = KeyCode = KeyboardListener = None import threading
def _ensure_pynput(): # ── evdev key name → hotkey string mapping ───────────────────────────────────
global _pynput_loaded, Key, KeyCode, KeyboardListener, _MODIFIER_MAP, _KEY_MAP _EVDEV_MODIFIER_MAP = {
if _pynput_loaded: "ctrl": {"KEY_LEFTCTRL", "KEY_RIGHTCTRL"},
return "ctrl_l": {"KEY_LEFTCTRL"}, "ctrl_r": {"KEY_RIGHTCTRL"},
from pynput.keyboard import Listener as _Listener, Key as _Key, KeyCode as _KeyCode "shift": {"KEY_LEFTSHIFT", "KEY_RIGHTSHIFT"},
Key = _Key "shift_l": {"KEY_LEFTSHIFT"}, "shift_r": {"KEY_RIGHTSHIFT"},
KeyCode = _KeyCode "alt": {"KEY_LEFTALT", "KEY_RIGHTALT"},
KeyboardListener = _Listener "alt_l": {"KEY_LEFTALT"}, "alt_r": {"KEY_RIGHTALT"},
_MODIFIER_MAP.update({ }
"ctrl": {Key.ctrl_l, Key.ctrl_r},
"ctrl_l": {Key.ctrl_l}, "ctrl_r": {Key.ctrl_r}, _EVDEV_KEY_MAP = {
"shift": {Key.shift_l, Key.shift_r}, "space": "KEY_SPACE", "tab": "KEY_TAB", "enter": "KEY_ENTER",
"shift_l": {Key.shift_l}, "shift_r": {Key.shift_r}, "esc": "KEY_ESC", "escape": "KEY_ESC",
"alt": {Key.alt_l, Key.alt_r}, "up": "KEY_UP", "down": "KEY_DOWN", "left": "KEY_LEFT", "right": "KEY_RIGHT",
"alt_l": {Key.alt_l}, "alt_r": {Key.alt_r}, "home": "KEY_HOME", "end": "KEY_END", "page_up": "KEY_PAGEUP", "page_down": "KEY_PAGEDOWN",
}) "insert": "KEY_INSERT", "delete": "KEY_DELETE", "backspace": "KEY_BACKSPACE",
_KEY_MAP.update({ }
"space": Key.space, "tab": Key.tab, "enter": Key.enter, for _i in range(1, 13):
"esc": Key.esc, "escape": Key.esc, _EVDEV_KEY_MAP[f"f{_i}"] = f"KEY_F{_i}"
"up": Key.up, "down": Key.down, "left": Key.left, "right": Key.right,
"home": Key.home, "end": Key.end, "page_up": Key.page_up, "page_down": Key.page_down,
"insert": Key.insert, "delete": Key.delete, "backspace": Key.backspace, def _parse_hotkey_evdev(hotkey_str):
}) """Parse hotkey string into evdev key code sets."""
for i in range(1, 13): parts = [p.strip().lower() for p in hotkey_str.split("+")]
_KEY_MAP[f"f{i}"] = getattr(Key, f"f{i}") modifiers = []
_pynput_loaded = True for p in parts[:-1]:
if p in _EVDEV_MODIFIER_MAP:
_MODIFIER_MAP = {} modifiers.append(_EVDEV_MODIFIER_MAP[p])
_KEY_MAP = {} elif p in _EVDEV_KEY_MAP:
modifiers.append({_EVDEV_KEY_MAP[p]})
else:
def _parse_hotkey(hotkey_str): modifiers.append({f"KEY_{p.upper()}"})
"""Parse hotkey string into (modifier_sets, trigger_key). trigger_part = parts[-1]
Returns: (list of sets-of-pynput-keys for each modifier, pynput key for trigger) if trigger_part in _EVDEV_KEY_MAP:
""" trigger = _EVDEV_KEY_MAP[trigger_part]
_ensure_pynput() elif trigger_part in _EVDEV_MODIFIER_MAP:
parts = [p.strip().lower() for p in hotkey_str.split("+")] trigger = next(iter(_EVDEV_MODIFIER_MAP[trigger_part]))
modifiers = [] else:
for p in parts[:-1]: trigger = f"KEY_{trigger_part.upper()}"
if p in _MODIFIER_MAP: return modifiers, trigger
modifiers.append(_MODIFIER_MAP[p])
elif p in _KEY_MAP:
modifiers.append({_KEY_MAP[p]}) def _can_use_evdev():
else: """Check if evdev is available and we have access to input devices."""
modifiers.append({KeyCode.from_char(p)}) if os.name == "nt":
trigger_part = parts[-1] return False
if trigger_part in _KEY_MAP: try:
trigger = _KEY_MAP[trigger_part] import evdev
elif trigger_part in _MODIFIER_MAP: devices = [evdev.InputDevice(p) for p in evdev.list_devices()]
trigger = next(iter(_MODIFIER_MAP[trigger_part])) keyboards = [d for d in devices if 1 in d.capabilities()] # EV_KEY
else: for d in devices:
trigger = KeyCode.from_char(trigger_part) d.close()
return modifiers, trigger return len(keyboards) > 0
except Exception:
return False
class HotkeyListener:
"""Hold-to-record hotkey using pynput. No root required on X11."""
class _EvdevListener:
def __init__(self, hotkey_str, on_press, on_release): """Low-latency hotkey listener using evdev (Linux kernel input)."""
self._modifiers, self._trigger = _parse_hotkey(hotkey_str)
self._on_press = on_press def __init__(self, hotkey_str, on_press, on_release):
self._on_release = on_release self._modifiers, self._trigger = _parse_hotkey_evdev(hotkey_str)
self._pressed = set() self._on_press = on_press
self._active = False self._on_release = on_release
self._listener = KeyboardListener(on_press=self._key_down, on_release=self._key_up) self._pressed = set()
self._listener.daemon = True self._active = False
self._listener.start() self._stop_event = threading.Event()
self._thread = threading.Thread(target=self._run, daemon=True)
def _matches_trigger(self, key): self._thread.start()
return key == self._trigger
def _run(self):
def _modifiers_held(self): import evdev
return all(any(k in self._pressed for k in mod_set) for mod_set in self._modifiers) import select
def _key_down(self, key): devices = [evdev.InputDevice(p) for p in evdev.list_devices()]
self._pressed.add(key) keyboards = [d for d in devices if 1 in d.capabilities()]
if not self._active and self._matches_trigger(key) and self._modifiers_held(): # close non-keyboard devices
self._active = True for d in devices:
self._on_press() if d not in keyboards:
d.close()
def _key_up(self, key):
self._pressed.discard(key) if not keyboards:
if self._active and self._matches_trigger(key): return
self._active = False
self._on_release() while not self._stop_event.is_set():
r, _, _ = select.select(keyboards, [], [], 0.5)
def stop(self): for dev in r:
self._listener.stop() try:
for event in dev.read():
if event.type != 1: # EV_KEY
continue
code_name = evdev.ecodes.KEY.get(event.code)
if isinstance(code_name, list):
code_name = code_name[0]
if code_name is None:
continue
if event.value == 1: # key down
self._key_down(code_name)
elif event.value == 0: # key up
self._key_up(code_name)
except OSError:
pass
for d in keyboards:
d.close()
def _key_down(self, code_name):
self._pressed.add(code_name)
if not self._active and code_name == self._trigger and self._modifiers_held():
self._active = True
self._on_press()
def _key_up(self, code_name):
self._pressed.discard(code_name)
if self._active and code_name == self._trigger:
self._active = False
self._on_release()
def _modifiers_held(self):
return all(
any(k in self._pressed for k in mod_set)
for mod_set in self._modifiers
)
def stop(self):
self._stop_event.set()
# ── pynput fallback (Windows, or Linux without evdev/input group) ────────────
_pynput_loaded = False
Key = KeyCode = KeyboardListener = None
_PYNPUT_MODIFIER_MAP = {}
_PYNPUT_KEY_MAP = {}
def _ensure_pynput():
global _pynput_loaded, Key, KeyCode, KeyboardListener
if _pynput_loaded:
return
from pynput.keyboard import Listener as _Listener, Key as _Key, KeyCode as _KeyCode
Key = _Key
KeyCode = _KeyCode
KeyboardListener = _Listener
_PYNPUT_MODIFIER_MAP.update({
"ctrl": {Key.ctrl_l, Key.ctrl_r},
"ctrl_l": {Key.ctrl_l}, "ctrl_r": {Key.ctrl_r},
"shift": {Key.shift_l, Key.shift_r},
"shift_l": {Key.shift_l}, "shift_r": {Key.shift_r},
"alt": {Key.alt_l, Key.alt_r},
"alt_l": {Key.alt_l}, "alt_r": {Key.alt_r},
})
_PYNPUT_KEY_MAP.update({
"space": Key.space, "tab": Key.tab, "enter": Key.enter,
"esc": Key.esc, "escape": Key.esc,
"up": Key.up, "down": Key.down, "left": Key.left, "right": Key.right,
"home": Key.home, "end": Key.end, "page_up": Key.page_up, "page_down": Key.page_down,
"insert": Key.insert, "delete": Key.delete, "backspace": Key.backspace,
})
for i in range(1, 13):
_PYNPUT_KEY_MAP[f"f{i}"] = getattr(Key, f"f{i}")
_pynput_loaded = True
def _parse_hotkey_pynput(hotkey_str):
_ensure_pynput()
parts = [p.strip().lower() for p in hotkey_str.split("+")]
modifiers = []
for p in parts[:-1]:
if p in _PYNPUT_MODIFIER_MAP:
modifiers.append(_PYNPUT_MODIFIER_MAP[p])
elif p in _PYNPUT_KEY_MAP:
modifiers.append({_PYNPUT_KEY_MAP[p]})
else:
modifiers.append({KeyCode.from_char(p)})
trigger_part = parts[-1]
if trigger_part in _PYNPUT_KEY_MAP:
trigger = _PYNPUT_KEY_MAP[trigger_part]
elif trigger_part in _PYNPUT_MODIFIER_MAP:
trigger = next(iter(_PYNPUT_MODIFIER_MAP[trigger_part]))
else:
trigger = KeyCode.from_char(trigger_part)
return modifiers, trigger
class _PynputListener:
"""Hotkey listener using pynput (Windows / fallback)."""
def __init__(self, hotkey_str, on_press, on_release):
self._modifiers, self._trigger = _parse_hotkey_pynput(hotkey_str)
self._on_press = on_press
self._on_release = on_release
self._pressed = set()
self._active = False
self._listener = KeyboardListener(on_press=self._key_down, on_release=self._key_up)
self._listener.daemon = True
self._listener.start()
def _key_down(self, key):
self._pressed.add(key)
if not self._active and key == self._trigger and self._modifiers_held():
self._active = True
self._on_press()
def _key_up(self, key):
self._pressed.discard(key)
if self._active and key == self._trigger:
self._active = False
self._on_release()
def _modifiers_held(self):
return all(any(k in self._pressed for k in mod_set) for mod_set in self._modifiers)
def stop(self):
self._listener.stop()
# ── Public API ───────────────────────────────────────────────────────────────
def HotkeyListener(hotkey_str, on_press, on_release):
"""Create the best available hotkey listener for the current platform."""
if _can_use_evdev():
return _EvdevListener(hotkey_str, on_press, on_release)
return _PynputListener(hotkey_str, on_press, on_release)