* fix(ibus): restore engine process launch after XDG path import
PR #484 switched ibus_engine to a relative import of xdg_data_home for
Flatpak-safe paths. start_engine_process and the IBus component exec still
run this file by path (python /path/to/ibus_engine.py), which has no
package context, so the process died immediately with ImportError and
Vocalinux fell back to ydotool (clipboard paste).
Accept absolute/inline import fallbacks when loaded as a script, log child
stderr from engine.stderr.log on startup failure, and add a path-based
--xml smoke test so this does not regress.
* test(ibus): cover start_engine_process failure diagnostics
Raise patch coverage for the engine.stderr.log failure paths so
codecov/patch meets the 80% target on #534.
* chore: re-trigger CI after rebase onto main
GitHub rejects job-level if expressions that reference matrix
(Unrecognized named-value: matrix). Resolve the include list in a
small setup job instead so PRs stay x86_64-only.
Using github.sha in the cache key forced a full rebuild on every commit.
Hash packaging/flatpak instead so .flatpak-builder can restore across
runs, and only rebuild modules that actually changed.
PRs build x86_64 only; main and workflow_dispatch still cover both arches.
Drop the extra upload-artifact step in favor of the action's own upload.
Patch release for Flatpak/AUR packaging and post-0.14.0 fixes:
installer wrappers, ydotool dual API and Flatpak socket probe, model
download timeouts, layout-aware hotkeys, and related docs/web bumps.
* feat(flatpak): add Flatpak packaging for universal distribution
Adds a Flathub-ready Flatpak for the whisper.cpp engine, plus the minimal
runtime adaptations Flatpak actually needs — deliberately scoped down from the
earlier attempts (#416, #428), which bundled unrelated whisper.cpp GPU, audio,
and clipboard changes.
Packaging (packaging/flatpak/):
- app manifest, AppStream metainfo, and desktop entry (com.vocalinux.Vocalinux)
- generated offline Python dependency modules (flatpak-pip-generator)
- bundled shared modules: PortAudio, libayatana-appindicator, xdotool, xsel
Runtime changes (Flatpak-only, behaviour unchanged outside a sandbox):
- utils/paths.py: honour XDG_CONFIG_HOME/XDG_DATA_HOME so config, models and
runtime data live under ~/.var/app/<app-id> in the sandbox
- autostart uses `flatpak run <app-id>` (the host launcher is always on PATH)
- text injection and keyboard backend detect a Wayland host reached via XWayland
(no Wayland socket granted) and route through xdotool / pynput-on-X11
- tray indicator references the exported `<app-id>-*` host icon theme names
Also: a CI workflow building x86_64 + aarch64 bundles, and README / packaging
README / distro-compatibility doc updates.
Verified with a full flatpak-builder build + bundle install on aarch64:
pywhispercpp loads its native libraries without LD_LIBRARY_PATH, XDG paths
redirect correctly, bundled tools are present, and the entrypoint runs.
Manifest and builddir lint are clean and AppStream metadata validates.
Closes#167
* chore(flatpak): sync metainfo version to 0.14.0-beta
Also ignore flatpak-build-dir/ so local builder output stays untracked.
* refactor(flatpak): trim docs and comments after ponytail review
Drop redundant packaging README command blocks, shrink path/tray/autostart
comments, and stop ignoring a non-canonical build-dir name.
* fix(flatpak): close readiness gaps for v0.14.0-beta
Route log files through XDG data dirs so Flatpak keeps them under
~/.var/app, refresh AppStream screenshots to vocalinux.com, and use
the SPDX GPL-3.0-only license id.
* fix(flatpak): enable global hotkeys via evdev and --device=input
pynput over XWayland only receives keys for the focused X client. Prefer
evdev inside Flatpak and grant /dev/input so shortcuts work app-wide.
* fix(flatpak): inject text with ydotool/uinput on Wayland
xdotool only reaches XWayland clients, so typing appeared to succeed but
never hit native apps. Ship ydotool, grant --device=all for uinput, start
ydotoold on demand, and type via uinput inside the Flatpak.
* fix(flatpak): paste via wl-copy + Ctrl+V instead of typing
ydotool type injects character-by-character. Ship wl-clipboard, grant
--socket=wayland, and prefer clipboard paste for instant insertion.
* test(text-injection): expect ydotool default key-delay of 2
Matches the shipped VOCALINUX_YDOTOOL_KEY_DELAY default used when
clipboard paste falls back to character typing.
* fix: sg not found on Ubuntu 26.04/Debian 13
The 'sg' command moved from the 'login' package to 'util-linux-extra'
in Debian 13 and Ubuntu 26.04, which is not installed by default.
This causes vocalinux and vocalinux-gui wrappers to fail with
'exec: sg: not found'.
Two changes:
1. Add util-linux-extra to APT dependency lists (Ubuntu + Debian 13+)
2. Add command -v sg safety check in both wrapper scripts so they
fall back to direct execution if sg isn't available
Fixes#522
* fix: attempt to install util-linux-extra if sg is missing
When the wrapper scripts detect that sg is not available, they now
attempt to install util-linux-extra via apt-get before falling back
to direct execution. This provides a better user experience on
Ubuntu 26.04/Debian 13 where sg moved to a separate package.
The installation runs silently and only triggers if:
1. User is in input group but session doesn't have it
2. sg command is not found
3. apt-get is available (Debian/Ubuntu systems)
If installation fails or sg still isn't available, the scripts
fall back to direct execution (app starts but keyboard shortcuts
won't work until next login).
* refactor: extract shared sg check, drop runtime install
- Extract sg fallback logic to shared SG_CHECK variable (eliminates duplication)
- Remove runtime apt-get install of util-linux-extra (dependency list handles it)
- Flatten to simple command -v sg check with direct exec fallback
net: -24 lines
---------
Co-authored-by: Alfred Pennyworth <alfred@openclaw.ai>
* docs(screenshots): refresh v0.14 settings/About shots and add website gallery
Replace the outdated About dialog screenshot and supersede the old single
settings dialog with the six v0.14.0-beta settings tabs. Keep unique product
shots that have no replacements. Add a dedicated /screenshots/ website page
with header/nav discoverability and a homepage link (no full gallery on home).
* docs(screenshots): drop obsolete features-overview shot
The annotated features overview was the pre-tabbed settings panel and no
longer matches the v0.14 UI. Remove it from README, website gallery, and assets.
* docs(screenshots): ship full-resolution web transcription shot
Restore the original PNG instead of the downscaled copy so the website
gallery matches the repo assets bit-for-bit.
* feat(web): add click-to-expand lightbox on screenshots gallery
Open a full-screen dialog when a shot is clicked, with prev/next controls,
keyboard arrows, Escape to close, and captioned counter for browsing the set.
* fix(web): avoid next/image HMR crash on screenshots gallery
Use plain img tags in the client gallery so dev-mode webpack chunk
reloads do not throw options.factory call errors while hydrating.
* feat(packaging): add AUR release package and CI publish
Ship packaging/aur/vocalinux for yay installs of tagged releases, wire
AUR publish into the release workflow (skipped until secrets are set),
and document one-time maintainer setup.
Fixes#116
* refactor(packaging): slim AUR PR to PKGBUILD + CI
Drop systemd unit, install script, website edits, and long docs.
Keep release PKGBUILD, minimal publish job, and short AUR setup notes.
* fix(hotkeys): resolve combo letters via active XKB layout
Settings capture stores character tokens from GDK keyvals (e.g. alt+a),
but the Wayland/evdev path mapped letters to fixed US KEY_* codes. On
AZERTY that made Alt+A listen for the key labeled Q. Resolve letter
tokens through libxkbcommon for the active layout so recording and
listening agree.
Fixes#513
* refactor(hotkeys): slim layout-aware key resolution
Drop unused resolve helper, dual knobs, setxkbmap/localectl probes, and
redundant tests. Keep gsettings + libxkbcommon map and the AZERTY combo path.
* test(hotkeys): drop redundant AZERTY resolve-only case
Keep fire/no-fire combo coverage and the XKB fr map check.
* test(hotkeys): cover layout map detection and error paths
Raise patch coverage for layout_key_map / evdev_code_for_key so codecov
patch target (80%) is met.
Bump brace-expansion, js-yaml, @babel/core, and related transitive
deps via npm audit fix. Override nested postcss so Next.js no longer
pulls vulnerable 8.4.31 (XSS in CSS stringify output).
* chore(release): prepare v0.14.0-beta
- Bump version 0.13.0-beta -> 0.14.0-beta (src/vocalinux/version.py)
- README: refresh What's New for v0.14.0-beta and bump comparison table
- docs/UPDATE.md: add v0.14.0-beta section and bump checkout example
- SECURITY.md: mark 0.14.x supported, 0.13.x unsupported
- docs/RELEASE_PROCESS.md: add v0.14.0-beta to version history
- web: bump package.json / package-lock.json / page.tsx schema + badge
- web changelog: add v0.14.0-beta entry
* ci(codecov): allow 0.5% project coverage threshold
Prevents version-bump/docs-only PRs from being blocked by minor
coverage fluctuations (e.g. -0.04% on a release-prep branch).
* docs(release): correct chronological order in version history table
* docs(release): add #509 shortcut UI exclusivity fix to v0.14.0-beta notes
Selecting a preset after recording a custom combo left the custom entry
filled, so both looked active. Sync the dropdown to "Custom Shortcut"
when a non-preset is set, and clear the custom field when a preset wins.
#502 only exempted KDE from the #478 real-engine gate. GNOME/Mutter has
the same scoped-activation path, so stock sessions with only an xkb:*
layout fell through to wtype, which Mutter rejects, then clipboard-only.
Allow any Wayland session without an explicit non-IBus IM to try IBus
when ibus-daemon is running; unbridged compositors still hit the
existing denylist.
Fixes#504
* feat(asr): add chat audio remote benchmarking
* style(asr): apply black formatting
* test(main): include remote API model kwarg
* test(asr): add coverage for chat-completions error handling and parsing
Add 10 new tests to cover uncovered lines in PR #468:
- ConnectionError handler in _try_chat_completions_audio_api (lines 1601-1602)
- _parse_chat_completion_transcription edge cases: None, list content,
empty/whitespace strings, JSON without text field, plain text, and
single-line language metadata (lines 1610, 1613-1621, 1625, 1629-1640)
Coverage for recognition_manager.py improved from 85% to 86%.
* feat(asr): support FunASR remote responses
* refactor(asr): scope remote FunASR support to the #469 path
Drop chat-completions/Qwen client, evaluation notes, and the remote ASR
benchmark harness. Keep model field, tolerant OpenAI response parsing,
SenseVoice label stripping, FunASR docs, and tests.
The CPU-only auto thread count was `min(cpu_count(), 8)`. On an 8-core
hybrid CPU (e.g. Intel Core Ultra, P+E cores) this selects 8 = every
core. whisper.cpp synchronises all worker threads at each decode step,
so pulling the slow efficiency cores into the pool makes every barrier
wait on the slowest core, and leaves nothing for the OS / mic capture.
Measured on a Core Ultra 7 355 (8 cores, no SMT), base model, 6s clip:
n_threads=6 -> RTF 0.14x
n_threads=8 -> RTF 2.13x (~15x slower, i.e. slower than real time)
Default to `max(1, min(cpu_count() - 2, 8))`, which leaves headroom and
picks 6 on that machine (optimal in a sweep) while staying <= the
previous value everywhere else. Users can still override via the
`whispercpp_n_threads` config setting.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* feat(shortcuts): configurable modifier+key hotkeys (e.g. Alt+R)
Generalize the shortcut system beyond single-modifier double-tap/hold to
support modifier+key combos (alt+r, ctrl+alt+r, super+space) in both toggle
(press once) and push-to-talk (hold) modes.
- base.py: ShortcutSpec + parse_shortcut_spec/is_valid_shortcut/format label;
parse_shortcut kept for back-compat (bare modifiers still rejected).
- evdev + pynput backends: combo detection with global (cross-device) modifier
tracking so split keyboards work; legacy gestures unchanged.
- keyboard_shortcuts: validate via is_valid_shortcut instead of a fixed allowlist.
- settings_dialog: additive Custom Shortcut row (typed entry + live Record capture).
- tests: 34 new tests (parser, back-compat, evdev + pynput combo, split keyboard).
* fix(shortcuts): apply saved shortcut (not stale preset) on mode change
When a custom shortcut (e.g. alt+r) is active, the preset combo still points at
a default/previous preset. _on_shortcut_mode_changed read the combo, so toggling
Toggle<->Push-to-Talk restarted the live listener on the wrong shortcut while
config still said alt+r. Read the saved shortcut from config instead.
Addresses Codex P2 review on PR #493.
* style: apply black formatting to configurable-hotkeys files
* test: cover _gdk_keyname_to_token shortcut-capture mapping
* fix(shortcuts): reset combo state after dropped/lost evdev events
SYN_DROPPED and device-disconnect recovery cleared key_pressed_devices but not
the combo state. A dropped modifier *release* left _combo_pressed thinking the
modifier was held, so the main key alone could falsely toggle/start dictation,
and a push-to-talk hold could stay stuck. Add _reset_combo_state() (clears the
pressed set and ends any live hold) and call it from both recovery paths.
Addresses maintainer review on PR #493.
* fix(settings): combo-aware toggle label (single press, not double-tap)
_update_shortcut_ui_for_mode always said 'Double-tap the key' for toggle mode,
but a modifier+key combo (e.g. alt+r) toggles on a single press. Read the saved
shortcut and use get_shortcut_display_name so a combo shows 'Press Alt+R'.
Addresses maintainer review on PR #493.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* fix(text-injection): wait for shortcut modifiers to release before Wayland inject
Injection emits real key events, so a held modifier from a toggle/PTT shortcut
(e.g. Alt from Alt+R) turns the simulated Ctrl+V paste into Ctrl+Alt+V and
nothing pastes — while the clipboard still shows the text. Because transcription
can finish in tens of ms, the user is often still holding the key, causing
intermittent paste failures.
Poll evdev for held modifier keycodes and defer injection until they clear
(bounded by VOCALINUX_INJECT_MODIFIER_WAIT, default 1.0s, 0 disables). Returns
immediately when nothing is held, so no added latency in the common case.
* style: black-format injection modifier-wait test
* test: cover _held_modifier_keycodes evdev scanning (patch coverage)
* test: cover error paths in _held_modifier_keycodes
* fix(text-injection): treat non-finite modifier-wait env as default
float('inf') parses cleanly and is > 0, so it slipped past the guard and made
the wait deadline infinite -- a held modifier could block injection forever
(nan was nonsensical too). Sanitize VOCALINUX_INJECT_MODIFIER_WAIT via a helper
that falls back to the 1.0s default for non-finite values. Regression tests
added, including that 'inf' still terminates.
Addresses maintainer review on PR #494.
* Fix: IBus engine restore uses wrong fallback on GNOME/Wayland (#497)
On GNOME/Wayland, when IBus has no global engine (common when Mutter
manages input sources), 'ibus engine' can return a misleading
'xkb:us::eng' default that doesn't match the user's actual keyboard
layout. When Vocalinux restores this wrong value after text injection,
Electron apps switch to US layout.
Root cause: get_current_engine() trusted the output of 'ibus engine'
without checking whether IBus actually had a global engine set, and
had no fallback to discover the real keyboard layout.
Fix:
- Add get_current_engine_gnome_fallback() that reads the actual
keyboard layout from org.gnome.desktop.input-sources via gsettings
- Modify get_current_engine() to detect 'No global engine' in stderr
and to detect the suspicious 'xkb:us::eng' default on GNOME/Wayland,
falling back to gsettings in both cases
- Modify stop() to log a message when skipping engine restoration
because no valid engine was captured (None means don't restore)
- Modify inject_text() to log when skipping restoration after
injection for the same reason
- Backward compatible: if gsettings is unavailable, falls back to
current behavior (returning None or the IBus-reported value)
Tests: 12 new tests covering GNOME fallback, 'No global engine'
detection, suspicious default detection, gsettings parsing, and
edge cases.
* ponytail: simplify gsettings uint32 parsing
gsettings prints 'uint32 N' or just 'N' — split()[-1] handles both,
no need for separate branches
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(audio): resolve device by name to prevent index mismatch crash (#498)
Audio device indices can shift between sessions when USB devices are
replugged or virtual devices are added/removed. A saved device_index=24
(BRIO webcam) could later point to 'speech-dispatcher-dummy', causing
PortAudio to attempt recording from a virtual device and triggering
malloc corruption / SIGABRT.
Fix has three parts:
1. Store and match by device name, not just index
- Config already saves device_name alongside device_index
- At recording time, _resolve_device_by_name() enumerates current
devices and finds the one matching the saved name
- Falls back to stored index, then system default (with warning log)
- main.py now passes audio_device_name to SpeechRecognitionManager
2. Filter virtual devices from input device enumeration
- _is_virtual_device() detects speech-dispatcher, dummy, null sink,
monitor, and ALSA output devices
- get_audio_input_devices() skips virtual devices
- Settings dialog dropdown only shows real input devices
- Debug logging in _record_audio also skips virtual devices
3. Validate resolved device has input channels (from flatpak branch)
- Extra safety check: after resolving, verify maxInputChannels > 0
- Falls back to system default if resolved device is invalid
- Brings the validation from feature/flatpak-packaging to main
Closes#498
* style: apply black formatting to recognition_manager.py
* test: add audio_device_name kwarg to mock assertion
* test: add coverage for _is_virtual_device, _resolve_device_by_name, device name management
* test: fix StopIteration in fallback test by using return_value for mock
* ponytail: remove redundant validation and simplify _resolve_device_by_name
- Remove ~20 lines of dead validation in _record_audio() — _resolve_device_by_name
and _resolve_valid_input_device already check maxInputChannels and virtual devices
- Simplify _resolve_device_by_name to just find index by name, then delegate all
validation to _resolve_valid_input_device (which already does channel/virtual checks)
- Update tests to match the simplified delegation pattern
* style(speech): format device fallback warning
* test(speech): cover audio device fallback edges
* test(speech): cover reconnection device fallback
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(ibus): require a real IM engine on Wayland (#478)
On Wayland a bare xkb:* IBus engine is a layout passthrough that never
delivers commit_text() to applications, so selecting the IBus backend
there silently no-ops (niri/sway, and GNOME/Mutter via the env-var
branch). Require a real IM engine before treating IBus as active on
Wayland, regardless of how it was detected; X11/XWayland (XIM) is
unchanged. Complements #486's compositor denylist.
* test(ibus): bind #478 patches to the module under test
Patch get_current_engine/is_ibus_daemon_running via patch.object on a
held ibus_engine reference and call is_ibus_active_input_method through
it, so the patched functions and the function under test always share
one module object. Fixes flakiness in the full CI suite where other test
files re-import ibus_engine and the dotted-string patch bound a
different module object.
On Wayland, setxkbmap only reaches the XWayland X11 server, so
restore_xkb_layout() rewrote the XWayland layout (flipping it to us for
XWayland/Electron apps) while native Wayland apps stayed correct. Make
restore_xkb_layout() a no-op on Wayland and stop defaulting the captured
layout to 'us' so a never-captured layout is never re-applied.
Pass --key-delay 8 to ydotool type instead of relying on the default
(12-20ms). This makes text injection ~3x faster while keeping key-delay
above zero to avoid the Shift-leak bug where capital letters corrupt
following characters.
The delay is configurable via VOCALINUX_YDOTOOL_KEY_DELAY env var.
Note: --key-hold flag from the original issue does not exist in standard
ydotool (verified against official man page and v1.0.4 source).
Fixes#482
* fix(text_injection): use wtype on compositors that don't bridge IBus to native apps
On Wayland compositors that don't bridge IBus to native clients (COSMIC,
Sway, Hyprland, Wayfire, river, niri, ...), the IBus engine's commit_text()
reaches only XWayland and GTK/Qt apps that load the IBus IM module. Native
apps such as cosmic-term receive nothing, yet inject_text() reports success,
so dictation silently does nothing.
- Add a compositor denylist (_wayland_compositor_bridges_ibus) so IBus is
skipped on those compositors and the virtual-keyboard path (wtype/ydotool)
is used instead. A denylist (rather than an allowlist) keeps the existing
IBus-preferred behaviour on unrecognised desktops. Complements the recent
KDE Plasma Wayland handling, which covers the mirror-image case.
- Replace the unreliable `ydotool type ""` probe (exits 0 even with no daemon)
with _is_ydotoold_running(), so a daemonless ydotool install correctly falls
back to wtype instead of selecting a no-op ydotool.
- Add unit tests for the compositor gate, the ydotoold check, and that an
active-IBus COSMIC session selects wtype.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(text_injection): cover ydotoold socket-env and ydotool direct-mode paths
Add tests for the two new branches codecov/patch flagged as uncovered:
the explicit YDOTOOL_SOCKET path in _is_ydotoold_running(), and the
ydotool direct-mode fallback when no daemon and no wtype are available.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two independent text-injection bugs on Wayland:
1. Clipboard copy silently failed. wl-copy/xclip/xsel fork a background
process that keeps owning the selection; capturing its stderr with
subprocess.PIPE makes run() block on the surviving child until the
clipboard is next overwritten, so the call always hits its timeout and
_copy_to_clipboard reports failure even though the copy succeeded.
Redirect stdout/stderr to DEVNULL so run() only waits for the
short-lived foreground process.
2. ydotool scrambled text on non-US keyboard layouts. `ydotool type`
emits positional evdev keycodes that get re-interpreted through the
active layout (assumed US QWERTY), so dictated text came out garbled on
AZERTY/QWERTZ/Dvorak (e.g. "message" -> ",essqge") and non-ASCII was
dropped. Always inject via clipboard paste (Ctrl+V) for ydotool, which
is layout-independent and Unicode-safe.
Adds regression tests for both.
Claude-Session: https://claude.ai/code/session_01EhyratrEE4vDWhriirUB9k
Configure actions/labeler@v5 to automatically label PRs based on file paths.
Maps changes to: app, web, documentation, ci, tests, installer, resources, dependencies.
Creates 6 new labels to complement existing issue labels for better PR organization.
* feat(whisper): refine whisper.cpp model selection
* refactor(whisper): exclude diarization model from picker
* docs(whisper): document guided model variants
* fix(ui): cap settings dialog height at 600px to prevent excessive height on high-res displays
* 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.
* 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
Reduce dialog height ratio from 50% to 40% of viewport and revert
Advanced tab scrolled window min_content_height from 500px back to
350px. The 500px min content height (introduced for Remote Server
section) forced the dialog to grow beyond its viewport cap on screens
under 1000px tall, causing it to dominate lower-resolution displays.
* 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.
Add a mock test server to help developers test the remote API feature
without needing a real server. The server supports both whisper.cpp
(/inference) and OpenAI-compatible (/v1/audio/transcriptions) formats.
- Move test_remote_server.py to scripts/ directory
- Make script executable
- Add documentation to CONTRIBUTING.md with usage examples
- Include instructions for testing both API formats
Usage:
python scripts/test_remote_server.py [--port PORT] [--delay SECONDS]
* 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
* fix(models): correct download size metadata for whisper.cpp and VOSK
The size_mb values in WHISPERCPP_MODEL_INFO were using parameter counts instead of actual download sizes, causing the UI to display incorrect sizes (e.g., showing 39 MB when actual download is 74 MB). Updated all whisper.cpp models with verified sizes from Hugging Face and adjusted VOSK medium/large sizes to better represent actual downloads.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
* docs: update model download sizes to match actual downloads
Update whisper.cpp size references from parameter counts to verified download sizes (e.g., tiny: 39MB → 74MB, medium: 769MB → 1.5GB, large: 1.5GB → 3.0GB).
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
* docs(web): update model sizes across website pages
Sync whisper.cpp size references on gpu-acceleration, compare, whisper-model-guide, offline, and homepage with corrected download sizes.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
* 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).