a38cff59ea7efffaf79d3fcaaec3a15c6f0eee55
227
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8322306aac |
feat(dictation): append trailing space after completed transcription (#608)
* feat(dictation): append trailing space after completed transcription
Fix cross-session glueing ("Hello.This") for push-to-talk and toggle
by appending a trailing space to each injected segment instead of relying
on in-session leading-space memory that is cleared on IDLE.
Adds an optional Settings toggle (default on) and preserves legacy
leading-space behavior when disabled. Newlines from voice commands are
left without a trailing space.
Fixes #605
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(dictation): honor trailing-space setting from disk live
Read append_trailing_space from config.json on each injection so the
Settings toggle applies without restart. main() and TrayIndicator each
own a ConfigManager, so an in-memory read missed Settings writes — same
pattern as TextInjector.copy_to_clipboard.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* test(dictation): align capitalize tests with trailing-space default
Update auto-capitalize expectations to include the default trailing
space, and add edge-case coverage for legacy mode, newlines, empty
input, and config-read failures so patch coverage clears 80%.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
|
||
|
|
7422bf927e |
feat: auto-capitalize sentences after punctuation (#554)
* feat: auto-capitalize sentences after punctuation Add capitalize_sentences() function that uppercases the first letter of dictated text and after sentence-ending punctuation (., !, ?) followed by whitespace. Preserves URLs, decimals, and abbreviations without trailing space. - Config option text_injection.auto_capitalize (default: true) - Settings toggle in General tab to opt out - 17 tests covering edge cases Fixes #553 * style: black-format auto-capitalize helpers for CI lint Fixes the Lint Python check failure on the auto-capitalize PR. * refactor: make auto-capitalize Vosk-only with clear UI indication - Gate auto-capitalize feature to Vosk engine only (Whisper outputs proper casing) - Update settings tooltip to explain Vosk-only behavior - Update subtitle to show '(Vosk only)' for user clarity * test(main): cover Vosk-only auto-capitalize injection path Add callback wiring tests for enabled/disabled auto-capitalize on Vosk and the Whisper skip path so codecov patch coverage clears the 80% bar. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * refactor(settings): move Auto-Capitalize to Recognition tab Auto-capitalize is dictation output post-processing, not application lifecycle behavior, so it belongs alongside Voice Commands rather than with autostart/clipboard settings in General. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
8c76578200 |
fix(ui): flatten settings info notices (#615)
Drop the thick left accent strip on .info-box helpers so they read as muted flat surfaces with a thin full border, matching the preference cards instead of looking like callout banners. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
29db3a05a4 |
fix(ibus): keep engine teardown correct when parent destroy fails (#613)
* fix(ibus): keep engine teardown correct when parent destroy fails
`do_destroy` passed `super().do_destroy` to `_handle_engine_destroy`.
PyGObject binds that vfunc to the GType rather than to the instance, so
calling it with no arguments raises:
TypeError: IBus.Object.destroy() takes exactly 1 argument (0 given)
This fired on every engine teardown. The exception escaped
`_handle_engine_destroy` before its return, so neither
`_active_instance = ...` nor `_focus_event.clear()` in the caller ran,
leaving a destroyed engine registered as active with focus still set --
the two values `_ibus_ping_status` (#523) reads to decide commit
readiness.
Two changes:
- Bind the parent destroy to the instance, falling back to
`IBus.Object.destroy(self)` only when the no-arg call raises TypeError,
so this keeps working if PyGObject binds the vfunc correctly later.
- Guard the parent destroy call so no failure there can skip teardown.
Fixes #606
* refactor(ibus): extract parent-destroy workaround into a testable helper
Codecov flagged the new code as uncovered (36% patch, below the 80%
target). The whole `if IBUS_AVAILABLE:` branch of `do_destroy` is
unreachable from the suite, which only ever exercises that method with
IBUS_AVAILABLE False -- the single line it replaced was already
uncovered on main for the same reason.
Move the workaround into a module-level `_invoke_parent_destroy`, the
same pattern `_handle_engine_destroy` already uses to make `do_destroy`
testable, and bind it with functools.partial at the call site.
Behaviour is unchanged. Coverage of the branch goes from 0 to all but
the one-line partial, and three tests now pin the actual contract:
pass-through when the binding is correct, retry with the instance on the
GType-binding TypeError, and no masking of other exceptions.
|
||
|
|
161129cf3c |
feat(text-injection): use IBus on unbridged compositors when ibus-wayland runs (#614)
* feat(text-injection): use IBus on unbridged compositors when ibus-wayland runs The `_IBUS_UNBRIDGED_COMPOSITORS` denylist assumes nothing sits between the compositor and ibus-daemon. Since IBus 1.5.32 that is no longer a safe assumption: IBus ships `ibus-wayland`, which implements `zwp_input_method_v2` and relays commits to native Wayland clients speaking text-input-v3. Every compositor on the denylist exposes `zwp_input_method_manager_v2`, so with the bridge running they are bridged, and skipping IBus falls back to a slower path unnecessarily. Add a live `pgrep -x ibus-wayland` probe and let it override the denylist. Scoped to the denylist branch only, so GNOME and the KDE VirtualKeyboard path (#574) are untouched. Behaviour is unchanged when the bridge is not running, and because the check is a live probe rather than configuration it degrades back to the denylist by itself if the bridge dies or was never started. Cost is one pgrep per TextInjector construction, not per injection. Verified on Hyprland 0.56.1 with IBus 1.5.34: dictation into Electron apps (VSCodium, Electron 39/41) and kitty commits through IBus, 303 chars in ~200ms versus 16.8s for 415 chars via `ydotool type`. Also adds VOCALINUX_FORCE_BACKEND (ibus|wtype|ydotool|auto) as discussed in #607. Autodetection has to infer whether IBus commits actually reach the focused app and cannot verify it -- commit_text() reports success even when the text is dropped -- so an escape hatch is useful when the inference is wrong, and it makes the two paths A/B-testable without editing code. Unknown values fall back to auto with a warning rather than silently pinning a backend. Related to #476 and #402; happy to drop this from the PR if it should follow their design instead. Two existing tests asserted the unbridged outcome without pinning the probe -- one blanket-mocks subprocess.run to returncode=0, the other shells out for real -- so both now state the "no bridge" precondition explicitly rather than depending on the host. Note for users: Chromium only opens a zwp_text_input_v3 seat when launched with --enable-wayland-ime; without it the bridge runs and the app still receives nothing. Closes #607 * test(text-injection): cover bridge override and forced-backend branches Codecov flagged the new branches in _check_dependencies as uncovered (74% patch, below the 80% target). Adds four tests to TestCheckDependencies: - unbridged compositor + ibus-wayland running selects IBus - VOCALINUX_FORCE_BACKEND=wtype pins wtype where IBus would be chosen - VOCALINUX_FORCE_BACKEND=ydotool pins ydotool even with wtype present - VOCALINUX_FORCE_BACKEND=ibus bypasses the reachability guards, with neither a daemon nor a bridge available Every line added by this PR is now covered. |
||
|
|
4fad897f14 |
fix(appimage): smoke-test fixes for tray, labels, and packaging (#602)
* fix(ui): AppImage smoke fixes for tray icons, labels, and identity Replace placeholder mic tray SVGs with opaque branded icons, show whisper.cpp instead of Whisper_cpp, set GLib/AppIndicator identity to Vocalinux (not main.py), and give About dialog buttons enough padding so labels are not clipped under Adwaita. * fix(packaging): harden AppImage build for Wayland and tray deps Pin the build interpreter, install onnxruntime into the bundle, set desktop Exec=AppRun + StartupWMClass, prune typelibs to an allowlist after linuxdeploy, and stop forcing GDK_BACKEND=x11 on Wayland (VOCALINUX_FORCE_X11=1 remains as an escape hatch). CI AppImage jobs also pull Ayatana AppIndicator and librsvg SVG loader packages. * fix(packaging): bundle full stdlib from base_prefix in AppImage When build.sh runs under a venv, sys.prefix only has site-packages, so the AppImage was missing encodings and crashed on start. Copy the interpreter and stdlib from sys.base_prefix, force pip --ignore-installed into AppDir, set Gdk program class to Vocalinux, and tone down the muted tray slash so small StatusNotifier icons stay slate. * fix(tray): use absolute icon paths so stale hicolor icons lose XFCE StatusNotifier resolves IconName through the system theme and can ignore IconThemePath. A leftover ~/.local/share/icons placeholder (vocalinux-microphone-off.svg filled #e74c3c) was winning over the AppImage's bundled icons. Outside Flatpak, pass absolute SVG paths to set_icon_full / new_with_path instead of bare theme names. |
||
|
|
e7022f83dd |
feat(settings): searchable sidebar settings instead of seven tabs (#601)
* feat(settings): replace tab navigation with searchable sidebar and topic pages - Sidebar (icon + label) with Gtk.Stack replaces the 7-tab notebook - Live settings search (Ctrl+F): filters rows in place, per-page match counts, dims pages without hits, empty-results state - Topic-based pages per GNOME HIG: Dictation, Speech Model, Audio, Performance, Application, Advanced - Persistent bottom status strip: recognition state, shared mic level bar, and Test Dictation with inline transcription output - GPU device selection moves out of the power-user gate onto the Performance page; Reset to Defaults moves into the Advanced page - Consolidated model info card (info + warning + legend in one surface) - Contextual custom-shortcut row, in-list auto-pause empty state, uniform control widths, aligned group descriptions - Esc clears search, Ctrl+W closes * test(settings): update source-inspection tests for sidebar redesign, add search coverage * fix(settings): index unlocked advanced controls in search Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * fix(settings): restore selected page after clearing search Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> --------- Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
c59fe8b577 |
fix: skip unbridged IBus on KDE Plasma Wayland (#577)
* fix(injection): skip unbridged IBus on KDE Plasma Wayland Do not auto-start XIM ibus-daemon on Wayland (circular viability check). On KDE, require KWin VirtualKeyboard enabled before selecting IBus. Fixes #574 * refactor(injection): tighten KDE VirtualKeyboard bridge check Ponytail cleanup: shorter logs and fewer failure branches, same behavior. * style: black-format ibus daemon Wayland test for CI * fix(injection): parse KWin VirtualKeyboard gdbus variants strictly Match <<true>>/<<false>> wrappers instead of bare substrings so ambiguous gdbus output cannot flip the #574 IBus bridge decision. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
4c070ded46 |
feat(offload): auto-pause apps + idle keep-alive model unload (#592)
* feat(auto-pause): unload speech model while selected apps run Add Settings toggle + process list so Vocalinux frees CPU/GPU/RAM while configured games/apps are running, then reloads the model when they exit. Closes the process-list half of #445 (window-focus pause is separate). * test(auto-pause): isolate tray wiring to avoid stale GLib mocks Move TrayIndicator auto-pause tests into test_suspend_handler so test_auto_pause_monitor never imports tray_indicator under a collection-time gi MagicMock. That import pinned GLib.SOURCE_REMOVE and broke resume tests later in the suite (CI on #517). * test(auto-pause): raise patch coverage for codecov target Cover process snapshot I/O, GLib poll scheduling, config coercion, unload edge cases, and tray fallback paths so codecov/patch clears 80%. * feat(keepalive): idle model offload for GPU/battery saving Add opt-in keep-alive that unloads the speech model after inactivity so hybrid-GPU laptops can release VRAM/Vulkan. Next dictation lazy-reloads. Shares unload_model with auto-pause (#445) using distinct reasons so app-pause still blocks dictation while idle unload does not. Fixes #591 (with #445 via shared offload infrastructure). * test(keepalive): mock gi.repository.GLib via sys.modules Match the auto-pause GLib test pattern so CI on Python 3.9/3.10 sees the late `from gi.repository import GLib` import. * test(keepalive): drop post-fire source_remove assertion After the GLib timeout callback runs, _timeout_id is already cleared, so shutdown does not call source_remove. * fix(settings): hide auto-pause empty-state label when apps are listed The dialog-wide show_all() after _load_and_apply_settings() re-showed the 'No apps listed yet' row that _refresh_auto_pause_list had hidden. Mark the row no_show_all and manage visibility explicitly. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * feat(settings): move auto-pause and idle unload to dedicated Power tab - New Power tab between Shortcuts and General so resource management features are easy to find; General returns to app-behavior toggles only - Rename 'Model Keep-Alive (Battery)' to 'Unload When Idle' — the old name read as the opposite of what the feature does - Clearer copy throughout: row titles, descriptions, tooltips, 'From running…' becomes 'Choose Running App…', empty-state hint - Gray out app-list and idle-timeout controls while their feature toggle is off Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * fix(settings): show which notebook tab is selected The tab CSS targeted :selected, but GTK3 marks the active notebook tab with :checked, so no selected styling ever applied — all tabs rendered identically. Use :checked and add an accent underline + bold label. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
b945137d60 |
fix(vulkan): auto-select discrete GPU and add manual device selection (#590)
* fix(vulkan): auto-select discrete GPU and add manual device selection On hybrid GPU systems (Intel iGPU + NVIDIA dGPU), whisper.cpp's Vulkan backend always selected device 0 (the integrated GPU), resulting in significantly slower performance. Changes: - Add detect_vulkan_devices() to enumerate all Vulkan GPUs and identify discrete vs integrated devices - Set GGML_VULKAN_DEVICE env var before model init to select the correct GPU (auto-selects discrete GPU when available) - Add whispercpp_gpu_device config option for manual device selection - Add GPU device dropdown in Settings > Advanced for users with multiple Vulkan GPUs Fixes #589 * test: add coverage for Vulkan GPU device selection - Test detect_vulkan_devices() with single/multiple GPUs - Test _prefer_discrete_vulkan_device() preference logic - Test GPU device selection in recognition_manager - Verify GGML_VULKAN_DEVICE env var is set correctly * fix(speech_recognition): pass whispercpp GPU device to pywhispercpp Model context_params The Vulkan device-selection logic from PR #590 set the GGML_VULKAN_DEVICE environment variable, but pywhispercpp did not pick it up. Pass the selected GPU device through the pywhispercpp Model context_params so whisper.cpp actually initializes the requested Vulkan device. Fixes #589 * fix(speech_recognition): fallback when pywhispercpp lacks context_params Older pywhispercpp releases (< ~1.4.0) reject the context_params keyword and raise TypeError. Detect that specific failure and retry without GPU device selection so the engine still loads on those versions. Fixes #589 * refactor(speech_recognition): inline context_params assignment Simplify the GPU device selection path and remove the temporary dict. * fix(vulkan): parse real vulkaninfo GPU0: headers detect_vulkan_devices() only recognized fictional "GPU id = N" headers, so real vulkaninfo --summary output (GPU0:/GPU1:) returned no devices and hybrid NVIDIA systems fell through to CUDA. Also resolve preferred device names by GPU id instead of list position. Fixes #589 * refactor(whispercpp): drop ineffective GGML_VULKAN_DEVICE env var pywhispercpp ignores GGML_VULKAN_DEVICE; device selection already goes through context_params={"gpu_device": N}. Remove the misleading env write so maintainers do not think the env var is the selection path. |
||
|
|
2a0e11e077 | fix: correct ydotool service setup guidance (#560) | ||
|
|
eb16a34328 |
feat(cli): add --version option 🤖🤖🤖 (#563)
* feat(cli): add version flag * fix(cli): parse args before acquiring single-instance lock so --version works while app is running Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
8c4f743e82 |
fix(ui): first-run dialog response without Gtk.Dialog.do_response (#580)
* fix(ui): use Gtk response signal for first-run dialog Overriding do_response and calling Gtk.Dialog.do_response() breaks on modern PyGObject with "Class GtkDialog doesn't implement response". Connect to the response signal instead so dialog.run() still works. Fixes #566 * test(ui): cover first-run dialog response handler Exercise connect("response") wiring and _on_response mapping so codecov/patch is green for the #566 Gtk.Dialog.do_response fix. * test(ui): isolate first-run dialog response coverage Load first_run_dialog under a unique module name with a real Gtk.Dialog stand-in so _on_response and connect("response") run under full-suite import pollution without clobbering other tests' gi mocks. * test(ui): cover _on_response via SimpleNamespace Call FirstRunDialog._on_response on a stand-in object instead of constructing Gtk.Dialog; enough for codecov/patch on the handler body. * test(ui): map first-run responses via SimpleNamespace Exercise FirstRunDialog._on_response for YES/NO/LATER/unknown without constructing Gtk.Dialog. * test(ui): collapse first-run response cases into one table One loop covers YES/NO/LATER/unknown instead of four copy-pasted tests. * test(ui): restore isolated first-run dialog coverage load SimpleNamespace calls against vocalinux.ui.first_run_dialog.FirstRunDialog fail under the full suite when earlier tests leave the class as a MagicMock. Reload the source under a unique module name with a real Gtk.Dialog stand-in (as in 680c5ed) so _on_response and connect("response") stay green on CI. |
||
|
|
513e4749e3 | fix(xdotool): preserve input focus after injection (#564) | ||
|
|
fbcd28d577 |
fix(vosk): add missing Italian and English-India entries to medium/large model tables (#551)
Fixes #550. Root cause: VOSK_MODEL_INFO["medium"]["languages"] and VOSK_MODEL_INFO["large"]["languages"] in vosk_model_info.py were missing entries for "it" (Italian) and "en-in" (English-India), even though both languages are present in the "small" table and VOSK actually publishes bigger models for them (vosk-model-it-0.22, vosk-model-en-in-0.5 at alphacephei.com/vosk/models). When a user selected Italian + the medium (or large) VOSK model, _init_vosk() built vosk_model_map["medium"] = None (via .get() on a missing key), and _get_vosk_model_path() then called os.path.join(MODELS_DIR, None), crashing with: join() argument must be str, bytes, or os.PathLike object, not 'NoneType' before any download was even attempted. This matches the exact debug log in the issue and the settings-dialog download flow the reporter exercised. Fix: - Add the missing "it"/"en-in" entries to the medium/large tables so all 10 supported languages now resolve consistently across all three sizes. - Add a guard in _get_vosk_model_path() that raises a clear ValueError ("No VOSK '<size>' model available for language '<lang>'...") instead of crashing deep inside os.path.join, so any future data gap fails loudly and helpfully rather than with a cryptic TypeError. |
||
|
|
3d65e5cea9 |
chore(release): prepare v0.14.2 (#537)
* chore(release): prepare v0.14.2 IBus stability patch on the 0.14 series: engine process launch after XDG import (#534) and FocusIn gate for first dictation on GNOME Wayland (#533). Keep 0.14 series feature list in README/UPDATE for patch releases; changelog and AppStream note only the delta. Document that rule in RELEASE_PROCESS. * fix(release): address 0.14.2 review findings - Dates: v0.14.2 uses 2026-07-17 (not before 0.14.1) - AUR: sha256sums=SKIP until tag; document updpkgsums in RELEASE_PROCESS - Single version source: package __init__ re-exports version.py; User-Agent uses __version__ - Docs: drop redundant series recaps; shrink AppStream/changelog delta - README ecosystem status: Stable v0.14.2 (not Beta) * docs(release): include settings scroll fixes in 0.14.2 notes Cover #538 and #541 (scrollable settings tabs / wheel polish) in README, UPDATE, website changelog, AppStream, and version history after rebasing onto main. |
||
|
|
9e9f197a7b |
fix(ibus): wait for FocusIn before commit on scoped injection (#533)
* fix(ibus): wait for FocusIn before commit on scoped injection Cold first activation of the vocalinux engine on GNOME Wayland could commit before mutter bound a client context, so logs reported success while the first dictation of a session was dropped. Fixes #523 * refactor(ibus): clear focus event directly in do_destroy Drop the clear_focus callback on the destroy helper; one local branch is enough. * test(ibus): cover FocusIn gate helpers for patch coverage Extract PING/focus-wait helpers and exercise them so codecov/patch clears the 80% target on the #523 fix. |
||
|
|
1e81b330c0 |
fix(ibus): restore engine process launch after XDG path import (#534)
* fix(ibus): restore engine process launch after XDG path import PR #484 switched ibus_engine to a relative import of xdg_data_home for Flatpak-safe paths. start_engine_process and the IBus component exec still run this file by path (python /path/to/ibus_engine.py), which has no package context, so the process died immediately with ImportError and Vocalinux fell back to ydotool (clipboard paste). Accept absolute/inline import fallbacks when loaded as a script, log child stderr from engine.stderr.log on startup failure, and add a path-based --xml smoke test so this does not regress. * test(ibus): cover start_engine_process failure diagnostics Raise patch coverage for the engine.stderr.log failure paths so codecov/patch meets the 80% target on #534. * chore: re-trigger CI after rebase onto main |
||
|
|
853c1504b2 |
chore(release): prepare v0.14.1-beta (#525)
Patch release for Flatpak/AUR packaging and post-0.14.0 fixes: installer wrappers, ydotool dual API and Flatpak socket probe, model download timeouts, layout-aware hotkeys, and related docs/web bumps. |
||
|
|
3deb523efd |
feat(flatpak): add Flatpak packaging for universal distribution (#484)
* feat(flatpak): add Flatpak packaging for universal distribution Adds a Flathub-ready Flatpak for the whisper.cpp engine, plus the minimal runtime adaptations Flatpak actually needs — deliberately scoped down from the earlier attempts (#416, #428), which bundled unrelated whisper.cpp GPU, audio, and clipboard changes. Packaging (packaging/flatpak/): - app manifest, AppStream metainfo, and desktop entry (com.vocalinux.Vocalinux) - generated offline Python dependency modules (flatpak-pip-generator) - bundled shared modules: PortAudio, libayatana-appindicator, xdotool, xsel Runtime changes (Flatpak-only, behaviour unchanged outside a sandbox): - utils/paths.py: honour XDG_CONFIG_HOME/XDG_DATA_HOME so config, models and runtime data live under ~/.var/app/<app-id> in the sandbox - autostart uses `flatpak run <app-id>` (the host launcher is always on PATH) - text injection and keyboard backend detect a Wayland host reached via XWayland (no Wayland socket granted) and route through xdotool / pynput-on-X11 - tray indicator references the exported `<app-id>-*` host icon theme names Also: a CI workflow building x86_64 + aarch64 bundles, and README / packaging README / distro-compatibility doc updates. Verified with a full flatpak-builder build + bundle install on aarch64: pywhispercpp loads its native libraries without LD_LIBRARY_PATH, XDG paths redirect correctly, bundled tools are present, and the entrypoint runs. Manifest and builddir lint are clean and AppStream metadata validates. Closes #167 * chore(flatpak): sync metainfo version to 0.14.0-beta Also ignore flatpak-build-dir/ so local builder output stays untracked. * refactor(flatpak): trim docs and comments after ponytail review Drop redundant packaging README command blocks, shrink path/tray/autostart comments, and stop ignoring a non-canonical build-dir name. * fix(flatpak): close readiness gaps for v0.14.0-beta Route log files through XDG data dirs so Flatpak keeps them under ~/.var/app, refresh AppStream screenshots to vocalinux.com, and use the SPDX GPL-3.0-only license id. * fix(flatpak): enable global hotkeys via evdev and --device=input pynput over XWayland only receives keys for the focused X client. Prefer evdev inside Flatpak and grant /dev/input so shortcuts work app-wide. * fix(flatpak): inject text with ydotool/uinput on Wayland xdotool only reaches XWayland clients, so typing appeared to succeed but never hit native apps. Ship ydotool, grant --device=all for uinput, start ydotoold on demand, and type via uinput inside the Flatpak. * fix(flatpak): paste via wl-copy + Ctrl+V instead of typing ydotool type injects character-by-character. Ship wl-clipboard, grant --socket=wayland, and prefer clipboard paste for instant insertion. * test(text-injection): expect ydotool default key-delay of 2 Matches the shipped VOCALINUX_YDOTOOL_KEY_DELAY default used when clipboard paste falls back to character typing. |
||
|
|
0a7acaea2f |
fix(hotkeys): layout-aware combo keys for non-US layouts (#513) (#514)
* fix(hotkeys): resolve combo letters via active XKB layout Settings capture stores character tokens from GDK keyvals (e.g. alt+a), but the Wayland/evdev path mapped letters to fixed US KEY_* codes. On AZERTY that made Alt+A listen for the key labeled Q. Resolve letter tokens through libxkbcommon for the active layout so recording and listening agree. Fixes #513 * refactor(hotkeys): slim layout-aware key resolution Drop unused resolve helper, dual knobs, setxkbmap/localectl probes, and redundant tests. Keep gsettings + libxkbcommon map and the AZERTY combo path. * test(hotkeys): drop redundant AZERTY resolve-only case Keep fire/no-fire combo coverage and the XKB fr map check. * test(hotkeys): cover layout map detection and error paths Raise patch coverage for layout_key_map / evdev_code_for_key so codecov patch target (80%) is met. |
||
|
|
1441be0512 | fix(text-injection): treat XIM none as unset (#512) | ||
|
|
f456160c45 |
fix(ui): keep preset and custom shortcut selection exclusive (#509)
Selecting a preset after recording a custom combo left the custom entry filled, so both looked active. Sync the dropdown to "Custom Shortcut" when a non-preset is set, and clear the custom field when a preset wins. |
||
|
|
872a745bc4 |
fix(text-injection): restore GNOME Wayland IBus with bare xkb engine (#506)
#502 only exempted KDE from the #478 real-engine gate. GNOME/Mutter has the same scoped-activation path, so stock sessions with only an xkb:* layout fell through to wtype, which Mutter rejects, then clipboard-only. Allow any Wayland session without an explicit non-IBus IM to try IBus when ibus-daemon is running; unbridged compositors still hit the existing denylist. Fixes #504 |
||
|
|
85927be4b4 |
feat(asr): FunASR/SenseVoice support for the Remote API engine (#468)
* feat(asr): add chat audio remote benchmarking * style(asr): apply black formatting * test(main): include remote API model kwarg * test(asr): add coverage for chat-completions error handling and parsing Add 10 new tests to cover uncovered lines in PR #468: - ConnectionError handler in _try_chat_completions_audio_api (lines 1601-1602) - _parse_chat_completion_transcription edge cases: None, list content, empty/whitespace strings, JSON without text field, plain text, and single-line language metadata (lines 1610, 1613-1621, 1625, 1629-1640) Coverage for recognition_manager.py improved from 85% to 86%. * feat(asr): support FunASR remote responses * refactor(asr): scope remote FunASR support to the #469 path Drop chat-completions/Qwen client, evaluation notes, and the remote ASR benchmark harness. Keep model field, tolerant OpenAI response parsing, SenseVoice label stripping, FunASR docs, and tests. |
||
|
|
b3605bf3c6 |
feat(shortcuts): configurable modifier+key hotkeys (e.g. Alt+R) (#493)
* feat(shortcuts): configurable modifier+key hotkeys (e.g. Alt+R) Generalize the shortcut system beyond single-modifier double-tap/hold to support modifier+key combos (alt+r, ctrl+alt+r, super+space) in both toggle (press once) and push-to-talk (hold) modes. - base.py: ShortcutSpec + parse_shortcut_spec/is_valid_shortcut/format label; parse_shortcut kept for back-compat (bare modifiers still rejected). - evdev + pynput backends: combo detection with global (cross-device) modifier tracking so split keyboards work; legacy gestures unchanged. - keyboard_shortcuts: validate via is_valid_shortcut instead of a fixed allowlist. - settings_dialog: additive Custom Shortcut row (typed entry + live Record capture). - tests: 34 new tests (parser, back-compat, evdev + pynput combo, split keyboard). * fix(shortcuts): apply saved shortcut (not stale preset) on mode change When a custom shortcut (e.g. alt+r) is active, the preset combo still points at a default/previous preset. _on_shortcut_mode_changed read the combo, so toggling Toggle<->Push-to-Talk restarted the live listener on the wrong shortcut while config still said alt+r. Read the saved shortcut from config instead. Addresses Codex P2 review on PR #493. * style: apply black formatting to configurable-hotkeys files * test: cover _gdk_keyname_to_token shortcut-capture mapping * fix(shortcuts): reset combo state after dropped/lost evdev events SYN_DROPPED and device-disconnect recovery cleared key_pressed_devices but not the combo state. A dropped modifier *release* left _combo_pressed thinking the modifier was held, so the main key alone could falsely toggle/start dictation, and a push-to-talk hold could stay stuck. Add _reset_combo_state() (clears the pressed set and ends any live hold) and call it from both recovery paths. Addresses maintainer review on PR #493. * fix(settings): combo-aware toggle label (single press, not double-tap) _update_shortcut_ui_for_mode always said 'Double-tap the key' for toggle mode, but a modifier+key combo (e.g. alt+r) toggles on a single press. Read the saved shortcut and use get_shortcut_display_name so a combo shows 'Press Alt+R'. Addresses maintainer review on PR #493. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
a2a5c569c5 |
fix(text-injection): wait for modifiers to release before Wayland injection (#494)
* fix(text-injection): wait for shortcut modifiers to release before Wayland inject
Injection emits real key events, so a held modifier from a toggle/PTT shortcut
(e.g. Alt from Alt+R) turns the simulated Ctrl+V paste into Ctrl+Alt+V and
nothing pastes — while the clipboard still shows the text. Because transcription
can finish in tens of ms, the user is often still holding the key, causing
intermittent paste failures.
Poll evdev for held modifier keycodes and defer injection until they clear
(bounded by VOCALINUX_INJECT_MODIFIER_WAIT, default 1.0s, 0 disables). Returns
immediately when nothing is held, so no added latency in the common case.
* style: black-format injection modifier-wait test
* test: cover _held_modifier_keycodes evdev scanning (patch coverage)
* test: cover error paths in _held_modifier_keycodes
* fix(text-injection): treat non-finite modifier-wait env as default
float('inf') parses cleanly and is > 0, so it slipped past the guard and made
the wait deadline infinite -- a held modifier could block injection forever
(nan was nonsensical too). Sanitize VOCALINUX_INJECT_MODIFIER_WAIT via a helper
that falls back to the 1.0s default for non-finite values. Regression tests
added, including that 'inf' still terminates.
Addresses maintainer review on PR #494.
|
||
|
|
a9a6ae3c51 |
fix(text-injection): restore KDE Wayland IBus path (#502)
* fix(text-injection): restore bridged Wayland IBus path * fix(text-injection): keep GNOME xkb fallback |
||
|
|
4e3637dfb5 |
Fix: IBus engine restore uses wrong fallback on GNOME/Wayland (#497) (#500)
* Fix: IBus engine restore uses wrong fallback on GNOME/Wayland (#497) On GNOME/Wayland, when IBus has no global engine (common when Mutter manages input sources), 'ibus engine' can return a misleading 'xkb:us::eng' default that doesn't match the user's actual keyboard layout. When Vocalinux restores this wrong value after text injection, Electron apps switch to US layout. Root cause: get_current_engine() trusted the output of 'ibus engine' without checking whether IBus actually had a global engine set, and had no fallback to discover the real keyboard layout. Fix: - Add get_current_engine_gnome_fallback() that reads the actual keyboard layout from org.gnome.desktop.input-sources via gsettings - Modify get_current_engine() to detect 'No global engine' in stderr and to detect the suspicious 'xkb:us::eng' default on GNOME/Wayland, falling back to gsettings in both cases - Modify stop() to log a message when skipping engine restoration because no valid engine was captured (None means don't restore) - Modify inject_text() to log when skipping restoration after injection for the same reason - Backward compatible: if gsettings is unavailable, falls back to current behavior (returning None or the IBus-reported value) Tests: 12 new tests covering GNOME fallback, 'No global engine' detection, suspicious default detection, gsettings parsing, and edge cases. * ponytail: simplify gsettings uint32 parsing gsettings prints 'uint32 N' or just 'N' — split()[-1] handles both, no need for separate branches --------- Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
3303d075a6 |
Fix: Audio device index mismatch causing crash on recording start (#498) (#499)
* fix(audio): resolve device by name to prevent index mismatch crash (#498) Audio device indices can shift between sessions when USB devices are replugged or virtual devices are added/removed. A saved device_index=24 (BRIO webcam) could later point to 'speech-dispatcher-dummy', causing PortAudio to attempt recording from a virtual device and triggering malloc corruption / SIGABRT. Fix has three parts: 1. Store and match by device name, not just index - Config already saves device_name alongside device_index - At recording time, _resolve_device_by_name() enumerates current devices and finds the one matching the saved name - Falls back to stored index, then system default (with warning log) - main.py now passes audio_device_name to SpeechRecognitionManager 2. Filter virtual devices from input device enumeration - _is_virtual_device() detects speech-dispatcher, dummy, null sink, monitor, and ALSA output devices - get_audio_input_devices() skips virtual devices - Settings dialog dropdown only shows real input devices - Debug logging in _record_audio also skips virtual devices 3. Validate resolved device has input channels (from flatpak branch) - Extra safety check: after resolving, verify maxInputChannels > 0 - Falls back to system default if resolved device is invalid - Brings the validation from feature/flatpak-packaging to main Closes #498 * style: apply black formatting to recognition_manager.py * test: add audio_device_name kwarg to mock assertion * test: add coverage for _is_virtual_device, _resolve_device_by_name, device name management * test: fix StopIteration in fallback test by using return_value for mock * ponytail: remove redundant validation and simplify _resolve_device_by_name - Remove ~20 lines of dead validation in _record_audio() — _resolve_device_by_name and _resolve_valid_input_device already check maxInputChannels and virtual devices - Simplify _resolve_device_by_name to just find index by name, then delegate all validation to _resolve_valid_input_device (which already does channel/virtual checks) - Update tests to match the simplified delegation pattern * style(speech): format device fallback warning * test(speech): cover audio device fallback edges * test(speech): cover reconnection device fallback --------- Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
8f32f01f0e | fix(installer): include xsel for Wayland clipboard fallback (#496) | ||
|
|
6bb6d8802e |
fix(ibus): require a real IM engine on Wayland (#478) (#491)
* fix(ibus): require a real IM engine on Wayland (#478) On Wayland a bare xkb:* IBus engine is a layout passthrough that never delivers commit_text() to applications, so selecting the IBus backend there silently no-ops (niri/sway, and GNOME/Mutter via the env-var branch). Require a real IM engine before treating IBus as active on Wayland, regardless of how it was detected; X11/XWayland (XIM) is unchanged. Complements #486's compositor denylist. * test(ibus): bind #478 patches to the module under test Patch get_current_engine/is_ibus_daemon_running via patch.object on a held ibus_engine reference and call is_ibus_active_input_method through it, so the patched functions and the function under test always share one module object. Fixes flakiness in the full CI suite where other test files re-import ibus_engine and the dotted-string patch bound a different module object. |
||
|
|
943d2b5cc1 |
fix(ibus): don't run setxkbmap on Wayland (#474) (#490)
On Wayland, setxkbmap only reaches the XWayland X11 server, so restore_xkb_layout() rewrote the XWayland layout (flipping it to us for XWayland/Electron apps) while native Wayland apps stayed correct. Make restore_xkb_layout() a no-op on Wayland and stop defaulting the captured layout to 'us' so a never-captured layout is never re-applied. |
||
|
|
34eb2f7ecf |
feat(text-injection): speed up ydotool injection with explicit --key-delay (#488)
Pass --key-delay 8 to ydotool type instead of relying on the default (12-20ms). This makes text injection ~3x faster while keeping key-delay above zero to avoid the Shift-leak bug where capital letters corrupt following characters. The delay is configurable via VOCALINUX_YDOTOOL_KEY_DELAY env var. Note: --key-hold flag from the original issue does not exist in standard ydotool (verified against official man page and v1.0.4 source). Fixes #482 |
||
|
|
03b0efb51d |
fix(text_injection): use wtype on compositors that don't bridge IBus to native apps (#486)
* fix(text_injection): use wtype on compositors that don't bridge IBus to native apps On Wayland compositors that don't bridge IBus to native clients (COSMIC, Sway, Hyprland, Wayfire, river, niri, ...), the IBus engine's commit_text() reaches only XWayland and GTK/Qt apps that load the IBus IM module. Native apps such as cosmic-term receive nothing, yet inject_text() reports success, so dictation silently does nothing. - Add a compositor denylist (_wayland_compositor_bridges_ibus) so IBus is skipped on those compositors and the virtual-keyboard path (wtype/ydotool) is used instead. A denylist (rather than an allowlist) keeps the existing IBus-preferred behaviour on unrecognised desktops. Complements the recent KDE Plasma Wayland handling, which covers the mirror-image case. - Replace the unreliable `ydotool type ""` probe (exits 0 even with no daemon) with _is_ydotoold_running(), so a daemonless ydotool install correctly falls back to wtype instead of selecting a no-op ydotool. - Add unit tests for the compositor gate, the ydotoold check, and that an active-IBus COSMIC session selects wtype. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(text_injection): cover ydotoold socket-env and ydotool direct-mode paths Add tests for the two new branches codecov/patch flagged as uncovered: the explicit YDOTOOL_SOCKET path in _is_ydotoold_running(), and the ydotool direct-mode fallback when no daemon and no wtype are available. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
14972672c5 |
fix(wayland): reliable clipboard copy and ydotool injection (#480)
Two independent text-injection bugs on Wayland: 1. Clipboard copy silently failed. wl-copy/xclip/xsel fork a background process that keeps owning the selection; capturing its stderr with subprocess.PIPE makes run() block on the surviving child until the clipboard is next overwritten, so the call always hits its timeout and _copy_to_clipboard reports failure even though the copy succeeded. Redirect stdout/stderr to DEVNULL so run() only waits for the short-lived foreground process. 2. ydotool scrambled text on non-US keyboard layouts. `ydotool type` emits positional evdev keycodes that get re-interpreted through the active layout (assumed US QWERTY), so dictated text came out garbled on AZERTY/QWERTZ/Dvorak (e.g. "message" -> ",essqge") and non-ASCII was dropped. Always inject via clipboard paste (Ctrl+V) for ydotool, which is layout-independent and Unicode-safe. Adds regression tests for both. Claude-Session: https://claude.ai/code/session_01EhyratrEE4vDWhriirUB9k |
||
|
|
c64b7270b3 |
fix(evdev): rescan hotplugged keyboard devices (#467)
* fix(evdev): rescan hotplugged keyboard devices * test(evdev): cover hotplug monitor edge cases |
||
|
|
e0ac5e409c |
feat(whisper): add guided whisper.cpp model variants (#465)
* feat(whisper): refine whisper.cpp model selection * refactor(whisper): exclude diarization model from picker * docs(whisper): document guided model variants * fix(ui): cap settings dialog height at 600px to prevent excessive height on high-res displays |
||
|
|
9c5065f452 | fix(input): clarify KDE Wayland IBus setup (#466) | ||
|
|
a73eec4a0a | fix(dictation): preserve spacing between silence segments (#464) | ||
|
|
49858f14e3 |
fix(ibus): capture current engine during prepare_engine for scoped activation (#458)
* fix(ibus): capture current engine during prepare_engine for scoped activation Two bugs were causing IBus injection to fail and fall back to ydotool: 1. get_current_engine() returned empty string (falsy) when 'ibus engine' output was empty, instead of None. This caused inject_text() to think no engine was available. 2. prepare_engine() didn't capture the current IBus engine, so when inject_text() tried to get it at runtime, it would fail on systems where 'ibus engine' returns empty output. Fix: get_current_engine() now returns None for empty output. prepare_engine() captures the current engine before starting the Vocalinux process. inject_text() uses the captured _previous_engine as fallback when get_current_engine() fails. Fixes the regression from PR #457 where IBus injection would fail with: WARNING Could not determine current IBus engine ERROR IBus engine instance is not active and the previous engine could not be captured safely * fix(ibus): always activate engine for injection on Wayland On Wayland, 'ibus engine' returns empty output, so get_current_engine() returns None. The previous code would skip activation when it couldn't determine the current engine, causing injection to fail and fall back to ydotool. Now we always activate the Vocalinux engine when it's not active, regardless of whether we can capture the previous engine. If we couldn't capture it, we simply don't restore anything after injection. This fixes the regression where IBus injection would fail with: WARNING Could not determine current IBus engine ERROR IBus engine instance is not active and the previous engine could not be captured safely Fixes #458 * test(ibus): add missing mocks for is_engine_active and get_current_engine Existing tests didn't mock is_engine_active() and get_current_engine(), which are now called at the start of inject_text() for scoped activation. This caused CI failures on Python 3.9-3.13 where the unmocked calls would try to run 'ibus engine' and get unexpected results. |
||
|
|
e1384aad2e |
fix(ibus): preserve user engine for dead keys (#457)
* fix(ibus): preserve user engine for dead keys Keep the VocaLinux IBus engine warm without selecting it as the active keyboard engine. Temporarily switch to the VocaLinux engine only while committing dictated text, then restore the user's previous IBus engine. Wait for the IBus commit to complete before acknowledging socket injection, so engine restoration does not race the commit. This fixes dead-key composition (e.g. ^ + e → ê) on French AZERTY and other layouts that rely on the active IBus/XKB engine for compose state. Fixes #442 Supersedes #446 (rebased onto main after thread-safety hardening in #452) * test(ibus): add coverage for scoped activation error paths Cover previously-untested error paths in inject_text() scoped activation: - switch_engine failure during temporary Vocalinux activation - unknown current engine (get_current_engine returns None) - NO_ENGINE retry when previous engine could not be captured - ConnectionRefusedError exhausting all retries - FileNotFoundError retry-then-succeed and retry-then-fail - Generic exception catch-all |
||
|
|
0c3c32f584 |
fix(installer): harden CUDA diagnostics with auto-remediation and behavioral tests (#451)
* fix(installer): surface pywhispercpp CUDA fallback diagnostics * fix(installer): add patchelf auto-remediation and behavioral tests - Add patchelf auto-remediation for bundled libcuda linkage issue - Add ARM64 CUDA toolkit path support (aarch64-linux, sbsa-linux) - Fix nvcc symlink resolution with readlink -f - Add --verbose flag to CPU pywhispercpp pip install - Add behavioral tests for CUDA diagnostic functions - Add string-presence test for patchelf remediation Fixes #450 * fix(installer): replace ★ with * to fix box border alignment The ★ character (U+2605) renders as 2 columns wide in most terminals, but the box padding assumes 1 column. This pushes the right border │ one column past the intended box edge, making it appear cut off. Replacing with ASCII * (always 1 column) fixes the alignment. |
||
|
|
ddf073711b |
fix(ui): respect advanced toggle for Remote Server section (#454)
* fix(ui): respect advanced toggle for Remote Server section The Remote Server section was packed directly into advanced_tab, bypassing the advanced_revealer controlled by the 'Unlock Advanced Settings' toggle. This made it always visible regardless of toggle state. Changes: - Move remote_api_group and remote_status_label packing from advanced_tab to controls_box (inside the revealer) - Add visual separator between whisper.cpp controls and Remote Server - Increase scrolled min_content_height from 350 to 500 for taller content - Update power_user_switch tooltip/subtitle to mention remote server - Update info label to clarify whisper.cpp vs remote server scope - Align connection test to use requests.Session (consistent with transcription path) - Add empty-URL warning when toggling remote server ON - Add source-assertion tests for UI structure Fixes: Remote Server now hidden when 'Unlock Advanced Settings' is OFF * refactor(ui): move remote server to Speech Engine tab as peer engine Remote server is now a first-class engine option in the Speech Engine tab dropdown, alongside Vosk, Whisper, and whisper.cpp. The 'Use remote server' override toggle has been removed from the Advanced tab. Changes: - Add 'Remote API' to engine combo dropdown (removed skip logic) - Move remote server UI fields (URL, API key, endpoint, test button) from Advanced tab to Speech Engine tab - Show/hide remote server fields based on engine selection - Remove 'Use remote server' toggle and override logic - Simplify get_selected_settings (engine comes from combo directly) - Simplify _update_engine_specific_ui (check combo value) - Remove _on_use_remote_toggled method - Update _load_and_apply_settings (no remote switch logic) - Update power_user tooltip/subtitle (no longer mentions remote server) - Update tests for new behavior Backend requires zero changes - recognition_manager.py already treats remote_api as a first-class engine. User flow: 1. Select 'Remote API' from engine dropdown 2. Model selection hides automatically 3. Remote server config fields appear (URL, API key, endpoint) 4. Enter server details and test connection 5. Done - clear and intuitive |
||
|
|
f417c28efa |
fix(threading): harden thread safety for Remote API, IBus, and text injection (#452)
* fix(threading): harden thread safety for HTTP session, IBus, and text injector Four critical thread-safety fixes identified during PR #335 review: 1. Remote API: snapshot _http_session under _model_lock in _process_audio_buffer() to prevent use-after-close races with reconfigure/reinitialize. Pass session as explicit parameter to _transcribe_with_remote_api, _try_whispercpp_server_api, and _try_openai_api. Call _http_session.close() before setting to None in reinitialize_after_resume(). Remove blocking connection probe from _init_remote_api() (settings dialog has a Test Connection button for that). 2. Text injector: extend _state_lock to protect stop() (writes _ibus_injector=None and _ibus_ready=False) and inject_text() reads of environment/_ibus_injector. Also guard environment/wayland_tool writes in _switch_to_non_ibus_backend() and WAYLAND_XDOTOOL fallback in inject_text's exception handler. 3. IBus engine: snapshot cls._active_instance into a local variable before the do_inject closure in the socket server handler, closing the TOCTOU race with do_destroy(). 4. Test suspend handler: add _http_session=None to _make_manager() fixture so reinitialize_after_resume() finds the attribute. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * test(threading): update tests for thread-safety changes - test_recognition_manager_remote_api: pass session param to _transcribe_with_remote_api, _try_openai_api, and _try_whispercpp_server_api. Update assert_called_once_with patterns for the new session argument. Simplify init tests that no longer need connection probe mocks (removed from _init_remote_api). - test_text_injector: add threading.Lock to all TextInjector.__new__() instances so _state_lock is available for lock-guarded methods. - test_suspend_handler: add _http_session=None to _make_manager() fixture so reinitialize_after_resume() finds the attribute. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * test(threading): improve coverage for thread-safety fixes Add tests for _process_audio_buffer remote API dispatch (session snapshot under lock, early return when session is None) and reinitialize_after_resume session close. Mark server_thread as pragma: no cover (blocked by conftest to prevent CI hangs). |
||
|
+1 |
84d2e1a929 |
feat: add remote API speech recognition engine with installation and configuration support. (#335)
* feat: add remote API speech recognition engine with installation and configuration support. * fix: reformatted by black * fix: unit test in test_main_initializes_components * fix typo * Apply suggestion from @jatinkrmalik Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> * feat: Validate remote API URL format, enhance connection testing, update default remote API endpoint. * fix typo * feat: add remote API recognition manager test suite * fix: add Python 3.14 support and lxml>=6.1.0 (fixes #404) - Add Python 3.14 to classifiers - Add lxml>=6.1.0 to dependencies to fix install on Python 3.14 (CachyOS, Arch) - Update black target-version to include py314 Fixes #404 Closes #405 * fix(install): validate pyproject.toml/setup.py content before entering local repo mode (fixes #396) Fix false local repo detection when users run `uv init` before installation. Changed detection from naive file existence check to content validation: - For pyproject.toml: check for `name = "vocalinux"` - For setup.py: check for "vocalinux" string - Falls through to remote clone mode if neither matches Fixes #396 * fix(recognition): preserve final speech on stop (fixes #401) Replace hard-coded stop tail trim with configurable stop_sound_guard_ms (default 200ms) so quick key release no longer clips the final spoken word in push-to-talk mode. Co-authored-by: Fakhareddine <fe.sabbari@claranet.com> * fix(ibus): handle engine instance destruction on layout switch (fixes #388, closes #389) Override do_destroy() to clear _active_instance when IBus destroys the engine on keyboard layout switch (Super+Space). Add retry logic in inject_text() — on NO_ENGINE, re-activates via switch_engine() and retries once to recover cleanly. Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com> * chore: ignore opentree worktree dir and config (#412) .opentree/ contains git worktrees created by the opentree CLI tool (https://github.com/axelgar/opentree). These are local developer artifacts and should not be tracked. opentree.toml is the per-repo opentree config - also local/personal, not something contributors need to share. * test(ibus): fix do_destroy CI failure (issubclass TypeError without IBus) (#410) * test(ibus): fix do_destroy super() test failing in CI without IBus installed Patch builtins.super instead of relying on mock_ibus.Engine.do_destroy, which fails with issubclass() TypeError when VocalinuxEngine is built with object as its base (IBus unavailable in CI). * test(ibus): fix MagicMock.__new__ TypeError on Python < 3.13 VocalinuxEngine inherits from MagicMock at import time (IBus is mocked via sys.modules at test-file level). Calling VocalinuxEngine.__new__(VocalinuxEngine) delegates to MagicMock.__new__ which runs issubclass(cls, AsyncMockMixin). On Python < 3.13 that raises TypeError when the module-cached class is a MagicMock spec object rather than a real type. Fix: introduce _make_engine() helper that allocates via a thin concrete subclass whose __new__ calls object.__new__ directly, bypassing the MagicMock allocation path entirely. Verified across all three TestVocalinuxEngineDestroy test methods. * test(ibus): use unbound do_destroy call to avoid MagicMock interference The previous fix (thin _ConcreteEngine subclass) still failed because VocalinuxEngine inherits MagicMock.__getattribute__, which intercepts .do_destroy on instances and returns a mock attribute rather than the real method - regardless of Python version. Call VocalinuxEngine.do_destroy(engine) directly with a plain object() as the self argument instead. do_destroy only uses self for identity comparison (is VocalinuxEngine._active_instance), so any object works. This bypasses MagicMock's MRO entirely. * test(ibus): reload module and instantiate real engine class for do_destroy tests * refactor(ibus): extract destroy handler for stable cross-version tests * test(ibus): cover destroy helper branches and do_destroy delegation * test(ibus): stabilize do_destroy CI behavior across environments Avoid brittle super() mocking in no-IBus environments and gate the IBus-only parent destroy path test behind runtime capability checks to prevent false CI failures on Python matrix runners. * test(ibus): remove flaky do_destroy class-path assertions Drop brittle class-method destroy tests that fail under mocked GI/IBus on older Python runners and keep deterministic helper-level coverage for destroy behavior. * fix(text-injection): recover from IBus runtime failures (#411) * fix(ibus): retry and recover from transient socket failures Harden IBus injection against connection-refused, timeout, and engine process races so dictation is more resilient in real-world desktop sessions. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * fix(text-injection): fallback to non-IBus backend on runtime IBus failures Allow dictation to continue when IBus runtime injection fails by automatically switching to xdotool/wtype/ydotool fallbacks instead of returning an immediate failure. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * fix(ibus): preserve live socket injection during recovery Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * test(coverage): add runtime fallback and ibus retry coverage Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * fix(ibus): probe engine readiness at startup and harden NO_ENGINE retries (#391) * fix(ibus): wait for engine readiness before first injection Add a startup readiness probe that pings the IBus engine after activation so first-use dictation no longer races do_enable(). Also switch NO_ENGINE retry behavior to exponential backoff and cover the new flow in tests. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test(ibus): cover readiness probe edge cases Add focused tests for the new startup readiness path, including direct PING success, retry exhaustion, unexpected responses, and engine re-activation before injection. This raises coverage on vocalinux.text_injection.ibus_engine from 68% to 73% in the targeted suite. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(text-injection): avoid blocking IBus and clipboard fallbacks Warm IBus in the background so startup no longer stalls while readiness probing, and make clipboard helpers fail fast with per-session health caching so non-ASCII ydotool fallback does not lock dictation. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test(ibus): update retry tests for merged NO_ENGINE recovery logic --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * chore(deps-dev): bump postcss from 8.4.39 to 8.5.10 in /web Bump postcss dev dependency (CSS processing). No runtime impact. * fix(logging): clean up runtime log noise and cache hardware detection Clean up duplicated code artifacts from recent merges and cache hardware detection to eliminate ~1.3s UI lag when opening settings dialog. * test: cover recognition internals and suppress desktop notifications (#414) * test(recognition): cover _perform_recognition and _enqueue_audio_segment The previous commit demoted logger.info('DEBUG: ...') to logger.debug(...) and removed a dead loop body in _perform_recognition. Codecov flagged 12 uncovered lines — all inside _perform_recognition and _enqueue_audio_segment, which existing tests always mocked out. Add direct (non-mocked) tests for both methods: - _enqueue_audio_segment: * empty buffer → early return * normal buffer → enqueues correctly * full queue → drops oldest and retries - _perform_recognition: * processes a segment then exits on None signal * drains remaining items on None signal * exits cleanly when idle with empty queue * continues looping on queue timeout while recording These tests exercise the actual threading/queue logic and cover the logger.debug / logger.warning calls as a side effect. * test(conftest): suppress real desktop notifications during test runs _show_notification shells out to notify-send, which pops up real system notifications when tests exercise the model-not-ready path. Add an autouse fixture that patches _show_notification for all tests EXCEPT those whose name contains 'show_notification' (so direct tests of the helper still verify subprocess.Popen calls). * chore(gitignore): ignore .sisyphus directory * fix(install): improve openSUSE dependency handling with fallbacks (#418) The suse case in install_system_dependencies() previously installed all packages blindly, causing the entire install to fail if a single package was unavailable (e.g., glslang on some openSUSE repos). Changes: - Check each package individually with rpm -q before installing - Add graceful fallback for glslang -> glslang-devel - Add graceful fallback for libappindicator-gtk3 -> typelib-1_0-AppIndicator3-0_1 -> libayatana-appindicator3-1 - Continue installation with warning instead of hard failure Fixes #407 * fix(remote_api): re-init engine on resume from suspend Add a remote_api branch to reinitialize_after_resume mirroring the dispatch chain in __init__, so the engine is recreated after a suspend/resume cycle. Null self._http_session alongside the existing self.model and self.recognizer resets so a stale pooled-connection session is not reused after resume. * perf(remote_api): pool HTTP connections via requests.Session Introduces a manager-owned requests.Session (_http_session) so all remote_api engine HTTP traffic reuses a single connection pool instead of opening a new connection for every request. Lifecycle: - __init__: self._http_session = None (attribute always present) - _init_remote_api: close prior session if it exists, then create a new requests.Session(); use session.get for the connection test. Prevents connection leaks on back-to-back inits (re-init guard). - _try_openai_api / _try_whispercpp_server_api: replaced requests.post with self._http_session.post; local 'import requests' retained for the requests.exceptions.ConnectionError catch. - reconfigure: captures old engine before self.engine is updated; closes and nulls _http_session when switching away from remote_api. - reinitialize_after_resume: nulls _http_session before _init_remote_api recreates it. Tests: added TestHTTPSession class (4 cases) in tests/test_recognition_manager_remote_api.py; all 40 tests pass. Updated _setup_requests_get_*/post_* helpers to configure the session instance (mock_requests.Session.return_value) instead of the module-level mock, keeping all 36 pre-existing tests green. * docs: add remote HTTP transcription setup guide * fix(install): avoid asking for remote server URL twice run_interactive_install collects the URL upfront in Step 3, but install_python_package re-declared REMOTE_API_URL as local and prompted again, ignoring the wizard value. Inherit the wizard's value instead. * ui(settings): hide remote API behind an Advanced tab Addresses upstream PR #335 review feedback from @jatinkrmalik that remote API is not plug-and-play and should not be a peer of the local engines for casual users. - Remove 'Remote API' from the Speech Engine tab's engine combo; the combo now shows local engines only (vosk / whisper / whisper_cpp). - Add a new 'Advanced' notebook tab with a 'Remote Server' group containing a 'Use remote server' switch, the existing URL/Key/ Endpoint fields, and the Test Connection button + status label. - The switch overrides the local engine selection. get_selected_settings returns engine='remote_api' iff the switch is on, otherwise the combo selection. URL/Key/Endpoint persist independently of the toggle state. - _load_and_apply_settings: if config has engine='remote_api', the switch is turned on and the combo is set to a sensible local fallback (whisper_cpp, or first available local engine) so toggling off has somewhere to land. - _update_engine_specific_ui no longer special-cases the combo for remote_api (the combo never has it); it hides the model row, info card and legend whenever the remote switch is on. - Update HTTP_REMOTE.md to reflect the new tab location. * ui(remote-api): mark remote API as advanced and default toggle off - install.sh: relabel option 4 as 'REMOTE API (ADVANCED)' to signal it as a non-default, advanced engine choice. - settings_dialog.py: explicitly initialise the 'Use remote server' switch to inactive at widget creation, making the default state unambiguous (saved-config load logic still toggles it on for existing remote_api installs). * fix(remote-api): preserve trailing newline from transcription responses The remote-API transcription path stripped all whitespace from the upstream response before injection, silently discarding a trailing newline that some pipelines (post-processing proxies, fixup layers) emit to signal "submit" / Enter. Whisper itself doesn't emit trailing newlines, so for the common case this is a no-op; for users with a post-processing layer it stops their submit signal from being eaten. _filter_non_speech now uses a whitespace-stripped probe for its non-speech pattern checks but returns probe + "\n" when the original input ended with "\n" or "\r" (CRLF normalises to LF). The redundant .strip() at the call site is removed so the function receives the raw API text. Existing filter behaviour (BLANK_AUDIO, music notes, low speech-content ratio, etc.) is unchanged. Four new unit tests cover the trailing-newline cases. * test(remote-api): cover transcribe exception handler and unknown-engine reinit branch Adds two unit tests buttoning up previously-uncovered defensive branches: - _transcribe_with_remote_api swallows unexpected exceptions and returns "" — verified by raising RuntimeError from _try_whispercpp_server_api. - reinitialize_after_resume short-circuits without dispatching when the engine attribute is unknown — verified by mutating engine to a bogus value and asserting no _init_* method is called. * feat(settings): add Advanced tab with whisper.cpp anti-hallucination parameters (#415) * feat(settings): add Advanced tab with whisper.cpp anti-hallucination parameters Add a new Advanced settings tab that exposes whisper.cpp decoding parameters to help users reduce hallucinations and tune recognition behavior. Parameters exposed: - No Timestamps (default: on) - Suppress Non-Speech Tokens (default: on) - No Context (default: on) - Initial Prompt (default: empty) - Temperature (default: 0.0) - Temperature Increment (default: -1.0, disables fallback) - Entropy Threshold (default: 2.4) - Logprob Threshold (default: -1.0) - No-Speech Threshold (default: 0.6) All parameters are saved under the advanced config section and are only active when the whisper.cpp engine is selected. The UI grays out these settings and shows an explanatory note when another engine is active. Changes: - config_manager.py: add whisper.cpp keys to DEFAULT_CONFIG[advanced] - recognition_manager.py: read params from kwargs/config, pass to Model() - main.py: pass advanced settings from config to SpeechRecognitionManager - settings_dialog.py: new Advanced tab with switches, spin buttons, entry - tests: update test_main.py assertions, add whispercpp_advanced_defaults and whispercpp_advanced_persistence tests Fixes #363 * fix(whispercpp): gracefully filter unsupported params for pywhispercpp 1.4.1 pywhispercpp 1.4.1 does not expose no_timestamps or suppress_non_speech_tokens to Python even though they exist in the underlying whisper.cpp C struct. This causes AttributeError on startup. Add _load_model_with_compatible_params() which catches AttributeError, identifies the unsupported param from the error message, removes it, and retries until the model loads successfully or a non-AttributeError occurs. Also refactor _build_whispercpp_model_kwargs() from _load_whispercpp_model() for testability. Fixes local startup crash reported during PR testing. * fix(whispercpp): persist advanced settings correctly * chore(config): add advanced power-user default Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * feat(settings): gate advanced controls behind power-user opt-in Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * fix(whispercpp): prefilter unsupported native params * fix(settings): polish advanced whispercpp controls * fix(installer): reuse existing whispercpp builds (#421) * docs: clarify PyPI installation requirements (#423) * chore(deps): bump next (#429) Bumps the npm_and_yarn group with 1 update in the /web directory: [next](https://github.com/vercel/next.js). Updates `next` from 15.5.15 to 15.5.18 - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v15.5.15...v15.5.18) --- updated-dependencies: - dependency-name: next dependency-version: 15.5.18 dependency-type: direct:production dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(installer): repair pywhispercpp library loading (#433) * fix: play stop sound immediately on release (#426) * fix(recognition): play stop sound after audio thread joins to prevent capture on short recordings (#436) Moves play_stop_sound() to after audio_thread.join() but before the final buffer processing. This closes the race where the audio thread's in-flight chunk could capture the stop cue for short (≤ ~192 ms) recordings, while still feeling immediate compared to the original placement after all recognition cleanup. * fix(install): correct openSUSE Tumbleweed dependencies (#420) * fix(install): correct openSUSE Tumbleweed dependencies * style: format openSUSE installer changes --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> * fix(installer): harden Debian compatibility layer (#437) Add missing pywhispercpp build prerequisites (libssl-dev, autoconf, automake, libtool, patchelf) to both Debian and Ubuntu apt package lists. These are not pulled in transitively on clean Debian installs and cause CMake bootstrap failures during source compilation. Add runtime smoke test for pywhispercpp to verify_installation() so "installs but does not run" failures surface at install time with actionable diagnostic steps instead of silently at first launch. Treat Debian as a first-class target — remove the "primarily designed for Ubuntu" warning for Debian systems. Remain conservative for non-Debian, non-Ubuntu distros. Add DEBIAN_FRONTEND=noninteractive to all apt install commands to prevent interactive debconf prompts from blocking unattended installs. Improve ydotool failure messaging on Debian with compile-from-source instructions since ydotool is not packaged in Debian standard repos. Add Debian-specific troubleshooting section to DISTRO_COMPATIBILITY.md covering build deps, ydotool compilation, scoped source builds, and libwhisper.so resolution diagnostics. References: https://medium.com/@cslev/talking-to-my-linux-box-without-talking-to-the-cloud-vocalinux-on-debian-without-the-tears-10bf053ea21b * fix(install): refresh ldconfig after openSUSE typelib install, clarify python3XY placeholder convention (#438) - Add 'sudo ldconfig' after zypper install in suse_install_appindicator_runtime() to ensure GI typelib cache is refreshed so newly installed typelib packages are immediately discoverable by GObject introspection - Add section-level comment in distro-package-map.yaml explaining that python3XY is a placeholder convention resolved at install time by suse_python_package_prefix() / suse_python_package_candidates() Follow-up polish from review of #420. * fix(whispercpp): reduce CPU threads and ensure GPU backend builds in dev mode (#439) * fix(whispercpp): reduce CPU threads and ensure GPU backend builds in dev mode - Reduce default n_threads from cpu_count() (16) to max 8 (CPU) / max 4 (GPU) - Add runtime pywhispercpp GPU capability detection (_detect_pywhispercpp_gpu_backend) - Make n_threads configurable via whispercpp_n_threads config option - Fix install.sh regression: GPU source build was skipped in --dev mode - Extract GPU install logic into install_whispercpp_with_gpu_support() - Add is_pywhispercpp_gpu_capable() to detect GPU libs in installed pywhispercpp - Force rebuild pywhispercpp when GPU is detected but lacks GPU support Fixes CPU spiking to 100% on GPU systems due to excessive thread count and ensures dev installs get GPU acceleration. * fix(audio): skip output-only HDMI devices when selecting input device Add _resolve_valid_input_device() that filters devices by maxInputChannels > 0, falling back preferred → default → first valid. Previously, HDMI output devices (0 input channels) could be selected as the default audio input, causing 'Invalid number of channels' errors. Now both _record_audio() and _attempt_audio_reconnection() validate the device has input channels before probing or opening the stream. * fix(whispercpp): treat n_threads=0 as auto-detect, not literal zero Config default whispercpp_n_threads=0 was being passed directly to whisper.cpp as n_threads=0, causing 'cannot create std::vector larger than max_size()' transcription errors. Now 0 and negative values are treated as auto-detect, falling through to the smart thread count logic (max 4 with GPU, max 8 on CPU). * test: add whispercpp_n_threads to mock assertions and config defaults Update test_main.py mock to include the new whispercpp_n_threads=0 kwarg and test_config_manager.py to assert the default config value. * fix(audio): harden device enumeration against test mocks The new HDMI-filter helper and GPU-backend detector crashed unit tests that pass MagicMock() for the PyAudio/pathlib surfaces: - _find_pywhispercpp_shared_library_dirs hit TypeError when tests monkey-patch os.stat (Path.is_dir internally calls S_ISDIR on a non-int st_mode). Widen the catch to OSError/TypeError/ValueError so unconfigured stat results just skip the candidate dir. - _resolve_valid_input_device crashed with 'MagicMock > int' on audio.get_device_count() / info.get('maxInputChannels'). Coerce the count, treat zero/unknown as 'use system default', and include non-dict info objects rather than filtering everything out. No production behavior changes: real PyAudio always returns int counts and dict device info, so the HDMI filter still works as designed. * test(recognition): cover new HDMI filter and GPU backend detector - TestResolveValidInputDevice: 9 cases for the HDMI-filter resolver (preferred passthrough, HDMI fallback, no-default fallback, no-input None return, and three mock-tolerance escape hatches). - TestDetectPywhispercppGpuBackend: 4 cases for runtime GPU library detection (cpu/vulkan/cuda/empty dirs). - TestFindPywhispercppSharedLibraryDirsResilience: proves the widened exception catch survives a TypeError from pathlib.Path.is_dir. * test(text-injection): wait for IBus init thread to avoid race on slow runners The IBus environment promotion runs in a background daemon thread spawned by TextInjector.__init__. The two TestIBusRuntimeFallback cases asserted on injector.environment immediately after construction, which raced the thread on Python 3.10 GHA runners and produced intermittent failures. Join the init thread (with a generous timeout) before asserting, so the test exercises the post-warmup state deterministically. * chore(release): prepare v0.11.0-beta (#440) - Bump version from 0.10.2-beta to 0.11.0-beta - Update all version references across codebase - Add What's New section for v0.11.0-beta in README and docs/UPDATE.md - Add v0.11.0-beta entry to website changelog - Update SECURITY.md supported versions to 0.11.x - Update website schema, badge, and package version - Update RELEASE_PROCESS.md version history table * fix(startup): allow launch without pynput backend (#448) * feat(vad): add Silero VAD and drop silence-only buffers (#447) * feat(vad): replace amplitude-based VAD with Silero neural network Replace the simple amplitude threshold VAD with Silero VAD, a lightweight neural network model that classifies audio as speech/non-speech. This addresses the root cause of #302 (silence not stopping recording) and - Add silero_vad.py wrapper using ONNX Runtime with bundled Silero v5 model - Accumulate 512-sample chunks with 64-sample context window per official API - Map existing vad_sensitivity (1-5) to Silero thresholds (0.8-0.3) - Use max() across sub-chunks to avoid missing speech onsets - Fall back to amplitude VAD when onnxruntime is unavailable - Add onnxruntime as optional dependency (pip install vocalinux[vad]) Closes #344 * address review: add tests, LICENSE, logging, validation fixes - Add unit tests for SileroVAD (process, reset, fallback, sensitivity mapping) - Add MIT LICENSE for bundled Silero VAD model in data/ - Log which VAD backend is active on init - Validate chunk length in SileroVAD.process() - Skip is_speech check until a full Silero chunk is processed - Add thread safety note to SileroVAD docstring - Document resampling rate effect on VAD resolution - Comment explaining why load_silero_vad() catches broad Exception * address review: bundle license, document [vad] extra, surface backend in UI - Pack data/LICENSE into the wheel (pyproject package-data) so attribution travels with the model - README: short note on enabling neural VAD via vocalinux[vad] - Settings: VAD Sensitivity row subtitle shows the active backend; if onnxruntime is missing, an info-row beneath the group spells out the install command and the restart requirement - silero_vad.is_silero_available() probes onnxruntime + model presence w/o instantiating an InferenceSession - CI installs .[dev,vad] so Silero paths get exercised - Integration test for _record_audio Silero branch (mocked pyaudio + scripted SileroVAD), covering silence flush, speech reset, and sensitivity threshold mapping at sens=1 vs sens=5 - Tests no longer skip when other modules globally mock numpy: pop the top-level numpy ref so a fresh import gives the real package, and swap sys.modules["numpy"] inside the _record_audio call so the lazy import inside the function picks up real numpy too * fix(vad): drop silence-only recording buffers * test(vad): align stop buffer expectations * test(vad): preserve real numpy module * fix(install): install vad support opportunistically * test(vad): cover recognition manager vad edges --------- Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com> * fix(web): clarify speech demo browser support (#449) * fix black format check --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: jay <sevenjay@users.noreply.github.com> Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> Co-authored-by: Fakhareddine <fe.sabbari@claranet.com> Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parko636 <alex@alexparkinson.net> Co-authored-by: juanfradb <94815906+juanfradb@users.noreply.github.com> Co-authored-by: Pep Locanto <peplocanto@gmail.com> |
||
|
|
3f5a5effff |
feat(vad): add Silero VAD and drop silence-only buffers (#447)
* feat(vad): replace amplitude-based VAD with Silero neural network Replace the simple amplitude threshold VAD with Silero VAD, a lightweight neural network model that classifies audio as speech/non-speech. This addresses the root cause of #302 (silence not stopping recording) and - Add silero_vad.py wrapper using ONNX Runtime with bundled Silero v5 model - Accumulate 512-sample chunks with 64-sample context window per official API - Map existing vad_sensitivity (1-5) to Silero thresholds (0.8-0.3) - Use max() across sub-chunks to avoid missing speech onsets - Fall back to amplitude VAD when onnxruntime is unavailable - Add onnxruntime as optional dependency (pip install vocalinux[vad]) Closes #344 * address review: add tests, LICENSE, logging, validation fixes - Add unit tests for SileroVAD (process, reset, fallback, sensitivity mapping) - Add MIT LICENSE for bundled Silero VAD model in data/ - Log which VAD backend is active on init - Validate chunk length in SileroVAD.process() - Skip is_speech check until a full Silero chunk is processed - Add thread safety note to SileroVAD docstring - Document resampling rate effect on VAD resolution - Comment explaining why load_silero_vad() catches broad Exception * address review: bundle license, document [vad] extra, surface backend in UI - Pack data/LICENSE into the wheel (pyproject package-data) so attribution travels with the model - README: short note on enabling neural VAD via vocalinux[vad] - Settings: VAD Sensitivity row subtitle shows the active backend; if onnxruntime is missing, an info-row beneath the group spells out the install command and the restart requirement - silero_vad.is_silero_available() probes onnxruntime + model presence w/o instantiating an InferenceSession - CI installs .[dev,vad] so Silero paths get exercised - Integration test for _record_audio Silero branch (mocked pyaudio + scripted SileroVAD), covering silence flush, speech reset, and sensitivity threshold mapping at sens=1 vs sens=5 - Tests no longer skip when other modules globally mock numpy: pop the top-level numpy ref so a fresh import gives the real package, and swap sys.modules["numpy"] inside the _record_audio call so the lazy import inside the function picks up real numpy too * fix(vad): drop silence-only recording buffers * test(vad): align stop buffer expectations * test(vad): preserve real numpy module * fix(install): install vad support opportunistically * test(vad): cover recognition manager vad edges --------- Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com> |
||
|
|
6dff5b2296 | fix(startup): allow launch without pynput backend (#448) | ||
|
|
c65cc14a47 |
fix(whispercpp): reduce CPU threads and ensure GPU backend builds in dev mode (#439)
* fix(whispercpp): reduce CPU threads and ensure GPU backend builds in dev mode
- Reduce default n_threads from cpu_count() (16) to max 8 (CPU) / max 4 (GPU)
- Add runtime pywhispercpp GPU capability detection (_detect_pywhispercpp_gpu_backend)
- Make n_threads configurable via whispercpp_n_threads config option
- Fix install.sh regression: GPU source build was skipped in --dev mode
- Extract GPU install logic into install_whispercpp_with_gpu_support()
- Add is_pywhispercpp_gpu_capable() to detect GPU libs in installed pywhispercpp
- Force rebuild pywhispercpp when GPU is detected but lacks GPU support
Fixes CPU spiking to 100% on GPU systems due to excessive thread count
and ensures dev installs get GPU acceleration.
* fix(audio): skip output-only HDMI devices when selecting input device
Add _resolve_valid_input_device() that filters devices by
maxInputChannels > 0, falling back preferred → default → first valid.
Previously, HDMI output devices (0 input channels) could be selected
as the default audio input, causing 'Invalid number of channels' errors.
Now both _record_audio() and _attempt_audio_reconnection() validate the
device has input channels before probing or opening the stream.
* fix(whispercpp): treat n_threads=0 as auto-detect, not literal zero
Config default whispercpp_n_threads=0 was being passed directly
to whisper.cpp as n_threads=0, causing 'cannot create std::vector
larger than max_size()' transcription errors. Now 0 and negative
values are treated as auto-detect, falling through to the smart
thread count logic (max 4 with GPU, max 8 on CPU).
* test: add whispercpp_n_threads to mock assertions and config defaults
Update test_main.py mock to include the new whispercpp_n_threads=0 kwarg
and test_config_manager.py to assert the default config value.
* fix(audio): harden device enumeration against test mocks
The new HDMI-filter helper and GPU-backend detector crashed unit tests
that pass MagicMock() for the PyAudio/pathlib surfaces:
- _find_pywhispercpp_shared_library_dirs hit TypeError when tests
monkey-patch os.stat (Path.is_dir internally calls S_ISDIR on a
non-int st_mode). Widen the catch to OSError/TypeError/ValueError so
unconfigured stat results just skip the candidate dir.
- _resolve_valid_input_device crashed with 'MagicMock > int' on
audio.get_device_count() / info.get('maxInputChannels'). Coerce the
count, treat zero/unknown as 'use system default', and include
non-dict info objects rather than filtering everything out.
No production behavior changes: real PyAudio always returns int counts
and dict device info, so the HDMI filter still works as designed.
* test(recognition): cover new HDMI filter and GPU backend detector
- TestResolveValidInputDevice: 9 cases for the HDMI-filter resolver
(preferred passthrough, HDMI fallback, no-default fallback, no-input
None return, and three mock-tolerance escape hatches).
- TestDetectPywhispercppGpuBackend: 4 cases for runtime GPU library
detection (cpu/vulkan/cuda/empty dirs).
- TestFindPywhispercppSharedLibraryDirsResilience: proves the widened
exception catch survives a TypeError from pathlib.Path.is_dir.
* test(text-injection): wait for IBus init thread to avoid race on slow runners
The IBus environment promotion runs in a background daemon thread spawned
by TextInjector.__init__. The two TestIBusRuntimeFallback cases asserted
on injector.environment immediately after construction, which raced the
thread on Python 3.10 GHA runners and produced intermittent failures.
Join the init thread (with a generous timeout) before asserting, so the
test exercises the post-warmup state deterministically.
|
||
|
|
a47747d6a4 |
fix(install): correct openSUSE Tumbleweed dependencies (#420)
* fix(install): correct openSUSE Tumbleweed dependencies * style: format openSUSE installer changes --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |