Commit Graph
89 Commits
Author SHA1 Message Date
Jatin K MalikandGitHub 9c5065f452 fix(input): clarify KDE Wayland IBus setup (#466) 2026-06-10 16:08:11 -07:00
Jatin K MalikandGitHub a73eec4a0a fix(dictation): preserve spacing between silence segments (#464) 2026-06-10 15:44:27 -07:00
Jatin K MalikandGitHub 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.
2026-06-06 20:41:15 -07:00
Jatin K MalikandGitHub 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
2026-06-06 19:57:25 -07:00
Jatin K MalikandGitHub 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.
2026-06-06 19:18:57 -07:00
Jatin K MalikandGitHub 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
2026-06-06 17:53:54 -07:00
Jatin K MalikandGitHub 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).
2026-06-06 16:26:28 -07:00
+1
sevenjayGitHubSbenazarjayJatin K MalikFakhareddineSisyphusdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>parko636juanfradbPep Locanto
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>
2026-06-06 15:52:46 -07:00
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>
2026-06-02 12:43:51 -07:00
Jatin K MalikandGitHub 6dff5b2296 fix(startup): allow launch without pynput backend (#448) 2026-06-02 12:06:20 -07:00
Jatin K MalikandGitHub 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.
2026-05-30 16:44:53 -07:00
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>
2026-05-30 14:48:36 -07:00
Jatin K MalikandGitHub 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.
2026-05-30 14:41:08 -07:00
juanfradbandGitHub a267f500dd fix: play stop sound immediately on release (#426) 2026-05-30 14:37:53 -07:00
Jatin K MalikandGitHub 722fc23403 fix(installer): repair pywhispercpp library loading (#433) 2026-05-21 21:37:04 -07:00
Jatin K MalikandGitHub 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
2026-05-06 22:40:01 -07:00
Jatin K MalikandGitHub e52c96fc38 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
2026-05-02 21:13:21 -07:00
Jatin K MalikandGitHub 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.
2026-05-02 20:07:13 -07:00
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>
2026-05-02 19:33:31 -07:00
Jatin K MalikandGitHub 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)
2026-05-02 19:11:47 -07:00
Jatin K MalikandGitHub 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.
2026-05-02 18:37:07 -07:00
jatinkrmalikandSbenazar 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>
2026-05-02 17:33:42 -07:00
jatinkrmalikandFakhareddine 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>
2026-05-02 17:33:42 -07:00
Jatin K MalikandGitHub 0ded1e3c93 test: mock Notify module in conftest to prevent real notifications (#390)
- Add Notify mock to global gi.repository mock in conftest.py
- This prevents real desktop notifications during test runs
- Clean up unnecessary comments in test_main_args_deps.py
2026-04-05 21:08:21 -07:00
Jatin K MalikandGitHub 9b5b1a3cef test(tray): cover degraded-startup and guard branches (#386) 2026-04-05 18:23:37 -07:00
Jatin K MalikandGitHub 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
2026-04-05 18:12:35 -07:00
Jatin K MalikandGitHub 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
2026-04-05 18:12:16 -07:00
Jatin K MalikandGitHub 45a69d5946 test(ibus): cover socket-readiness timeout warning branch (#384)
Add a test for the for/else warning path in _setup_engine() introduced
in PR #361: when all 15 socket-readiness poll attempts exhaust without
SOCKET_PATH appearing, a WARNING is logged and activation is attempted
anyway. The new test mocks time.sleep to avoid real delays and asserts
the exact retry count (15) and log message.

Fixes Codecov patch-coverage gap reported on #361.
2026-04-05 17:37:47 -07:00
SbenazarandGitHub 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
2026-04-05 17:31:35 -07:00
Jatin K MalikandGitHub 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
2026-04-03 15:47:55 -07:00
Jatin K MalikandGitHub 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.
2026-04-02 14:53:29 -07:00
Jatin K MalikandGitHub 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.
2026-03-30 00:08:31 -07:00
Jatin K MalikandGitHub 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.
2026-03-29 23:21:55 -07:00
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>
2026-03-29 22:41:21 -07:00
Jatin K MalikandGitHub 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.
2026-03-28 00:53:45 -07:00
Jatin K MalikandGitHub 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.
2026-03-27 14:31:00 -07:00
Jatin K MalikandGitHub 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.
2026-03-27 11:29:18 -07:00
Jatin K MalikandGitHub 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
2026-03-25 22:54:10 -07:00
Jatin K MalikandGitHub 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.
2026-03-25 22:16:58 -07:00
Jatin K MalikandGitHub 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.
2026-03-21 12:18:21 -07:00
Jatin K MalikandGitHub 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.
2026-03-21 12:15:10 -07:00
Jatin K MalikandGitHub 99699f18eb test(ibus): cover launch-mode and main entrypoint paths (#329) 2026-03-17 18:19:02 -07:00
Jatin K MalikandGitHub fa4da40e7b test(coverage): improve test coverage from 57% to 80%+ (#327)
* test(coverage): improve test coverage from 57% to 80%+

Add 30 new test files with 861 additional test cases to improve
overall test coverage from 57% to 80%+ (statement coverage).

Key modules covered:
- recognition_manager.py: 43% → 69% (audio device detection, transcription,
  model download, engine initialization, buffer management)
- text_injector.py: improved coverage for X11/Wayland text injection,
  environment detection, clipboard fallback, shortcut injection
- ibus_engine.py: 22% → 73% (engine lifecycle, text injection, socket server)
- evdev_backend.py: 11% → 78% (device detection, key event handling)
- pynput_backend.py: 43% → 76% (start/stop, key handling, callbacks)
- keyboard_backends/__init__.py: 54% → improved (backend creation, env detection)
- whispercpp_model_info.py: 32% → improved (GPU detection, model recommendations)
- about_dialog.py, first_run_dialog.py, main.py, single_instance.py,
  common_types.py, config_manager.py, autostart_manager.py: various improvements

All 1433 tests pass with proper test isolation to prevent mock pollution
between test modules.

* test(quality): audit and fix test quality issues

Code review audit identified and fixed the following issues:

- Remove bare try/except blocks that swallow all errors (7 tests fixed
  in recognition_manager_deep, replaced with proper assertions or deleted)
- Delete source code inspection tests that read .py files as strings
  instead of testing behavior (deleted test_about_dialog_coverage.py and
  test_first_run_dialog_coverage.py entirely - ~35 useless tests removed)
- Add meaningful assertions replacing weak assertIsNotNone/assertIsInstance
  checks (common_types_coverage, ibus_engine_coverage, keyboard_backends_extra)
- Fix tests that mock the system under test instead of its dependencies
  (text_injector_extra now mocks subprocess.run instead of injection methods)
- Add missing assertions to tests with no verification
  (ibus_engine focus_out, log window info, copy to clipboard)
- Verify download tests check requests.get was called with correct URLs
  (coverage_boost download tests)
- Fix pytest warning by renaming test_audio_input import to avoid
  pytest collecting it as a test function

All 1356 tests pass with 0 failures and 0 warnings.
Statement coverage: 80.3% (3257/4054 lines covered).

* style(tests): fix black formatting and isort import ordering

Auto-format all new test files with black and isort to pass CI lint checks.

* fix(tests): resolve CI hangs and rename test files for clarity

- Fix CI hangs by properly mocking gi/GTK/IBus in all test files
- Fix sys.modules pollution in recognition manager tests by using tmp_path
- Add pytest-timeout to prevent future hangs
- Rename 28 test files to use descriptive, consistent names mapping to source modules

* fix(tests): resolve Python 3.13 CI failures

- Fix _load_stdlib_module() in test_recognition_manager_downloads.py to
  handle stdlib modules that are packages (directories with __init__.py)
  rather than single .py files. In Python 3.13, zipfile became a package,
  causing FileNotFoundError during test collection (exit code 2).

- Fix invalid escape sequence '$' in test_cross_distro_compatibility.py
  by using a raw string. Python 3.13 reports this as SyntaxWarning and
  Python 3.14 will make it a SyntaxError.

* fix(tests): use direct import for zipfile instead of _load_stdlib_module

The _load_stdlib_module approach fails on Python 3.13 where zipfile is a
package (zipfile/__init__.py). Loading it via spec_from_file_location with
a custom name breaks relative imports inside the package (e.g.
'from ._path import ...' resolves to '_stdlib_zipfile._path' which doesn't
exist).

Simply import zipfile at module level before any test mocks can pollute
sys.modules — this is safe because the module-level code runs during test
collection, before any test fixtures or patches are applied.

* fix(tests): mock gi/GTK in conftest to prevent CI hangs

On CI, real PyGObject is installed and module-level imports in vocalinux
source files (ibus_engine, tray_indicator, settings_dialog, etc.) trigger
GTK initialization or IBus daemon connections that hang indefinitely in
headless environments. pytest-timeout's signal-based timeout cannot
interrupt these C-level blocking calls.

Fix by mocking gi and gi.repository in conftest.py (which runs before any
test module is collected), ensuring all test files are protected regardless
of whether they remember to mock individually.

Also add timeout-minutes: 10 to the CI test job as a safety net so that
any remaining hangs fail fast instead of running indefinitely.

* fix(tests): unconditionally mock gi in conftest for CI

The previous 'if gi not in sys.modules' guard was ineffective on CI
because PyGObject is installed via pip, so the real gi module is already
loaded. This caused GTK initialization and IBus daemon connections that
hang indefinitely in headless CI environments.

Now unconditionally replace gi and gi.repository with mocks during test
collection, regardless of whether the real package is installed.

* fix(tests): reduce timeout to 10s and use thread-based timeout

Use timeout_method=thread instead of signal (default) so that
pytest-timeout can interrupt C-level blocking calls (GTK operations,
socket accepts, subprocess waits) that the signal-based method cannot
handle. Also reduce per-test timeout from 30s to 10s since no legitimate
unit test should take longer, and hanging tests were consuming the entire
CI job timeout budget.

* fix(tests): set global socket timeout to prevent blocking on CI

Tests in test_ibus_engine.py create real Unix sockets with daemon threads
that block on socket.accept(). When tests fail or timeout, these threads
remain blocked indefinitely, preventing pytest from completing. The
thread-based timeout catches the hang but cannot terminate the blocking
accept() call.

Set socket.setdefaulttimeout(5) in conftest.py so all socket operations
(accept, connect, recv) will raise socket.timeout after 5 seconds instead
of blocking forever.

* fix(tests): add cleanup fixture for IBus socket server threads

Add autouse fixture that stops any leftover VocalinuxEngine socket server
threads after each test. Tests that trigger _start_socket_server() spawn
daemon threads blocking on socket.accept() that persist after the test
completes, eventually causing pytest-timeout to kill the process.

The cleanup fixture sets _server_running=False and closes the server
socket, which unblocks the accept() call and allows the daemon thread
to terminate.

* fix(tests): reduce socket timeout to 0.1s to avoid slowing tests

The 5-second socket timeout caused socket operations in daemon threads
to block for 5 seconds per iteration before timing out, significantly
slowing the test suite on CI. Reduce to 0.1s so blocking socket calls
fail fast without noticeably impacting test execution time.

* fix(tests): use 2s socket timeout and increase CI timeout to 15min

The 0.1s socket timeout was too aggressive and caused legitimate test
socket operations to fail. Use 2s which is long enough for test sockets
to work but short enough to unblock daemon threads.

Increase CI job timeout to 15 minutes to accommodate the socket timeout
overhead from daemon threads that retry every 2 seconds.

* fix(tests): block known-blocking daemon threads to prevent CI hangs

Instead of using socket timeouts (which either break tests or are too
slow), prevent specific known-blocking daemon threads from starting:
- ibus_engine.server_thread: blocks on socket.accept()
- evdev_backend._monitor_devices: blocks on select.select()

These threads are identified by their target function name and skipped
during Thread.start(). The thread is marked as started/stopped so
join() calls in cleanup code don't raise RuntimeError.

Test-created daemon threads (e.g. mock socket servers) are unaffected
since they use different target function names.

* fix(tests): patch Thread.join for Python 3.13 compatibility

Python 3.13 changed threading internals — join() now uses _handle.join()
which raises 'thread not started' for threads that were never actually
started. Instead of trying to fake internal state, patch join() to be a
no-op for threads that were skipped by our _safe_thread_start guard.

This fixes the 2 remaining test_evdev_backend_ext failures on Python 3.13.
2026-03-17 14:05:16 -07:00
Jatin K MalikandGitHub 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)
2026-03-14 14:16:38 -07:00
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>
2026-03-14 13:21:09 -07:00
Jatin K MalikandGitHub 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.
2026-03-14 12:40:58 -07:00
Jatin K MalikandGitHub 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
2026-03-14 12:37:51 -07:00
Jatin K MalikandGitHub 0469defae6 chore(release): prepare v0.8.0-beta (#280)
* chore(release): prepare v0.8.0-beta

* docs(web): align release messaging with v0.8.0-beta features

* docs(web): tighten homepage messaging for v0.8.0-beta
2026-02-28 20:58:55 -08:00
Jatin K MalikandGitHub 86ca22ff5c feat(shortcuts): add push-to-talk mode and improve shortcut reliability (#278)
* feat(shortcuts): add push-to-talk mode as alternative to toggle

This commit implements push-to-talk functionality (Issue #252) as an
alternative to the existing double-tap toggle mode:

- Add keyboard shortcut mode setting (toggle vs push_to_talk)
- In push-to-talk mode: hold key to speak, release to stop
- Update both pynput and evdev backends to support press/release callbacks
- Add UI controls in settings dialog for mode selection
- Maintain backward compatibility with existing toggle mode as default

Closes #252
Related: Closes #185 (superseded by push-to-talk approach)

* fix(shortcuts): remove duplicate code blocks that broke settings dialog

The previous push-to-talk implementation had duplicate code blocks
in tray_indicator.py and settings_dialog.py that caused the settings
dialog to fail silently. This commit cleans up the duplicate code
while preserving the push-to-talk functionality.

Fixes issue where settings dialog would not open.

* fix(ui): add set_subtitle method to PreferenceRow class

The PreferenceRow class was missing a set_subtitle method, which is called by
_update_shortcut_ui_for_mode when switching between toggle and push-to-talk
modes. This caused an AttributeError that silently crashed the settings dialog
during initialization.

Also stores subtitle_label as instance variable for later updates.

* fix(keyboard): remove duplicate code and fix mode handling

Critical fixes for keyboard backends:
- evdev_backend.py: Removed duplicate __init__ and _handle_key_event methods
- pynput_backend.py: Removed duplicate __init__, _on_press, _on_release methods
- Both backends now properly handle toggle and push-to-talk modes

Fixed first_run_dialog.py:
- Added do_response() override to properly close dialog
- Added explicit destroy() call in show_first_run_dialog()

Fixed tray_indicator.py:
- Added _unregister_keyboard_callbacks() method
- Updated _setup_keyboard_shortcuts() to properly clean up old callbacks
- Updated update_shortcut() to properly handle mode switching

Fixed settings_dialog.py:
- Added set_subtitle() method to PreferenceRow class
- Updated UI text for push-to-talk mode
- Fixed _on_shortcut_changed to properly pass mode to callback

All 547 tests pass locally.

* feat(shortcuts): improve mode switching and add mode-aware display names

Key improvements:
- Add SHORTCUT_MODE_DISPLAY_NAMES for mode-specific shortcut names
- Add get_shortcut_display_name() function for mode-aware display
- Fix restart_with_shortcut() to properly save and restore all callbacks
- Fix update_shortcut() to properly handle mode changes
- Fix _setup_keyboard_shortcuts() to clear old callbacks before registering new ones
- Ensure only one type of callback is active at a time

This fixes issues where:
- Old callbacks remained registered after mode switch
- Single-tap triggered action after switching from push-to-talk to toggle
- Display names weren't appropriate for the selected mode

* fix(shortcuts): stabilize live mode switching callback lifecycle

* test(shortcuts): cover callback reset on mode transitions

* fix(shortcuts): remove super preset and migrate legacy config

* test(shortcuts): update coverage for super preset removal
2026-02-28 20:40:32 -08:00
Jatin K MalikandGitHub bcd6d94eea fix(text-injection): detect if IBus is the active input method before using it (#277)
* fix(text-injection): detect if IBus is the active input method before using it

This commit fixes issue #249 where Vocalinux incorrectly tries to use
IBus for text injection when IBus is installed but not configured as
the active input method (e.g., user uses ydotool on Arch + KDE Plasma 6).

Changes:
- Add is_ibus_active_input_method() function that checks GTK_IM_MODULE,
  QT_IM_MODULE, and XMODIFIERS environment variables to determine if
  IBus is the active input method
- Update _check_dependencies() to skip IBus when it's not the active
  input method, falling back to ydotool/wtype/xdotool instead
- Add comprehensive unit tests for the new functionality

The fix ensures Vocalinux will use the appropriate text injection
method based on the user's actual input method configuration,
rather than just checking if IBus is installed.

Fixes #249

* test: fix existing tests to mock is_ibus_active_input_method

* test: add comprehensive test coverage for is_ibus_active_input_method

Add additional tests to ensure complete coverage of the new functionality:

- Edge cases for empty string env vars
- Case-insensitive detection for all three env vars
- Priority ordering (GTK > Qt > XMODIFIERS)
- X11 fallback when IBus is not active input method
- Exception handling during IBus setup falls back correctly

Total tests: 62 (was 54), all passing.
2026-02-28 17:08:47 -08:00