f417c28efa93245761ca3c3229e6421c9e0ab4d5
134
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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) | ||
|
|
b3c73dd872 |
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 |
||
|
|
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> |
||
|
|
c39f9ce431 |
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. |
||
|
|
a267f500dd | fix: play stop sound immediately on release (#426) | ||
|
|
722fc23403 | fix(installer): repair pywhispercpp library loading (#433) | ||
|
|
0556d169b3 |
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 |
||
|
|
7c9ef372de |
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. |
||
|
|
9939ce6b63 |
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> |
||
|
|
653f0e3d90 |
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) |
||
|
|
6eba384da7 |
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. |
||
|
|
3e4089a45c |
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> |
||
|
|
d04947374f |
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> |
||
|
|
166bb8c49a |
chore(release): prepare v0.10.2-beta (#394)
- Bump version from 0.10.1-beta to 0.10.2-beta in version.py - Update README.md What's New section with v0.10.2 highlights and bug fixes - Update SECURITY.md supported versions table - Update docs/UPDATE.md with v0.10.2 release notes - Update docs/RELEASE_PROCESS.md version history table - Update web/src/app/page.tsx softwareVersion and version badge - Update web/src/app/changelog/page.tsx with v0.10.2 release entry - Update web/package.json and web/package-lock.json to 0.10.2 Key changes in this release: - Non-ASCII text injection via ydotool clipboard fallback (#376) - IBus detection on Wayland without legacy env vars (#381) - IBus engine process startup fix (#361) - Missing deps for Pop!_OS and Ubuntu 24.04+ (#379) - Systematic code quality refactor (#377) - Redesigned website OG image (#392) |
||
|
|
bd96772307 |
fix(text-injection): handle non-ASCII characters with ydotool via clipboard paste (fixes #362) (#376)
* fix(text-injection): handle non-ASCII characters with ydotool via clipboard paste (fixes #362) ydotool simulates evdev key events which only map to US ASCII keycodes. Non-ASCII characters (accented letters like á, é, ú, ñ, ü, etc.) get silently dropped during injection, so 'Esdrújula' becomes 'Esdrjula'. When ydotool is the active Wayland injection tool and the text contains non-ASCII characters, the new code: 1. Copies text to clipboard via wl-copy (or xclip/xsel fallback) 2. Simulates Ctrl+V via ydotool key to paste it 3. Falls back to regular ydotool type if clipboard tools unavailable This preserves all Unicode characters perfectly. ASCII-only text still uses ydotool type directly for lower latency. * style: fix black formatting in text_injector.py * test: add coverage for clipboard paste paths (xclip/xsel/wtype fallbacks) Covers the uncovered paths flagged by Codecov: - xclip fallback when wl-copy unavailable - xsel fallback when wl-copy/xclip unavailable - wtype Ctrl+V paste simulation - Paste failure (Ctrl+V fails after clipboard copy) - No clipboard tools available * refactor(text-injection): eliminate duplication and dead code in clipboard paste path - _inject_via_clipboard_paste now delegates to _copy_to_clipboard instead of reimplementing wl-copy/xclip/xsel selection (removes ~30 lines of duplication identified in code review) - Remove unreachable wtype branch from _inject_via_clipboard_paste; wtype handles Unicode natively via Wayland virtual-keyboard protocol so it never calls this method - Drop unnecessary time.sleep(0.05) after clipboard write; subprocess.run for wl-copy is synchronous so the sleep was a no-op - Add debug log noting the clipboard will be temporarily overwritten so operators can observe the side-effect without diving into the code test improvements: - Fix fragile string-repr assertion in test_ydotool_ascii_uses_type_directly; now compares actual list slices and verifies wl-copy is NOT called - Replace test_clipboard_paste_with_wtype with test_wtype_injects_unicode_directly which actively asserts wtype calls the tool directly without clipboard - Add test_inject_text_ydotool_non_ascii_end_to_end covering the full inject_text -> _inject_with_wayland_tool -> _inject_via_clipboard_paste stack for accented text on Wayland+ydotool (fixes #362) - Remove now-unnecessary @patch('time.sleep') decorators from three tests |
||
|
|
410f6b27d8 |
fix(tray): clarify missing GNOME AppIndicator support on Debian (#385)
* fix(tray): detect missing AppIndicator watcher on GNOME * test(tray): cover AppIndicator watcher and failure branches |
||
|
|
c4f5b797ce |
fix(ibus): start engine process before checking registration (fixes #360) (#361)
* fix(ibus): start engine process before checking registration _setup_engine() gated on is_engine_registered() which queries `ibus list-engine`. On systems where the IBus daemon doesn't scan user-level component directories (~/.local/share/ibus/component/), the engine never appeared in the list, blocking start_engine_process() from ever being called — even though its register_component() D-Bus call is the correct registration path for standalone-launched engines. Reorder _setup_engine() to start the engine process first, relying on register_component() for runtime registration. Remove the XML install step from the setup path since install_ibus_component() triggers `ibus restart` which kills the engine process. Follow-up to PR #304 which added register_component() support. * fix(ibus): remove dead code and add engine readiness check Address review feedback: - Remove install_ibus_component(), is_engine_registered(), is_component_up_to_date(), _get_expected_component_xml() and their tests — no longer called after setup order fix - Add SOCKET_PATH poll after start_engine_process() to verify engine is fully initialized before activation - Rebase onto current main |
||
|
|
93032471c3 |
fix(ibus): detect IBus on Wayland without env vars and fix text injection (#381)
Fix two related IBus issues on KDE Plasma Wayland (and similar DEs): 1. IBus not detected without legacy environment variables: On Wayland, IBus is configured via the DE's Virtual Keyboard setting and IBus itself recommends unsetting GTK_IM_MODULE / QT_IM_MODULE. The detection logic only checked these env vars, so it never found IBus on a properly configured Wayland session. Fix: When no IM env vars are set, check if ibus-daemon is running and has an active engine. If another IM (e.g. Fcitx) is explicitly configured via env vars, respect that and don't override. 2. IBus text injection silently fails on Wayland: After switching to the Vocalinux IBus engine, the code called setxkbmap to capture/restore the keyboard layout. On Wayland, setxkbmap does not reflect the compositor's state and returns incorrect results (e.g. 'us' when the user has 'de'). Calling setxkbmap to restore this wrong layout interfered with IBus's ability to commit text, causing silent injection failures. Fix: Skip setxkbmap-based XKB layout management entirely on Wayland sessions. The Wayland compositor manages keyboard layouts natively, and setxkbmap can only cause harm there. Fixes #380 |
||
|
|
be0614010d |
refactor: systematic code quality improvements across 20 dimensions (#377)
* refactor: improve code quality across multiple dimensions Code quality improvements identified and driven by desloppify analysis: **action_handler.py** - Replace 9 repetitive shortcut forwarding methods with data-driven _SHORTCUT_ACTIONS class-level dispatch table - Add _make_shortcut_handler() factory for shortcut closures - Add explicit type annotation for action_handlers dict **config_manager.py** - Move 'import copy' from __init__ to module level (stdlib best practice) - Add _VALID_SECTIONS frozenset for section name validation in set() - Narrow broad Exception catch to (KeyError, TypeError) in set() - Add deprecation warning to save_settings() guiding toward save_config() **tray_indicator.py** - Consolidate all imports at module top level - Remove noqa E402 suppressions for mid-file imports - Move ResourceManager import alongside other local imports **recognition_manager.py** - Narrow broad 'except Exception' to 'except OSError' for audio device enumeration errors - Narrow 'except Exception' to 'except (FileNotFoundError, OSError)' for notification subprocess errors **Tests** - Update test assertions to use specific exception types matching narrowed catches (OSError, FileNotFoundError instead of Exception/RuntimeError) * refactor: improve type safety, callback contracts, and method decomposition **main.py** — Mid-level elegance - Document callback wiring contract: text_callback, action_callback, state_callback with threading context, argument types, and behavior - Add return type annotations to nested callback functions - Reorder: define callbacks before registration for readability **config_manager.py** — Type safety, error consistency - Add typed accessors: get_str(), get_bool(), get_int(), get_float(), get_optional_int() eliminating Any returns for common access patterns - Narrow load_config exception to (json.JSONDecodeError, OSError) - Narrow save_config exception to (OSError, TypeError) **recognition_manager.py** — Low-level elegance - Decompose _init_whispercpp (141 lines) into three focused methods: - _init_whispercpp: validation and download gating (35 lines) - _load_whispercpp_model: backend detection and model loading (55 lines) - _handle_gpu_fallback: GPU incompatibility recovery (40 lines) - Narrow exception catch from broad Exception to specific types **tray_indicator.py, settings_dialog.py** — Type safety - Migrate config access from untyped get() to typed get_str(), get_bool(), get_optional_int() across 8 call sites * refactor: narrow 11 more broad Exception catches to specific types Systematically replaced broad 'except Exception' with specific exception types across recognition_manager.py: - test_audio_input: (OSError, ValueError) for audio read errors - test_audio_input outer: (OSError, ValueError, RuntimeError) for unexpected - _init_whisper: (RuntimeError, OSError) for engine init failures - _transcribe_with_whisper: (RuntimeError, OSError, ValueError) for transcription - _download_whispercpp_model: (OSError, RuntimeError, ValueError) for downloads - _download_vosk_model: (OSError, RuntimeError, ValueError) for downloads - _download_whisper_model: (OSError, RuntimeError, ValueError) for downloads This brings the total narrowed exception catches to 18+ across the codebase, improving error consistency and making failure modes more predictable. * style: fix black formatting issues Auto-format with black to fix CI lint failures in recognition_manager.py, settings_dialog.py, and test_settings_shortcuts.py. * test: add coverage for typed config accessors Add TestTypedAccessors class with 15 tests covering: - get_str(): string values, defaults, non-string conversion, None handling - get_bool(): boolean values, defaults - get_int(): integer values, invalid input fallback, missing key defaults - get_float(): float values, invalid input fallback - get_optional_int(): int values, None values, missing keys, invalid input Fixes codecov/patch coverage requirement for new typed accessor methods. |
||
|
|
c1f12db845 |
chore(release): prepare v0.10.1-beta (#358)
* chore(release): prepare v0.10.1-beta * chore(release): update release notes with new fixes from main |
||
|
|
aa2dbc2fee |
fix(suspend): delay keyboard restart for USB re-enumeration (#372)
* fix(suspend): delay keyboard restart to 6s for USB re-enumeration The 2s delay is enough for the speech engine, but USB devices re-enumerate slowly after resume. At T+2s, stale /dev/input nodes still exist and evdev opens them, but the kernel tears them down at ~T+4s, causing another round of disconnections. Split the reinit into two separate timers: - 2s: speech engine reinit (unchanged) - 6s: keyboard backend restart (after USB re-enumeration completes) * fix(suspend): use inotify to detect device settle before keyboard restart Replace hard-coded 6s keyboard restart delay with event-driven Gio.File.monitor_directory() on /dev/input. After resume, a 2s settle timer resets on each inotify event; keyboard shortcuts restart only once devices stop changing. A 10s safety cap and a 6s fallback timer guard against environments where inotify is unavailable. |
||
|
|
5909def48d |
fix(suspend): restart keyboard shortcut backend after resume (#371)
After suspend/resume, evdev devices disconnect and the monitor thread exits when all file descriptors close. The resume handler only reinitialized the speech engine, leaving keyboard shortcuts dead. Restart _setup_keyboard_shortcuts() in _reinit_after_resume() to re-scan /dev/input and open fresh device file descriptors. |
||
|
|
4d501f1711 |
fix(settings): disable copy-to-clipboard by default (#370)
Copy-to-clipboard is now opt-in. Users can enable it in Settings under Text Injection. This avoids unexpected clipboard overwrites on fresh installs and reduces background wl-copy/xclip timeout warnings on Wayland where clipboard access from daemon threads can be unreliable. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> |
||
|
|
80536c0fa4 |
fix(suspend): auto-recover speech recognition after system resume (#369)
* feat(suspend): add D-Bus based suspend/resume handler Monitors org.freedesktop.login1.Manager.PrepareForSleep via Gio.DBusProxy to detect system suspend/resume. On resume, invokes a callback to reinitialize the audio pipeline and speech model so dictation works immediately without manual intervention. Fixes #367 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(suspend): wire suspend handler into tray indicator and recognition manager TrayIndicator now creates a SuspendHandler on startup that listens for D-Bus PrepareForSleep signals. On suspend, active recognition is stopped. On resume, a 2-second delay allows audio hardware to stabilize before reinitializing the speech engine and resetting audio reconnection state. Fixes #367 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> |
||
|
|
113c846d52 |
fix(ptt): prevent premature transcription during push-to-talk silence (#359)
* fix(ptt): prevent premature transcription during push-to-talk silence In push-to-talk mode, a brief silence while holding the hotkey would trigger transcription mid-hold, injecting text before key release. Add recognition_mode parameter to start_recognition() so the silence timeout in _record_audio() defers enqueuing audio in PTT mode. Audio accumulates in the buffer until key release calls stop_recognition(), which already handles final buffer enqueue. Toggle mode is unchanged. * fix(clipboard): run clipboard copy in background thread to prevent blocking The _copy_to_clipboard calls (wl-copy, xclip, xsel) have 5s timeouts each and run synchronously on the recognition thread. When they hang, they block stop_recognition() from completing, delaying the state transition to IDLE and preventing the next push-to-talk press. Move clipboard copy to a daemon thread on the two success paths so it never blocks transcription or state transitions. The failure-fallback path stays synchronous since it needs the return value. |
||
|
|
9ba63484c2 |
fix(keyboard): preserve XKB layout when activating Vocalinux IBus engine (fixes #292) (#343)
* fix(keyboard): preserve XKB layout when activating Vocalinux IBus engine When Vocalinux starts, it may start or restart the IBus daemon, which can override the user's system-level keyboard layout with IBus's configured layout (often defaulting to US). This fix captures the current XKB layout before activating the Vocalinux IBus engine and restores it after activation, ensuring users with non-US keyboard layouts don't lose their settings. Fixes #292 * fix(keyboard): restore XKB layout after engine activation, not just on shutdown (#292) The draft PR only restored the user's XKB layout when Vocalinux shut down. The French AZERTY reporter confirmed the real problem: switching to the Vocalinux IBus engine overrides the system keyboard layout immediately, so typing 'a' produces 'q' (AZERTY mapped through US QWERTY). The same applies to Spanish and other non-US layouts. Changes: - Restore XKB layout immediately after IBus engine activation in _setup_engine(), not just in stop(). This preserves the user's keyboard layout while Vocalinux is actively running. - Remove the 'layout != us' guard in stop() -- always restore whatever layout was captured, including US with custom options. - Fix 4 broken tests: wrong expected options values, missing TimeoutExpired argument, incorrect call count assertion. - Add test cases for Spanish and French AZERTY layouts. * test(keyboard): add coverage for XKB layout capture/restore in IBusTextInjector Addresses Codecov report on PR #343 - adds tests covering: - stop() restores captured XKB layout (Spanish with variant/options) - stop() skips restore when no layout was captured - _setup_engine() captures XKB layout and restores it after engine activation (French AZERTY) * fix(test): mock start_engine_process in setup_engine XKB test The test was missing a mock for start_engine_process, causing it to fail on CI where the IBus engine PID file path doesn't exist. |
||
|
|
fd33a9a4ae |
fix(settings): add Close button to settings dialog for WM compatibility (fixes #323) (#356)
Some window managers (on both X11 and Wayland) hide the title-bar close button for Gtk.Dialog windows that have no action buttons. The previous fix (PR #333) added set_decorated(True) which was not sufficient. This adds an explicit Close action button to the dialog so users always have a visible way to dismiss it, regardless of WM behavior. The instant-apply pattern is preserved — settings still apply immediately on change. |
||
|
|
5b53e850dd |
fix(engine): stop recognition before switching engines to prevent segfault (fixes #350) (#355)
When switching from whisper.cpp to VOSK via the settings dialog, the _auto_apply_settings path called reconfigure() without first stopping active recognition. This allowed background audio/transcription threads to continue using the old engine's native C resources (whisper.cpp model) while they were being freed by Python's GC, causing a segmentation fault. Changes: - Add stop_recognition() guard in reconfigure() itself when the engine or model changes and recognition is active — this makes the API safe regardless of which caller triggers the reconfiguration - Add defense-in-depth stop_recognition() call in _auto_apply_settings to match the existing pattern in _apply_settings_internal |
||
|
|
860a9f9ae6 |
fix: Bundle resources in package to fix missing system tray icons (fixes #349) (#354)
* fix: bundle resources in package to fix missing icons in system tray (fixes #349) The resources directory (icons and sounds) was not included in pip-installed packages because it lived outside the src/ package directory. This caused the resource_manager to find an empty icon directory at runtime, resulting in the ellipsis (three dots) icon in the GNOME system tray. Changes: - Copy resources into src/vocalinux/resources/ so setuptools bundles them - Add 'vocalinux.resources/**/*' to package-data in pyproject.toml - Add MANIFEST.in for sdist builds - Update resource_manager to check package-bundled resources first (module_dir.parent / 'resources') before checking external paths - Add install_resources_to_venv() to install.sh for curl-based installs, copying resources to /share/vocalinux/resources This fixes both pip installs (resources bundled in the wheel) and curl-based installs (resources copied to venv share directory). * fix: ensure bundled resources are included in sdist and pip installs - Add 'vocalinux.resources' to setuptools packages list so the subpackage (and its __init__.py) is recognized by setuptools - Add __init__.py to src/vocalinux/resources/ to make it a proper Python subpackage for reliable discovery - Add 'recursive-include src/vocalinux/resources *' to MANIFEST.in so sdist archives include the bundled icons and sounds |
||
|
|
84ec79c769 |
chore(release): prepare v0.10.0-beta (#345)
* chore(release): prepare v0.10.0-beta Bump versions and release references across app, docs, installer, and website for the next beta release. Add v0.10.0-beta release notes entries covering reliability fixes and CI/test improvements since v0.9.0-beta. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs(release): align Python baseline references to 3.9+ Synchronize stale Python 3.8 references in agent guidance and distro dependency docs/scripts with the current 3.9+ minimum used by project metadata and release docs. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> |
||
|
|
ca39ba930e |
fix(keyboard): generalize pynput modifier alias matching across layouts (#342)
* fix(keyboard): support AltGr (ISO_Level3_Shift) for Right Alt on int'l keyboards On UK/International keyboards, Right Alt (AltGr) sends ISO_Level3_Shift (keysym 0xfe03) instead of Alt_R (keysym 0xffea). The pynput backend was only checking for keyboard.Key.alt_r, missing the alt_gr key that pynput uses for ISO_Level3_Shift. Changes: - Add keyboard.Key.alt_gr to MODIFIER_NORMALIZE_MAP (normalizes to alt) - Include alt_gr in the "alt" and "right_alt" key variants - Update side-specific matching to check key variants instead of exact key comparison Fixes #322 * fix(keyboard): generalize pynput modifier alias matching * test(keyboard): make variant tests independent of pynput install |
||
|
|
929f890b70 |
fix(audio): probe channels using device-supported sample rates (#341)
Fixes #340 by making channel probing use the device default sample rate first, then fallback rates, so 48kHz-only USB interfaces initialize correctly. |
||
|
|
350cfcf17f |
fix(evdev): handle SYN_DROPPED to prevent stale modifier state (#339)
When the kernel's evdev buffer overflows it sends SYN_DROPPED to signal lost events. Without handling this, a missed key-release can leave key_pressed_devices in a stale state, causing stuck push-to-talk. Discard all events between SYN_DROPPED and the next SYN_REPORT, then reset modifier key state for the affected device. Refs: https://www.kernel.org/doc/html/latest/input/event-codes.html |
||
|
|
9e0a98c0a3 |
fix(tray): use icon names for AppIndicator updates (#330)
* fix(tray): use icon names for AppIndicator updates AppIndicator expects icon names resolved via theme path; passing absolute paths can result in blank or stuck status icons on some desktops. Use icon names for runtime updates and tighten tests to lock behavior. * fix(resources): prefer populated asset directories Selecting the first existing resources path could choose an empty install location and break tray icons and sounds. Score candidates by expected assets so runtime picks a usable resources directory. |
||
|
|
fa17b88f74 |
chore: bump minimum Python from 3.8 to 3.9 (#336)
* chore: bump minimum Python from 3.8 to 3.9 and adopt PEP 585 builtins - Update requires-python, classifiers, mypy target, and black target in pyproject.toml - Replace Python 3.8 with 3.9 in CI matrix and nightly workflow - Replace typing.List/Dict/Tuple/Set with builtin list/dict/tuple/set (PEP 585) - Update all 3.8 version references in README, docs, website, and install.sh - Fix test_wayland_no_tools to mock IBus availability - Remove List from common_types re-exports and update test * style: reformat test files for Black 26.x and pin version in CI Black 26.x introduced parenthesized context manager formatting. Reformat 7 test files and pin Black>=26,<27 in CI to prevent future drift from unpinned version upgrades. |
||
|
|
e478f7e353 |
fix(settings): force window decorations on settings dialog (#333)
Fix missing close button in settings window title bar on Ubuntu 22.04 (GNOME 42). The dialog was created with flags=0 and no transient parent, which causes GTK to not apply decorations on certain window managers. Explicitly call set_decorated(True) to ensure the close button is always visible regardless of WM hints. Fixes #323 |
||
|
|
b6fb2b838b |
fix(text-injection): use register_component for IBus engine activation (#304)
* fix(ibus): add register_component for standalone launch and --xml/--ibus flag handling (#303) * refactor(ibus): consolidate engine metadata and add command_line/request_name checks (#303) |
||
|
|
f24674a4e8 |
docs: improve project description with narrative intro (#325)
* docs: improve project description with narrative intro - Replace the one-liner README intro with a more compelling narrative that highlights key differentiators: offline, open source, multi-engine, GPU-accelerated, and privacy-first - Update version.py __description__ to a more descriptive one-liner * docs: replace all em-dashes with hyphens/commas/colons Remove all em-dash (—) characters from README.md, replacing them with contextually appropriate alternatives (hyphens, commas, colons). |
||
|
|
44af1703c6 |
fix(docs): point all install curl URLs to main/install.sh (#314)
The install.sh on main now dynamically resolves the latest release tag via GitHub API (merged in PR #313), so pinning to a specific release tag in install commands is no longer needed — and was causing users to run the old hardcoded installer. Update all curl install command URLs across docs, code, and website from versioned tags (v0.9.0-beta, v0.8.0-beta) to main/install.sh: - README.md (4 URLs + note text) - docs/INSTALL.md (2 URLs + note text) - docs/UPDATE.md (2 URLs) - docs/DISTRO_COMPATIBILITY.md (1 URL) - tests/distro-testing-checklist.md (1 URL) - src/vocalinux/main.py (1 URL) - web/src/app/page.tsx (remove dynamic release fetch, hardcode main) |
||
|
|
baeee19f17 |
chore(release): prepare v0.9.0-beta (#310)
* chore(release): prepare v0.9.0-beta - Bump version from 0.8.0-beta to 0.9.0-beta - Update README What's New section with new features and bug fixes - Update all install script URLs to v0.9.0-beta - Add v0.9.0-beta What's New section to docs/UPDATE.md - Update SECURITY.md supported versions (0.9.x supported, 0.8.x dropped) - Update website schema softwareVersion and nav badge to 0.9.0 - Update website featureList with new sound effects and clipboard features - Add v0.9.0-beta entry to changelog page - Update web/package.json version to 0.9.0 - Add v0.9.0-beta row to RELEASE_PROCESS.md version history * docs(readme): add contributors avatar grid via contrib.rocks * feat(about): add Changelog link to About dialog * Revert "feat(about): add Changelog link to About dialog" This reverts commit 15e0984f516caccb14a0609e96d5fb6870ea247e. |
||
|
|
e2fdf5ddf2 |
fix(clipboard): copy to clipboard after IBus text injection (#311)
The IBus injection path had an early return that skipped the clipboard copy logic. Store the result, copy to clipboard if enabled, then return. Also add xclip and wl-clipboard to install.sh for all supported distros so clipboard tools are available out of the box. |
||
|
|
18e9e0a966 |
feat(audio): add simple sound effects toggle (#309)
Add a single toggle switch to enable/disable sound effects in the Audio settings. This provides a simpler alternative to full sound customization while covering the most common use case. - Add sound_effects.enabled config option (default: true) - Add toggle switch in Audio settings tab - Add helper methods to ConfigManager - Update play_*_sound() to check enabled state - Add tests for the new functionality Closes #283 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> |
||
|
|
8a4dbad4ee |
fix: improve Debian/pipx installation experience (#289)
* fix: improve Debian/pipx installation experience - Enhanced error messages in check_dependencies() to provide distro-specific installation commands (Ubuntu/Debian, Fedora, Arch Linux) - Added explicit pipx guidance: install system packages BEFORE pipx install - Updated error message to use gir1.2-ayatanaappindicator3-0.1 for Debian - Added comprehensive pipx installation section to DISTRO_COMPATIBILITY.md with step-by-step instructions for all major distributions * fix: add display availability check to prevent GTK crash on Debian The original PR only added documentation and improved error messages. This commit adds the actual fix for issue #288. Root cause: On some systems (especially Debian), GTK widgets can be created before the display connection is properly established, causing the crash: gtk_widget_get_scale_factor: assertion 'GTK_IS_WIDGET (widget)' failed Fix: Add check_display_available() function that verifies Gdk.Display is available before any GTK widgets are created. This provides a clear error message instead of a cryptic GTK crash. Fixes #288 |
||
|
|
73082fa393 |
fix: add clipboard fallback for Wayland compositors without virtual keyboard support (#299)
* fix: add clipboard fallback for Wayland compositors without virtual keyboard support Fixes #297 On KDE Plasma Wayland, the compositor doesn't support the virtual keyboard protocol that wtype requires. When both wtype and xdotool fallback fail, text injection was completely broken. Changes: - Add _fallback_to_clipboard() method that tries wl-copy, xclip, and xsel - When all injection methods fail, copy text to clipboard so user can paste - Play success sound when clipboard fallback succeeds (text is recoverable) - Improve error messages with KDE-specific guidance and ydotool instructions For KDE Plasma Wayland users, the recommended solution is: 1. Install ydotool: sudo apt install ydotool 2. Enable daemon: sudo systemctl enable --now ydotoold Alternatively, clipboard fallback will copy text for manual pasting. * style: fix black formatting * fix: wrap clipboard fallback in try/except to handle test mocks * feat(clipboard): add configurable always-copy-to-clipboard feature Enhanced the clipboard functionality beyond just a fallback mechanism: - Renamed _fallback_to_clipboard to _copy_to_clipboard (more general) - Added _should_copy_to_clipboard() to read user preference from config - After successful text injection, copies to clipboard if setting enabled - Still falls back to clipboard on injection failure regardless of setting - Added 'copy_to_clipboard' setting to config (default: true) - Added toggle switch in Settings > General for user control This provides better UX by: 1. Allowing users to reference pasted text later via clipboard history 2. Providing a backup when injection partially works 3. Enabling easy pasting into apps where injection might not work Fixes #297 * feat(clipboard): show notification when text copied as fallback When text injection fails but clipboard succeeds, show a desktop notification telling the user to paste with Ctrl+V. This ensures users know their text was saved and can be recovered. |
||
|
|
e35bdb2f62 |
fix(dictation): remove unwanted leading space when starting new transcription (#307)
Reset last_injected_text when recognition transitions to LISTENING state, ensuring the first dictation segment in each session is not prefixed with a space. Also simplify the space-prepending logic: always add a separator space between consecutive segments (not just after punctuation), and strip incoming text as a safety net against whisper tokenizer artifacts. |
||
|
|
80108c3456 |
feat(shortcuts): add left/right modifier key distinction for keyboard shortcuts (#308)
Allow users to bind voice commands to specific modifier key sides (e.g., Right Ctrl only), preventing conflicts with common left-side shortcuts like Alt+Tab or Ctrl+C. - Expand SUPPORTED_SHORTCUTS from 3 to 9 entries (either/left/right) - Add SHORTCUT_GROUPS for grouped display in settings UI - Implement side-aware key matching in pynput and evdev backends - Update settings dialog with grouped shortcut selector - Add comprehensive tests for side-specific shortcuts Closes #306 |