9e6a03cd207b4e55c87fb8554c222639dc0be130
247
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9e6a03cd20 |
feat(settings): make the settings category list scrollable (#886)
* feat(settings): make the settings category list scrollable Wrap the sidebar category ListBox in a Gtk.ScrolledWindow (matching the page wrappers) so the dialog can shrink vertically without clipping categories and new pages do not pin the minimum window height. Fixes #678 * fix(settings): reveal the selected sidebar row in the scroller * fix(settings): retry sidebar scroll until the selected row is laid out --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
ff108595fd |
fix(hotkeys): consume the dictation shortcut so it stops leaking to apps (#873)
* fix(hotkeys): consume the dictation shortcut so it stops leaking to apps Grab each monitored keyboard with EVIOCGRAB and re-emit every event the shortcut does not own through a paired uinput clone. The configured shortcut key is now consumed by Vocalinux instead of also reaching the focused app (right-alt opening menus in Thunderbird/Firefox, F11/F12 triggering browser actions). - Pure-modifier gestures reserve the modifier's events entirely; bare function-key shortcuts consume that key; modifier+key combos consume the main key only while the combo owns it, so typing and chords like Alt+Tab keep working and injected text is untouched - BUS_VIRTUAL devices (our clones, ydotool, remappers) are never grabbed - EV_REP is stripped from clones and repeats are forwarded verbatim, so repeat rate is preserved without doubling - Feedback event types (EV_LED/EV_SND/EV_FF) are never re-injected - Falls back to ungrabbed listening when /dev/uinput or the grab is unavailable, warning once that keys will also reach the app Fixes #871 * test(hotkeys): cover grab/forward edge cases Race cleanup on duplicate open, forwarder close errors during stop and device removal, mid-SYN_DROPPED forwarding, write failures, and the combo consume edge cases (unowned repeat/release, missing main key). * fix(installer): grant read on /dev/uinput so key suppression works The evdev backend's uinput clone opens /dev/uinput O_RDWR, but the installed 80-dotool.rules gave the input group write-only access (0620), so shortcut suppression silently fell back to ungrabbed monitoring on stock installs. Bump the rule to 0660 and refresh it when the installed copy differs so existing installs pick up the fix. * fix(hotkeys): harden evdev grab/forward lifecycle and udev install - release the device grab and close the clone when a uinput write fails - close all devices if the monitor exits unexpectedly (releases grabs) - monitor virtual devices too; skip only our own uinput clones - track consumed combo presses per device so releases stay paired - after SYN_DROPPED, release keys the clone still believes are held - append the uinput udev rule instead of overwriting local rules - annotate new signatures and narrow the clone-creation catch * fix(hotkeys): reconcile swallowed combo releases after SYN_DROPPED - resync the swallowed-press pairing even when the clone holds no keys; a dropped combo release otherwise leaves the next ordinary press consumed, eating a keystroke - narrow every new close-cleanup catch to OSError/IOError/RuntimeError with debug context instead of blanket Exception passes --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
004696bbe0 |
refactor(settings): consolidate dialog guard flags and widget construction (#878)
* refactor(settings): consolidate dialog guard flags and widget construction The settings dialog grew the same work in several places. This gathers the shared pieces into module helpers with no behavior change: - _suppressed(): a context manager for the try/finally flag dance the handlers used to open-code for _populating_models, _processing_language_change, _simple_syncing, and _simple_driving. The apply-guard handoff and the nested resync keep their explicit save/restore because they do not fit a lexical block. - _handlers_suppressed(): one name for the "initializing or applying" check every handler repeated on entry. - _set_no_show_all_visible(): the clear-flag / show / re-arm sequence for rows created hidden, used by the second-language row and the custom-shortcut row. - _add_switch_row(), _combo_chrome(), _new_spin(), _connect_picker_entry(): the repeated switch-row, tooltip + scroll-guard, spin-button, and picker-entry wiring. - _undownloaded_model_info() and _make_download_dialog(): the per-engine download gate both apply paths carried verbatim. Flags stay plain booleans and helpers stay module-level so existing tests keep working unchanged. * fix(settings): annotate download dialog builder and align visibility docs --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
ba721779c3 |
fix(downloads): abort blocked model fetches on cancel (#888)
* fix(downloads): abort blocked model fetches on cancel requests.get() has no way to abort while it is still resolving, connecting, or waiting for response headers — which is what an unreachable download server looks like. The cancel flag was only checked between response chunks, so Cancel sat until the request timed out. The blocking open now runs on a helper thread the caller polls every 200 ms; on cancel it raises at once while the abandoned attempt stays bounded by the existing timeouts. The settings dialog's download workers also reported "Model ready to use" whenever the apply returned True, but reconfigure() can no-op when the selection matches live state — success without a download. They now pass force_reinit so a reported success means the engine actually initialized the downloaded model. Fixes #679 * fix(downloads): bound cancel openers, close late responses - Track blocked request opener threads and give each attempt its own stop event so a cancel-and-retry cannot stack unaccounted requests and a response landing after cancel is closed instead of leaked - Catch requests.exceptions.RequestException in the opener instead of a bare Exception - Close the streaming response on the mid-stream cancel path - Make the blocked-open test helper cancellable and reap tracked openers; check each download worker call site for force_reinit separately; add missing return annotations on new test helpers and methods --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
c9ea6c7de7 |
feat(flatpak): publish a signed self-hosted OSTree remote on releases (#875)
* feat(flatpak): publish a signed self-hosted OSTree remote on releases (#785) release.yml gains publish-flatpak-remote: it imports the per-arch .flatpak bundles into an OSTree repo, signs commits and the summary with a provisioned GPG key, regenerates static deltas and the vocahq.flatpakrepo/index page, and pushes to VocaHQ/vocalinux-flatpak served by GitHub Pages — the Homebrew-tap analogue, so installs auto-update via flatpak update instead of per-release downloads. The job is secret-gated like publish-aur (FLATPAK_GPG_PRIVATE_KEY + FLATPAK_REPO_TOKEN) and skips itself while unconfigured. Prior repo objects are pulled back in from gh-pages so clients get incremental deltas. The manifest now declares branch: stable and the flatpakrepo declares DefaultBranch=stable + RuntimeRepo for the Flathub GNOME runtime. Docs: new docs/FLATPAK_REMOTE.md (one-time setup, rotation, verification); README, docs/INSTALL.md, packaging/flatpak/README.md and the release notes now lead with the remote and keep release bundles as the no-auto-update fallback. tests/test_release_integrity.py pins the job's shape. Closes #785 * fix(flatpak-remote): publish a .flatpakref and gate the remote to stable tags - needs build-and-release so the remote cannot lead the GitHub Release - stable tags only: prereleases must not reach `flatpak update` - probe gh-pages before the empty-repo fallback: a transient checkout failure no longer discards remote history - publish com.vocalinux.Vocalinux.flatpakref (RuntimeRepo lives there, not on .flatpakrepo) plus vocahq.gpg, so clean installs resolve the GNOME runtime and key rotation works via remote-modify --gpg-import - gate the release-notes remote instructions on the publish actually running; the bundle path stays primary otherwise - document rotation via remote-modify --gpg-import / remote-delete --force and the explicit Flathub setup step --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
d29c884782 |
fix(settings): gray out Dictation Tone while sound effects are off (#877)
* fix(settings): gray out Dictation Tone while sound effects are off The Enable Sound Effects switch mutes every dictation cue, so the tone picker changed nothing while it was off. The row now goes insensitive with the switch and follows the state loaded from config, matching the keep-alive timeout and auto-pause sensitivity pattern. Also bump the pre-commit isort pin 5.13.2 -> 8.0.1 to match uv.lock: the two versions disagree on single-name import wrapping, so the old hook collapsed imports the locked isort (and CI) then expanded back. Fixes #849 * test(settings): cover tone-row sensitivity on settings load --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
77def71354 |
fix(audio): stop passing explicit index when zero devices enumerate (#891)
* fix(audio): stop passing explicit index when zero devices enumerate With zero audio devices, _resolve_valid_input_device returned the saved preferred_index, so _open_capture_stream opened input_device_index=<stale> — a device that cannot exist — and Pa_OpenStream aborted the whole process (SIGSEGV) before the clean IOError->ERROR path could run. Zero enumerated devices now returns None so PyAudio falls back to the system default and fails catchably; a negative count (enumeration failure) still passes the preferred index through. * test(audio): cover capture path omitting device index * test(audio): cover saved-device zero-enumeration capture path * test(audio): annotate new resolver tests |
||
|
|
a89966a744 |
feat(recognition): record while an idle model reloads (#851)
* feat(recognition): buffer audio during model reload * fix(recognition): recover buffered reload failures * test(recognition): cover buffered reload edge cases * fix(recognition): narrow buffered reload errors * fix(settings): wait for IDLE before restoring Test Dictation callbacks Buffered reload's stop_recognition() returns while PROCESSING. Test Dictation must keep its text callbacks until recognition reaches IDLE so the test utterance cannot inject into the focused app. * style: apply Black formatting to PR 851 files * fix(settings): cancel buffered reload before Test Dictation timeout restore On the ~3min idle-wait timeout, do not restore live injector callbacks while a buffered reload worker may still deliver text. Cancel via _cancel_reload_recording off the GTK thread, then restore; if cancel is unavailable or fails, leave test callbacks installed and show an error. Add return/param annotations and regression coverage. * fix(settings): keep Test Dictation waiting if buffered cancel fails After the idle-wait timeout, if cancel is unavailable or raises, keep polling with test callbacks and _test_active instead of marking the test done. Ending early left the test callback as the only text destination, so later normal dictation could land in the hidden test buffer. Restore live injectors only once recognition reaches IDLE. * fix(settings): reset Test Dictation timeout-cancel flag per session Clear _test_timeout_cancel_attempted when a test starts, when entering the buffered idle-wait, and when restoring UI so a later Test Dictation can still cancel after timeout. Narrow the cancel-worker except clause to AttributeError/OSError/RuntimeError/TypeError so unrelated defects are not treated as ordinary cancel failure. * fix(recognition): signal capture completion when buffered capture dies A dead microphone during a buffered model reload left should_record=false with no stop_recognition handoff, so the reload worker blocked forever on _capture_finished.wait(). Signal the event from the audio thread's failure teardown paths instead. Refs VocaHQ/vocalinux#851 --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
db07590761 |
feat(activation): D-Bus activation for compositor global shortcuts (#568)
Rebased onto latest main (conflict-resolution squash for PR #568). Co-authored-by: Alexander <ab@digital-benefits.de> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
c0217a8aec |
refactor(installer): generate distro package map (#872)
* refactor(installer): generate distro package map * fix(installer): handle Debian derivative package maps * docs(installer): document generated package map * fix(installer): validate package map and refresh derivative probes * fix(installer): name openSUSE package roles |
||
|
|
fb9166ed9e |
feat(settings): run local VocaGateway from Advanced (podman-first) (#774)
* feat(settings): run local VocaGateway from Speech Engine (podman-first) Add an optional Local VocaGateway panel next to Remote Server so Linux users can start the pinned v0.1.0 compose project with podman (docker fallback), watch Live/Pairable/Ready status, pair a phone without loopback QR URLs, and preset remote_api to the gateway transcriptions endpoint. Stop preserves volumes; Flatpak fails closed; tray offers Stop only when this session manages the gateway. Default on-device engine stays whisper.cpp. * fix(gateway-embed): drop nonexistent compose cpu profile VocaGateway v0.1.0 exposes default service gateway plus native/cuda/vulkan profiles, not cpu. Start and stop the default service without --profile, keep the latest image pin rejection, and prefer save_config for the remote_api preset. * fix(gateway-embed): keep compose --profile cpu per embed contract v0.1.0 has no cpu profile entry, but Compose still starts the unprofiled gateway service when --profile cpu is set, and keeps cuda/vulkan/native off. That matches the frozen desktop embed contract and stays forward-compatible. * fix(gateway-embed): harden embed path for settings and compose Keep podman/docker probing off the GTK thread, reject link-local and loopback public URLs for phone QR, sanitize compose .env image overrides, and cap bootstrap token reads so a hostile file cannot blow memory. * fix(gateway-embed): keep LAN toggle and phone QR in sync When Allow LAN flips while compose is already managed, rewrite publish host and PUBLIC_URL and recreate the gateway before Pairable comes back. Also reject default docker/podman bridge addresses for phone QR. * test(gateway-embed): pin bridge and LAN-gate pairing cases Cover 172.17.0.2 / link-local not becoming Pairable, and the mid-run LAN toggle path that must wait for compose republish before advertising a phone URL. * style(gateway-embed): black format embed modules * fix(gateway-embed): reject short-form loopback and localhost.* for QR Align Pairable/QR host checks with vocagateway desktop-embed policy so 127.1 / localhost.* never become phone-reachable display URLs. * fix(gateway-embed): detach Settings status listener on destroy Gateway poll callbacks were left registered after Settings closed, so idle_add could touch destroyed GTK widgets while compose stayed up. * fix(gateway-embed): pass host_env to bridge ip probe _bridge_iface_ips shells out to host ip; without host_env the AST gate fails and AppImage bundles can hand the wrong GLib to ip. * fix(packaging): ship gateway_embed in the AppImage The explicit setuptools packages list omitted the new package, so the bundle booted from source trees but died with ModuleNotFoundError on every distro boot test. * style(packaging): black the gateway_embed package guard * fix(settings): pack local VocaGateway into the Advanced island The rebase onto main auto-merged textually onto the simple Speech Model page. Keep the gateway card on advanced_box, next to Remote Server. * fix(gateway-embed): treat missing Compose as unavailable When podman or docker is present but Compose is not, stop advertising Run. Return empty compose_args, show an install-Compose hint, and cover it with a unit test. Also add module loggers on the new gateway_embed modules and annotate the gateway Settings/tray callbacks Greptile flagged. just lock found no lockfile drift. * fix(gateway-embed): keep Allow LAN bind matched to the switch Snapshot the LAN flag for start/republish, record what compose actually got, then republish again if the switch moved mid-flight so 0.0.0.0 never lingers while Allow LAN reads off. * fix(gateway-embed): detect leftover compose after Quit Quit does not stop compose, so a LAN-published gateway can keep running. Settings and the tray treated that as Stopped because managed_by_us is only process memory. Probe compose after runtime detection, show Stop when it is actually up, and let Stop work even if this session did not start it. * fix(gateway-embed): restore LAN switch from leftover compose Quit leaves compose running, so the next launch now reads VOCAGATEWAY_PUBLISH_HOST from the embed .env, adopts the project as managed, and keeps Allow LAN honest so republish can close 0.0.0.0. * fix(gateway-embed): restore .env if LAN republish fails compose recreate can fail after write_env_file already pointed VOCAGATEWAY_PUBLISH_HOST at loopback. Restore the previous .env so orphan adopt still sees a live 0.0.0.0 bind, and snap Allow LAN back to the running publish mode. * style(gateway-embed): type helper args and black MagicMock * refactor(types): move Readable protocol to common_types AGENTS.md keeps Protocol interfaces in common_types. Pairing now imports the shared Readable surface instead of a local _Readable helper. * fix(gateway-embed): satisfy mypy on rebased embed code * fix(audio): keep duck restore point when a set reports failure |
||
|
|
c2654609ac |
feat(ui): floating glowing dictation overlay (#516)
* feat(ui): add floating glowing dictation overlay Show a bottom-center non-focus overlay while recognition is LISTENING or PROCESSING so users get a clear visual cue beyond the tray icon. Optional GtkLayerShell on Wayland; plain always-on-top GTK fallback elsewhere. Configurable via ui.show_overlay (default on) and Settings. * fix(test): isolate dictation overlay tests from GTK pollution Overlay unit tests no longer construct real GTK windows or import tray_indicator, which was loading gi.repository early and breaking later tests that mock GI (single-instance and suspend GLib checks). * docs: add dictation overlay screenshots Capture the floating glowing orb in listening (green) and processing (amber) states for the PR and README screenshots section. * test(ui): raise dictation overlay coverage for Codecov patch gate Exercise GTK init, draw/animation, positioning, layer-shell fallbacks, tray overlay wiring, settings toggle, and config edge cases so patch coverage on the floating overlay clears the 80% Codecov target. * test(tray): call overlay helpers via unbound methods Avoid instance-level MagicMock shadowing of set_overlay_enabled under some import/mock orderings (seen on CI Python 3.10). * test(tray): inject explicit config mock for overlay helpers Assign a MagicMock config_manager on the instance so assertions do not depend on the constructor patch (which can leave a real ConfigManager under some Python versions / import orders). * refactor(ui): ponytail pass on dictation overlay Drop the hand-drawn mic glyph, simplify the Cairo clear/pulse path, reuse ConfigManager get_bool/set, tighten tray overlay helpers, and delete string-grep structural tests that proved nothing. * fix(ui): pin overlay to bottom strip and stop stealing focus Use a full-width bottom strip (layer-shell left/right/bottom anchors, or resize+move fallback) so the orb is bottom-center on Wayland. Map the window once and toggle opacity instead of hide/show so compositors do not re-activate it and yank focus out of the editor (which broke IBus inject). * fix(appimage): seed GtkLayerShell typelib for dictation overlay * fix(ui): address greptile findings on dictation overlay * fix(appimage): keep layer-shell typelib optional for direct builds |
||
|
|
eef2554f7c |
feat(audio): lower other audio while dictating (#861)
* feat(audio): lower other audio while dictating Opt-in setting lowers the default PipeWire or PulseAudio sink while the microphone is open, then restores that exact level. It stays off by default. * fix(test): keep pytest.approx working when numpy is mocked Other test modules replace sys.modules["numpy"] with a MagicMock at import and leave it there. pytest.approx then crashes on isinstance(..., numpy.bool_). * fix(audio): restore every channel and do not duck if it cannot be undone A failed restore-point write no longer lowers the sink. PulseAudio puts each channel back, and a dead microphone restores playback instead of waiting for an explicit stop. The recognition test patches the real audio module when an older test left a mock behind. * fix(test): put the audio-feedback mock back after the ducking test The recognition test has to patch the real module, but later tests still expect the mock that test_text_injector installs. Restore that mock when the test ends, and annotate the new helpers. * fix(test): annotate the playback-duck tests AGENTS.md requires a type hint on every new function. These tests were the ones still missing them. * fix(audio): skip arming playback duck after recording already failed * chore: retrigger review after rebase |
||
|
|
12e440ce1c |
feat(text-injection): let config.json pin the injection backend (#649)
* feat(text-injection): let config.json pin the injection backend (#476) Autodetection has to infer whether IBus commits actually reach the focused application, and it cannot verify that: commit_text() reports success even when the compositor drops the text. When the inference is wrong the failure is silent, and dictation appears to work in XWayland windows while doing nothing in native ones. VOCALINUX_FORCE_BACKEND already provides an escape hatch, but it is an environment variable: it covers a single run, is undocumented outside the test suite, and a user whose compositor is misdetected has to wrap the launcher in a shell script to make the choice stick. Add a persistent `text_injection.backend` setting alongside it, accepting the same ibus/wtype/ydotool/auto values. The environment variable still wins so a backend can be A/B-tested for one run without touching the saved configuration, and the startup log now names which of the two pinned the choice so a stale setting is diagnosable from --debug output. Reads the file directly rather than through ConfigManager, keeping this package independent of the UI layer, exactly as _should_copy_to_clipboard already does. An unreadable or corrupt config falls back to autodetection instead of blocking startup, and an unrecognised value is ignored with a warning so a typo cannot silently pin the wrong backend. Defaults to "auto", so existing installs behave exactly as before. * fix(text-injection): tell an explicit VOCALINUX_FORCE_BACKEND=auto from unset The variable is meant to override the saved text_injection.backend pin for a single run, which is what makes the two paths A/B-testable without editing config. Setting it to auto -- the value that means "autodetect this time" -- did not do that: the reader collapsed unset and an explicit auto into the same "auto" return, so the caller read both as "nothing was forced" and fell through to the saved pin. The pin won, and there was no way to ask for autodetection for one run short of editing the config file the variable exists to avoid. Read the variable as three states instead: unset, an explicit auto, or a backend name. Only the unset state consults config, so an explicit auto now short-circuits to autodetection. An unrecognised value is deliberately treated as unset rather than as auto. A typo means the user failed to override their preference, not that they asked to discard it, so falling through to a valid saved pin is the kinder reading. _forced_backend() keeps its signature and its collapsed return, so its existing callers and tests are untouched; the new three-state reading lives alongside it and is what the preference resolver consumes. * feat(text-injection): accept an xdotool pin and warn when ibus overrides the denylist Two gaps in the pinnable backend set. xdotool was not accepted, even though on X11 it is the tool that actually does the typing. Users were told to pin a *Wayland* backend to mean "not IBus", and get xdotool as a side effect. Naming it directly is what they were reaching for. On Wayland it needs no special handling: the value skips IBus like any other non-ibus pin and falls through to the existing XWayland fallback, so no branch is added to the selection chain. The accepted values now live in one tuple, because four places listed them -- both readers and both "expected ..." messages -- and a fifth value would have had to be added to all four. The two messages derive from the tuple, so they cannot fall behind it. Pinning ibus also silently bypasses the compositor denylist. That check exists because a denylisted compositor makes IBus report a commit as delivered while the text never arrives (#478, #485), so a pin is the one setting that can put a user back into a failure with no symptom other than nothing happening. The pin is still honoured -- an override that ignores you is worse than one that warns -- but it now says so at warning level, names the compositor, and gives the remedy. It stays quiet when the compositor genuinely bridges IBus, including a denylisted one running the ibus-wayland bridge, and on X11 where the denylist does not apply. The check is evaluated once. The guard chain short-circuits on the pin, so a pinned run never reaches the existing compositor branch, and this is its first and only call -- worth knowing because the check can shell out to pgrep or gdbus. A test asserts the call count so that stays true. Documents xdotool in the value table and rewrites the X11 paragraph, which described xdotool as the implicit result of pinning something else rather than as a value you can name. * fix(text-injection): say so when the pinned backend is not the one in use A pin could be quietly ignored. Pinning a tool that is not installed fell through to autodetection and typed with whatever it found, under a log line that had already announced the pin was overriding autodetection. Pinning xdotool on Wayland did nothing at all, since the selection chain reaches ydotool and wtype first. Pinning ibus where IBus support is unavailable skipped the whole IBus branch. In each case the user set a preference, saw no complaint, and got something else. These are one condition, not three: whatever ended up doing the typing is not what was pinned. The check compares the pin against the backend read back from final state, so a cause nobody has thought of yet is covered without another branch. It runs at the end of construction rather than inside _check_dependencies, which returns early on two paths and which the wtype probe can still override afterwards. The reason clause is added only where a generic check answers it -- the binary is absent, or IBus is unavailable -- and omitted otherwise rather than guessing. Reporting the source separately was letting the environment variable and the config setting disagree. _check_dependencies resolved the backend, then re-derived which source had won by asking again -- so the environment was parsed twice per construction, and a malformed value was reported to the user twice. Resolution now returns the backend and its source together, which fixes the duplicate warning and means the source named in a log line is the one that actually chose the backend. _backend_preference() keeps its signature and reads the first element. Two log lines also claimed VOCALINUX_FORCE_BACKEND regardless of where the pin came from, so a config-set backend was reported as an environment variable the user had never set. They now name the real source. The new warning is a startup diagnostic, not a live invariant: a tool switched later by _try_recover_from_fallback is not reported again. Noted in the docstring so the boundary is deliberate. * docs(user-guide): file the injection-backend section under Troubleshooting The section sat under Customization, directly beneath GPU Acceleration -- where the paragraph immediately above it reads "To check which backend is being used" about Vulkan and CUDA. The word means something else there, and two adjacent sections using it for different things is a bad place to explain either. It also is not a customization option. It exists because a compositor can make IBus report a commit as delivered while nothing arrives (#478, #485), and someone hitting that reads Troubleshooting, not Customization. The section already opens on the symptom, so it needs no rewriting to belong there. Pure reorder: no wording changes, and the heading text is unchanged so any existing link still resolves. * test(text-injection): cover the pin paths that skip the selection chain _check_dependencies had three uncovered branches, all reachable by pinning a backend and none of them exercised: the two early returns that hand over to IBus before a tool is chosen, and the daemonless-ydotool fallback a wtype pin lands on when wtype is absent. The early returns matter most, since the pin check runs at the end of construction precisely so those paths cannot skip it, and nothing proved that. Adds the ydotool counterpart to the missing-binary case, so a check keyed on one tool name cannot pass by coincidence; an X11 test for the guide's claim that any non-ibus pin turns the IBus path off, with a control asserting IBus is constructed without a pin so the assertion is the pin's doing; and an end-to-end explicit-auto case, which until now was only proven at the resolver rather than through selection. The pin log line said it was "overriding backend autodetection" before anything had been checked. An unavailable pin falls through to autodetection, so that line announced an outcome it could not know and then read as contradicting the "was not applied" warning. It now states the request only. _run_with_pin gains ydotoold_ready, defaulting to the previous behaviour so existing callers are unchanged. * fix(text-injection): report a config file the backend pin cannot be read from A corrupt config.json disappeared into logger.debug. The setting is hand-edited -- that is the workflow the user guide documents -- so a stray comma is a likely way to reach this, and staying quiet left the user with autodetection and the silent IBus miss the pin was set to avoid, with nothing to tell that apart from the pin not working. Unknown values already warned; an unusable file now does too, naming the file and the parse error. The bare handler was also catching AttributeError by accident. Valid JSON that is not an object -- a bare list, string, number or null -- reaches the lookup and raises there rather than at parse time, and so does a text_injection section that is not an object, where the top level is a mapping and the section is not. Both are now shape checks rather than exception handling, which is what lets the handler narrow to OSError and ValueError without those five shapes escaping it. Locating the file moves out of the try. It fixes the handler being unable to name the path it failed on, and it is a deliberate boundary: config_dir() resolving a path is not a failure this should paper over, so it is no longer guarded here at all. A missing config.json stays silent, which is the default install. * test(text-injection): stop the ibus-pin tests depending on an installed ydotoold The bridged-compositor and X11 cases asserted that a run logs no warnings at all. That is broader than what they are about, and _run_ibus_pin patches shutil.which to claim every tool exists without patching the probe that then shells out to one, so the real _ensure_ydotoold ran and warned whenever /usr/bin/ydotoold was absent. The tests passed on a machine that happens to have it installed and failed on the CI runners that do not. The assertions now name the warning they are about, and the helper patches _ensure_ydotoold the way its sibling _run_with_pin already did, so nothing in this class of test reads the host's installed packages. * fix(text-injection): report pending IBus initialization state * docs(text-injection): clarify backend pin behavior * fix(config): preserve a hand-edited backend across unrelated saves ConfigManager writes its full in-memory configuration on every save. A user who edits text_injection.backend in config.json while Vocalinux is running therefore loses that choice when an unrelated Settings or tray save writes the stale default back. Remember only the backend value observed at load or after a successful save. If the in-memory value is unchanged, preserve the current value from disk; if it changed through ConfigManager, treat that as the intentional write. Missing and explicit auto both reset to auto. The save aborts rather than overwriting a file whose JSON or text_injection shape cannot be read safely. This is deliberately per-key: no recursive configuration merge or injection-layer dependency is added. * style(text-injection): remove redundant typing suppression * fix(config): keep invalid UTF-8 config from blocking startup * fix(config): preserve malformed backend sections on unrelated saves * fix(config): retain text setting changes after malformed config * test(text-injection): annotate backend pin test helpers |
||
|
|
be77ea40f4 |
fix(settings): keep the download dialog visibly working after the last byte (#864)
The transfer ending is not the dialog's work ending. The checksum still has to run and the engine still has to load the file, seconds of it on a large model, and until now that window showed a bar parked at 100% with the downloader's "Complete!" under it and Cancel as the only button. It reads as a finished download waiting to be confirmed, or as a hang, and the one button on offer is the opposite of what the user wants to press. - ModelDownloadDialog: a fraction of 1.0 no longer paints a finished-looking bar. It bounces the bar, names the step, says the download is done but the work is not, and greys out the Cancel button, which cannot interrupt verification or a model load anyway. Only set_complete() says "Complete!" and offers OK. - whisper.cpp: report "Verifying model..." before hashing a file that can be hundreds of megabytes, the same step the Vosk path has always reported, and "Loading model..." before backend detection and the load. The tray's download notification reads the same statuses. - tests: add return annotations on the new finishing-feedback helpers and tests (Greptile P2). Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
244b62c8ad |
refactor(installer): split implementation into sourced modules (#862)
* refactor(installer): split implementation into sourced modules * test(installer): harden module path coverage * fix(installer): qualify interactive privacy and engine claims The guided installer overstated local-only privacy and ranked whisper.cpp without evidence. Match PRODUCT.md: on-device local engines, optional Remote API, and the ~33 language catalog plus Auto-detect. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
38e5be70ba |
feat(parakeet): add optional verified Orukeet model (#840)
* feat(parakeet): add optional verified Orukeet model * fix(parakeet): recover from invalid release manifests * chore: mark Orukeet tip verified Empty signed tip so the ready-bar Verified gate clears on contributor unsigned history. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
7bdbedce0b |
feat(language): follow the active keyboard layout while dictating (#837)
* feat(language): follow the active keyboard layout while dictating Adds a "Follow keyboard layout" switch to Settings -> Speech Model -> Advanced. With it on, the active keyboard layout is re-read at the start of every dictation and the matching language is used, so someone who switches layouts to work in two languages gets the right one without opening Settings. #796 already seeded the language once on first run, but from the *configured* primary layout, which never changes as the user switches between them: a "fr,us" setup reports fr forever. detect_active_keyboard_ layout() reads GNOME's input sources instead. It takes mru-sources rather than the current index, because that index goes stale -- observed still reading 0 after a switch to the second layout -- which is why the IBus injection path already trusts the MRU list (#497, #738). An active source that is an IBus engine rather than an xkb layout is a real answer that cannot be mapped, so the language is left to the engine's own auto-detect. Borrowing the configured xkb layout there would pin English for someone typing Japanese through mozc-jp. The mode is a switch over the language picker rather than an entry inside it, since it is a mode and not a language. While on, the picker is disabled and shows what the layout resolves to; turning it off pins that. A language change no longer re-initialises the engine when the loaded model takes the language per utterance, which is the whole whisper family, so following a layout costs a settings read rather than a model reload. VOSK loads one model per language, Parakeet ignores the picker, and an English-only model has no non-English weights, so Settings refuses the mode for all three rather than offering a switch that does nothing. Fixes #821 * fix(language): keep follow mode armed and map IBus engines Swap English-only whisper.cpp weights to the multilingual sibling while passing the layout sentinel into reconfigure, so follow mode stays armed. Map common GNOME IBus engines onto catalogue languages instead of leaving them unmapped. * fix(language): stop IBus hyphen false-maps and gate the sibling swap The last-token hyphen heuristic mapped mozc-us and foo-us to English. Restrict it to *-jp/*-kr and name Mozc's extra engines as Japanese. Only whisper.cpp may consult the whisper.cpp download catalog. * fix(language): keep multilingual weights while following layout The picker shows the active layout while follow is on, often en-us. Deriving whisper.cpp .en from that display made Settings refuse the mode as English-only, then the next auto-apply persisted a concrete language and disarmed follow. Use LANGUAGE_FOLLOWS_LAYOUT for .en vs multilingual derivation instead, including on the load path before the switch is synced. * fix(language): type follow helpers and log sibling-swap failures * fix(test): stub follow-layout model rec so mocked psutil does not blow up follow-mode English-only guard calls get_recommended_whispercpp_model, which reads RAM through psutil. Full CI leaves a MagicMock in sys.modules["psutil"], so ram_gb >= 16 raises TypeError. Patch the settings_dialog alias to return a multilingual tiny rec so the assertion still holds without real RAM. * fix(language): catch specific errors on layout lookup and sibling swap * fix(language): swallow more sibling-swap reconfigure failures ImportError, TypeError, AttributeError, and ChecksumError from the multilingual swap must not abort dictation or disarm follow mode. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
9646011e77 |
fix(settings): speech model follow-ups from QA (#836)
* fix(settings): speech model follow-ups from #834 Keep the Unused downloads expander in sync with search, recommend Faster Whisper .en weights for English, parse nvidia-smi MiB VRAM, stop simple mode from clobbering Faster Whisper/Parakeet, and ceil RAM so ~7.4 GiB counts as 8. * fix(settings): keep FW .en picker ids and test unused search Represent Faster Whisper English catalog variants in the Settings size combo so tray-saved small.en is not rejected and overwritten. Add behavioral unused-downloads search coverage for match/nonmatch/clear. |
||
|
|
06f049ac76 |
fix(shortcuts): record unmapped F19/F24 and XF86-aliased F13–F23 (#844)
* fix(shortcuts): record unmapped F19/F24 and XF86-aliased F13–F23 * fix(shortcuts): keep split-keyboard modifiers while recording The recorder only looked at the keyboard that sent the function key, so a modifier held on the other half was dropped. A dead device also stayed in the watch list, which left F19/F24 uncapturable until Record was restarted. Union held keys across every open keyboard before committing, and drop a device on hangup or read error while scanning for a replacement. * fix(shortcuts): keep capture tests off real input devices CI opened real /dev/input paths because the harness patched a stale evdev module, so io watches never attached. Patch the live module, and move the capture device protocol to common_types. * fix(tests): patch the recorder's own GLib globals test_settings_dialog reimports the dialog module and leaves the replacement in sys.modules. Recorder methods still close over the original dict, so the harness never recorded timeouts or io watches. |
||
|
|
a38cff59ea |
fix(vosk): refuse to load a model that would exceed available memory (#850)
* fix(vosk): refuse to load a model that would exceed available memory vosk.Model() allocates the whole model in one spike during loading, including the RNNLM rescoring component some models ship, with no way to check the requirement up front or load incrementally. Walk the model directory, compare its on-disk size against psutil's available memory with a 1.5x margin, and raise before calling into the native loader instead of letting the kernel OOM killer take the process down mid-load. Fixes #676 * fix(vosk): match memory safety margin to the documented peak greptile flagged the guard's own comment: loading can spike to ~2x the model's on-disk size, but the margin only required 1.5x available memory, so a load between those two multiples could still trigger the OOM this guard exists to prevent. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
74d6f505d4 |
fix(update): fall back when GitHub API rate-limits (#846)
Shared NAT hits the unauthenticated api.github.com quota and the update check logged a WARNING that looked like a failure. On 403 or 429, look up the latest stable tag from the releases page JSON, and stay quiet if that also fails. Nightly still refuses to treat the stable latest tag as a nightly. Fixes #845 |
||
|
|
5b4e2c3572 |
fix(snap): add hardware-observe so hotkeys can read input devices (#858)
Snap confinement denies /proc/bus/input/devices unless hardware-observe is connected. Declare the plug, tell users to connect it next to raw-input, and update the in-app hints. Fixes #857 |
||
|
|
acd90fdd05 |
fix(installer): install hash-pinned dependencies and build tools (#856)
* fix(installer): install hash-pinned dependencies and build tools * fix(installer): add cairo-devel for the pinned pycairo build * fix(installer): reject missing or empty bootstrap exports * fix(installer): preserve remote behavior for older tags --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
4df612dd7a |
fix(settings): drop Test Dictation scrolled-window stripe (#853)
* fix(settings): drop Test Dictation scrolled-window stripe GTK3 ScrolledWindow defaults to an IN etched shadow, which GNOME/Wayland renders as a thin vertical stripe on the left of the Test Dictation revealer even when the buffer is empty. Call set_shadow_type(NONE) to match the other ScrolledWindows in this dialog. Fixes #847 * Add missing test return annotation |
||
|
|
dd3bd69add |
Pin the ydotoold probe in the wtype fallback tests (#841)
* test(injection): pin the ydotoold probe so local runs match CI * chore(pytest): filter the three known warnings out of the summary * fix(pytest): stop hiding save_settings deprecation globally Keep the ignore on the one test that still exercises the deprecated alias so the rest of the suite can surface new save_settings() calls. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
923bc42708 |
fix(ci): verify a release whose run failed after publishing (#842)
* fix(ci): verify a release whose run failed after publishing * fix(ci): only forgive a confirmed 404 when verifying a published release |
||
|
|
c7b8cd4802 |
fix(ibus): stop the xkb:us::eng guard flipping GNOME/X11 to a US layout (#827)
The gsettings fallback for the bogus xkb:us::eng default only fired on GNOME/Wayland; on GNOME/X11 the default is accepted and written back into the X keymap, permanently switching the user off their real layout (Hungarian, Brazilian ABNT2, etc). _get_gnome_current_source() also had no fallback when mru-sources is empty (a fresh session with a single input source never populates the MRU list), so even with the guard fixed the recovery path still returned None. It now falls back to the static sources list. Fixes #699 |
||
|
|
1f121546fb |
fix(release): attach the snap to GitHub Releases (#839)
* fix(release): attach the snap to GitHub Releases v0.17.0 packed the snap, then Store review of uinput failed the job, so the file never reached the tag. Build and attach first. A Store hold is a warning, not a failed release. SHA256SUMS covers the .snap. snap-backfill.yml rebuilds an existing tag, appends the snap to the manifest, and attests only that file. * fix(release): keep snap tags out of the shell and fail real Store errors Pass the tag through the environment like verify-release.yml. Treat only an allow-installation review as a successful Store publish. snap-backfill runs verify_release.py after it rewrites SHA256SUMS. * style(test): format snap-backfill shell-injection guard with black |
||
|
|
26f90b6998 |
chore(release): prepare v0.17.0
Minor on the stable line. Faster Whisper and Parakeet extras, Speech Model simple setup, Snap uinput, Flatpak bundles on the tag, and QA fixes from the release branch. Fixes tracked in the PR. Post-release follow-ups: #834. |
||
|
|
b90bdb075d |
fix(deps): export and cap every engine extra (#828)
* fix(deps): export and cap every engine extra * fix(deps): docstring main and tighten export pairing tests Give check_exports.main the required public docstring, annotate the dependency export test helpers, and require --extra to sit with its -o on the same just lock line (whisper pip-compile stays special-cased). --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
092dde105e |
fix(audio): open 3-8ch HDA mics at native layout (#813) (#829)
* fix(audio): open 3-8ch HDA mics at native layout (#813) HDA analog capture often reports 4 channels. Opening below that native count can abort PortAudio after read with free(): corrupted unsorted chunks (same class of bug as #666/#673). Try the reported channel count first when it is between 3 and 8, then 2ch and 1ch. Pulse devices that advertise 32/128 channels still open stereo-first. Downmix N channels to mono so engines keep seeing 1ch. Virtual-device filtering for pipewire/default (#629) is unchanged. * fix(audio): downmix HDA capture from first channel pair Averaging all N HDA channels halves speech when only the front pair is live. Keep stereo mean-of-both, and for 3+ channels average channels 0 and 1. * fix(audio): pick loudest channel for N-ch downmix First-pair-only downmix silenced mics on ch2+. For 3+ channels, select the loudest channel by per-buffer mean-square so speech stays at full level whether it lands on the front or rear pair. Stereo still averages both channels. * fix(audio): sticky N-ch downmix channel for open stream Pin the loudest-channel pick for the life of a capture stream so a noise burst on another input cannot switch the mic mid-utterance. * fix(audio): gate sticky downmix lock on speech energy Don't pin the N-ch sticky channel from a silent/ambient first buffer; only lock once the loudest channel clears a documented mean-square floor. * style: isort sticky-lock import in device config tests |
||
|
|
5e8a08fbb8 |
fix(ui): follow OS dark/light on native GTK installs (#830)
* fix(ui): follow OS dark/light on native GTK installs AppImage already maps portal/gsettings color-scheme into GTK_THEME. AUR and install.sh use system GTK3, which ignores prefer-dark. Set gtk-application-prefer-dark-theme from the same signals, and skip when GTK_THEME is already set so AppImage and user overrides stay in charge. Fixes #816 * docs: portal no-preference still checks gsettings The helper already falls back after a portal 0 or a failed Read. The module doc said that left GTK defaults, which is only true after gsettings also comes up empty. |
||
|
|
79df154292 |
ci(flatpak): restore previous builder cache and skip src-only PRs (#825)
The builder action restore-keys is `flatpak-builder-${arch}`. Our key
was `flatpak-builder-${hash}`, which the action stores as
`flatpak-builder-${hash}-x86_64`. That does not start with
`flatpak-builder-x86_64`, so a packaging hash change (a lock refresh,
or merging one) discarded the whole .flatpak-builder cache and
recompiled numpy and pywhispercpp from source, ~18 min instead of ~4.
Put ${{ matrix.arch }} in the key first so the fallback matches. Then
flatpak-builder skips unchanged modules and only rebuilds what the
hash change actually touched.
PRs no longer watch src/**. python-test already covers those, and the
expensive compile does not. push to main still does, so a sandbox
install break fails the default branch.
|
||
|
|
d5656b8060 |
fix(text-injection): route XWayland xdotool fallback through clipboard paste (#680)
* fix(text-injection): route XWayland xdotool fallback through clipboard paste xdotool type simulates keypresses against the active X keyboard layout, so a character the layout doesn't map comes out as the wrong glyph or garbled (#657). The native ydotool path already avoids this by pasting via the clipboard instead of typing (_inject_with_wayland_tool); the XWayland xdotool fallback (_inject_with_xdotool) never got the same treatment. Prefer clipboard paste on that fallback too when ydotool is installed, falling back to raw xdotool type only when no paste tool is available or the paste itself fails. Fixes #657 * fix(text-injection): X11 clipboard paste for the XWayland fallback jatinkrmalik's review on #680 pointed out the AppImage build does not bundle ydotool, so the prior fix's ydotool clipboard paste never ran on the actual reported setup; it fell through to the same layout-dependent xdotool type as before. It also flagged a real paste-target mismatch: an XWayland window reads the X11 CLIPBOARD selection, not the Wayland clipboard wl-copy/ydotool write to. The XWayland fallback now copies via xclip/xsel and pastes with xdotool key --clearmodifiers ctrl+v when either is installed, waiting for held modifiers to release first (the same wait already used by the native ydotool path). ydotool clipboard paste stays as a second fallback when xclip/xsel are absent; xdotool type is the last resort. New regression test drives inject_text() end-to-end with no ydotool on PATH, reproducing the reported AppImage/GNOME log rather than calling the fallback method directly, so it also covers the recovery check that could otherwise skip this path. * fix(text-injection): thread x11_only through clipboard read/clear so restore matches the paste backend jatinkrmalik's rebase pulled in the layout-aware paste work from #788, and greptile's review on the merged branch caught a real gap in this PR's own XWayland path: _copy_to_clipboard already takes x11_only to write the X11 CLIPBOARD via xclip/xsel (since that is what an xdotool paste actually reads), but _read_clipboard and _clear_clipboard never gained the same parameter. On a Wayland host, _read_clipboard prefers wl-paste, so the "previous clipboard" saved before an XWayland paste could be read from an unrelated Wayland clipboard, and restoring it back through _copy_to_clipboard with no x11_only wrote to that same wrong backend, leaving the X11 CLIPBOARD stuck on the dictated text. Threads x11_only through both helpers and every restore call site (the immediate-failure path and the delayed _restore closure), so an XWayland paste captures, clears, and restores the same X11 CLIPBOARD selection it wrote to. * fix(text-injection): annotate clipboard helpers and XWayland paste tests Greptile P2 on #680: give `_get_clipboard_tools` a return annotation and mark the new XWayland clipboard-paste regression tests `-> None`. * fix(text-injection): derive XWayland paste from environment Drop the ydotool/wl-copy second fallback on WAYLAND_XDOTOOL (wrong clipboard, and rc=0 is not a paste) and the x11_only flag stack. Paste picks xclip/xsel + xdotool from environment; user-facing copy stays on the session clipboard. * fix(text-injection): always pass host_env() to the paste chord The AST guard only recognizes a direct host_env() call. The ternary around it looked like we were handing the AppImage library path to xdotool. * test(text-injection): give XWayland injector stubs clipboard restore state _inject_with_xdotool now pastes first, and that path reads _clipboard_restore_target. The __new__ helper in test_text_injector_ext never set it. * fix(text-injection): skip clipboard restore on a stale paste failure If a newer paste has already copied its text, the older paste's failure handler must not write its saved clipboard back. Success restores already honored the generation counter; the failure path did not. * fix(text-injection): keep original clipboard across a stale paste failure Publish the restore target as soon as copy succeeds (first writer wins) so an overlapping paste inherits it. If the older paste then fails, the newer restore still writes the pre-first clipboard, not intermediate dictated text. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
4b6e81d441 |
fix(snap): type into native Wayland apps with ydotool (#823)
* fix(snap): inject into native Wayland apps via ydotool Edge rev 7 only ships wtype and xdotool. GNOME rejects the virtual keyboard protocol, so the snap falls back to xdotool and types only into XWayland windows. Native GTK/Qt apps get nothing. Stage ydotool, request the uinput plug, and prefer ydotool when wtype is rejected. Distro ydotool 0.1.x still needs a writable /dev/uinput; log `snap connect vocalinux:uinput` when the Snap cannot open it. * docs(snap): v0.16.2 edge has no uinput plug The store snap (0.16.2, latest/edge rev 7) does not declare uinput. `sudo snap connect vocalinux:uinput` fails with "has no plug named uinput" and dictation only reaches XWayland apps. Spell that out in README, INSTALL, UPDATE, the website changelog, and the store copy so people do not run the 0.17 connect command against rev 7. * docs(snap): drop the unconfirmed 0.17 version from uinput notes Greptile P2: the tree is still 0.16.2 and the release process does not name the next tag. Tell people to refresh until `snap info` lists the uinput plug, instead of waiting on a version we have not cut. |
||
|
|
70d5ce7688 |
fix(snap): put gnome-platform first so SVG icons load (#822)
core24 desktop-launch rebuilds gdk-pixbuf-loaders.cache from the first LD_LIBRARY_PATH directory that ends in the arch triplet. Stage-packages drop a gdk-pixbuf tree without libpixbufloader_svg.so at $SNAP/usr/lib/<triplet>, ahead of gnome-platform, so About-page SVGs fail with "Image type svg is not supported". Fixes the same core24 GTK3 gap documented at: https://forum.snapcraft.io/t/missing-svg-support-for-gtk3-in-core24/48851 |
||
|
|
1fac26ff07 |
fix(packaging): derive the Flatpak and AUR dependency sets from the lock (#819)
* fix(packaging): derive the Flatpak and AUR dependency sets from the lock * test(packaging): add return annotations on new packaging tests Greptile asked for -> None on the new tests, matching AGENTS.md. * test(packaging): type parametrized Flatpak source arguments Greptile asked for type hints on the source parameter, matching AGENTS.md. --------- Co-authored-by: jatinkrmalik <jatinkrmalik@gmail.com> |
||
|
|
4d8c58f536 |
feat(shortcuts): allow bare function keys for push-to-talk (#815)
* feat(shortcuts): allow bare function keys for push-to-talk Accept F1–F24 shortcuts without a modifier so dictation can use a dedicated key (e.g. F10) instead of Shift/Ctrl/Alt combos that fire while typing. Settings shortcut capture records bare function keys too. * fix(shortcuts): evdev availability for bare function keys Check EV_KEY capabilities for bare F1-F24 shortcuts instead of an empty modifier name. Add availability regression tests and type hints on new tests. * refactor(shortcuts): fold bare F-keys into ShortcutSpec Teach primary_modifier to return the bare key so parse_shortcut and the backend stop special-casing empty modifiers. Capture now asks the parser instead of re-encoding the F-key allow-list, and evdev shares one capability probe for modifiers and main keys. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
b45a26e1cd | feat(ci): run install.sh in the distro matrix instead of grepping it (#810) | ||
|
|
94c2eaac08 |
fix(tray): make PTT tray icon turn red on every hold (#809)
* fix(tray): refresh PTT icon when AppIndicator reuses a path Right-Alt push-to-talk turned the tray red only on the first hold. Later holds dictated fine but left the icon gray until restart. StatusNotifier hosts often skip a redraw when IconName returns to an absolute path already used in the session. Nudge IconThemePath before set_icon_full so gray-red-gray-red actually turns red again. * test(tray): assert alternating IconThemePath nudge args Greptile P2: the PTT redraw regression only checked set_icon_theme_path call count, so identical paths would still pass. Assert the alternating ICON_DIR / ICON_DIR+os.sep arguments that drive the host reload. |
||
|
|
ab1400b7c1 |
fix(injection): read WM_CLASS with xprop so xdotool stops dumping core (#807)
* fix(injection): read WM_CLASS with xprop so xdotool stops dumping core
`xdotool getwindowclassname` hands an uninitialized XClassHint to XFree
when the window carries no WM_CLASS, so it aborts with SIGABRT
("free(): invalid size") and leaves a coredump behind. On KDE Plasma
Wayland that is the normal case, not an edge case: while a native
Wayland client holds focus, KWin points XWayland's _NET_ACTIVE_WINDOW at
a property-less window, and `xdotool getactivewindow` reports it happily.
Every paste-shortcut probe therefore killed a child process and filled
the journal with coredumps. Injection still worked, because the abort
only made the probe return an empty class, so the symptom was silent
except in `coredumpctl`.
read_wm_class() now reads the property with xprop, which handles a
missing WM_CLASS as an ordinary empty result, and falls back to xdotool
only when xprop is absent and the window looks like a real client (it has
a pid or a title). The debug window-info logging in text_injector.py goes
through the same helper.
Reproduced on Fedora 44, Plasma 6 Wayland, xdotool 3.20211022.1:
`xdotool getwindowclassname 0x200000` exits 134 on the window that
`_NET_ACTIVE_WINDOW` points at.
* fix(injection): keep xdotool classname for class-only X11 clients
When xprop is missing, always allow the xdotool classname fallback so a
real terminal that exposes WM_CLASS but no title or _NET_WM_PID still
classifies as a terminal (Ctrl+Shift+V). xprop remains the primary path,
so Plasma Wayland with xprop never calls getwindowclassname.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
|
||
|
|
cdfe541c25 |
feat(speech_recognition): add faster-whisper engine backend (#543)
* feat(speech_recognition): add EngineType enum and Engine Protocol Introduce a typed EngineType enum and an Engine Protocol in common_types.py, plus an engines/ registry so new speech recognition backends can be plugged in without scattering string comparisons across the manager. * feat(speech_recognition): add faster-whisper engine backend Add a faster-whisper engine implementation using CTranslate2/INT8 for fast CPU inference. Includes model metadata utility, wiring through the recognition manager, CLI, settings UI, config, optional dependency, and unit tests. Also updates the suspend-handler test fixture for the new engine attribute. * style(ui): format faster-whisper imports for isort 8 * test(speech_recognition): expand faster-whisper coverage for codecov * test(speech_recognition): fix failing faster-whisper coverage tests - Import missing get_compute_type helper - Add tests for English-only detection, cache exceptions, and import-error registry path - Clear _has_torch_cuda lru_cache around exception test to avoid cached True - Replace broad builtins.__import__ patch with a fake engine module for registry test - Restore registry module state after test * test(speech_recognition): add coverage for faster-whisper edge cases Cover previously missing lines in: - faster_whisper_model_info.py: HF cache path/none, hf_hub lookup, fallback traversal, torch CUDA true, psutil exception - faster_whisper_engine.py: model None guard after is_ready() - engine registry import-error path * feat(install): add faster_whisper engine option to install.sh - Add faster_whisper to --engine help text and examples - Add faster-whisper as interactive option 5 with description - Install faster-whisper package and verify import at install time - Add fallback to whisper.cpp if faster-whisper installation fails - Add smoke test for faster-whisper import - Update welcome message engine display name - Note: faster-whisper models download automatically on first use * refactor(faster_whisper): apply ponytail review cleanups - Inline FASTER_WHISPER_MODEL_INFO dict literal; drop unused AVAILABLE_MODELS/MODEL_SIZES - Reuse _has_torch_cuda() in FasterWhisperEngine.device property - Remove redundant model None guard after is_ready() verification - Shrink faster-whisper default config in install.sh to engine + model_size - Update device tests to manage _has_torch_cuda lru_cache - Remove test for deleted dead-code guard * fix(faster_whisper): integrate optional engine with current main Keep whisper_cpp as default and Parakeet as an extra. Put engine selection in the Advanced island, install faster-whisper via the optional extra, and refuse unpinned Hugging Face downloads until model_checksums.txt pins them. * fix(faster_whisper): download verified models and refresh uv.lock Settings already opened a download dialog for uncached faster-whisper, but init only raised ChecksumError. Wire a Parakeet-style downloader that stores bundles under models_dir, verifies each file against model_checksums.txt, and loads WhisperModel from that path. Regenerate uv.lock for the optional faster-whisper extra, pin digests for all eleven Systran models, and add the missing -> None return annotations. * fix(faster_whisper): clear engine on unload and fix auto-pause fixture Auto-pause tests stub __init__, so they never set _faster_whisper_engine. reinitialize_after_resume then AttributeErrors once Test Python runs. Mirror the suspend-handler fixture, and release the faster-whisper engine inside unload_model the same way resume already does. * fix(faster_whisper): restore previous engine after a failed switch reconfigure() selected the new engine and released the live model before init. A failed faster-whisper download then left the manager in ERROR on the new backend while Settings reverted the pickers to the saved engine, so dictation stayed dead until restart. Reload the previous engine from disk (no download) before re-raising so the UI and live manager agree. * fix(speech_recognition): restore all live settings after failed switch Failed reconfigure already put the previous engine back, but VAD, device, remote API, and whisper.cpp knobs stayed on the attempted values while config on disk did not. Snapshot every field reconfigure writes and restore the lot before reloading the old backend. Also bind the real _init_selected_engine onto the MagicMock in the force_reinit test. * fix(speech_recognition): restore every live field after failed reconfigure A failed engine switch already reloaded the previous backend, but Greptile could still miss that VAD, audio device, remote API, and whisper.cpp knobs were part of the snapshot. Keep one explicit attr list for snapshot/restore, including the derived voice-commands flag, so live state matches persisted config after a failed Settings apply. * chore(speech_recognition): sign tip after rollback follow-up |
||
|
|
7664266823 |
feat(commands): localize punctuation voice commands (#640) (#642)
* feat(commands): localize punctuation voice commands by language Voice command phrases were English-only even when recognition language was set to Italian, French, etc. Merge locale punctuation/line-break aliases from the recognition language into CommandProcessor, keep English phrases available, and document the behavior. Fixes #640 Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * fix(commands): keep line breaks and French apostrophe aliases Attach-left punctuation now eats only horizontal whitespace, so a localized "new line" followed by a period or comma still inserts a newline. French aliases include ASCII, U+2019, and backtick apostrophes, and longest-match still wins so "point" cannot fire inside "point d'interrogation". --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
f90e4d5765 |
fix(injection): release ydotool Ctrl after a timed-out paste (#675)
subprocess.run(..., timeout=3) SIGKILLs the ydotool client. ydotoold keeps any keys it already applied, so virtual Left Ctrl can stay held until logout. Send a best-effort V-up/Ctrl-up (or a legacy ctrl tap) after paste failure. Fixes #658 |
||
|
|
2c6acbc15e |
fix(inject): layout-aware clipboard paste for Neo and friends (#788)
* fix(inject): resolve paste chord for non-QWERTY layouts ydotool was sending physical KEY_V (47) for Ctrl+V. On German Neo that key types p, so paste opened Print instead. Prefer wtype keysyms when usable; otherwise resolve the evdev key for Latin v via xkbcommon, and read KDE kxkbrc so Plasma Wayland Neo users are covered. Fixes #787 * fix(inject): read Plasma active layout from real session sources kxkbrc has no CurrentLayout/LayoutIndex, so multi-layout Neo always fell back to us at index 0 and paste still hit KEY_V. Prefer D-Bus getLayout or layout_memory.xml for the active layout, keep kxkbrc for the list only, and prefer KDE over leftover GNOME on Plasma. |
||
|
|
d07c70dbc9 |
fix(audio): stop sink-wake from clipping start/stop cues (#804)
* fix(audio): prepend silent preroll so sink-wake does not clip cues Short catalog tones start in a fresh paplay process while PipeWire is still waking a suspended sink, so the first tens of milliseconds of the cue are missing. Prepend 100ms of silence in the same WAV, cache the result by source path, mtime, and preroll length, and fall back to the original file if the preroll cannot be built. CI mock players keep the raw path. Fixes #800 * fix(audio): cache sink-wake preroll without tempfile.mkstemp Some tests replace sys.modules["tempfile"] with a MagicMock, so mkstemp unpacking fails and playback falls back to the raw cue. Name the cache file from a short sha256 of the source path, mtime, and preroll length, write a sibling .partial, then os.replace it into place. Reload real tempfile and wave in the audio feedback tests so they still pass after those mocks leak. Fixes #800 * fix(tests): pin real wave for preroll without reloading tempfile Popping tempfile out of sys.modules in the audio feedback fixture made preroll tests pass, but later whisper tests then bound TemporaryDirectory from a MagicMock. Import real tempfile and wave at module top, pin only wave while audio_feedback is reimported, and bind TemporaryDirectory from the real stdlib if a sibling test already replaced tempfile. Fixes #800 * fix(audio): catch specific preroll errors and type test helpers The new preroll path swallowed every exception, which hid bugs and disagreed with AGENTS.md. Catch OSError, ValueError, and wave.Error on the inner partial cleanup and the outer fallback so a bad WAV or cache write still falls back to the original cue, and other errors still propagate. Annotate the two test helpers the same review flagged. * fix(audio): fall back when truncated cues raise EOFError wave.open can raise EOFError on a truncated RIFF before playback. Catch it with the other preroll failures so start/stop still play the original file. Fixes #800 |
||
|
|
dcd8d06077 |
fix(asr): keep Parakeet recognition alive after decode errors (#803)
* fix(asr): keep Parakeet recognition alive after decode errors sherpa-onnx create/decode exceptions were escaping the recognition thread. Catch them like whisper.cpp, annotate the Parakeet helpers, and cover create_stream failure. * fix(asr): narrow Parakeet transcription exception handling Keep create_stream/decode survival, but stop swallowing unrelated bugs. Same RuntimeError/OSError/ValueError tuple as Whisper. |
||
|
|
affbf6a9af |
feat(settings): simple setup for the speech model, with Advanced as an island (#801)
* feat(settings): make the language picker searchable while its list is open Entry completion only ever covered typing into the closed entry. Open the dropdown and the keyboard goes to GTK's own first-letter jump, so nothing typed reaches a filter, and with thirty-odd languages scrolling is the only way through. The first attempt at fixing this (a filtered ComboBox model) had the same limitation for the same reason. Replace the combo with a picker that opens a popover holding a search entry, focused on open, above the list. Every keystroke narrows the rows, Enter takes the first match, a click takes that row. It keeps the GtkComboBoxText methods the dialog calls, mirrors the store's column order in base_model for the shared row helpers, and emits "changed" only when the selection actually changes, so re-selecting the saved language on open is not taken for a user edit. Visibility uses the matcher the typed-text resolver already uses, so the list and Enter cannot disagree. The completion helper and the combo styling skip the picker: it filters its own list, and the ComboBox-only calls do not exist on a Box. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * feat(settings): add a simple mode that derives the model from the language The Speech Model panel asked four questions, but only two of them can be answered without knowing the hardware: what language you speak and whether you want it faster or more accurate. Size follows from the machine and the specialization follows from the language, and the app already computed both — it just printed the answer beside the pickers instead of using it. Simple mode asks for the main language, whether whole texts are also dictated in other languages, and a priority. There is deliberately no second language field: whisper takes one language or none, so a pair cannot be passed, and with a language pinned the multilingual weights already handle inserted foreign words. The switch is the "none" case. Priority moves relative to what hardware detection recommends rather than naming an absolute size, so "most accurate" means something different on a laptop than on a workstation. Opening simple mode reads the priority back from the size already configured, so switching modes never changes the loaded model on its own. Simple mode steers the existing controls rather than writing the configuration itself, so applying, downloading and the info card keep going through one path. Advanced mode is the previous panel, unchanged. Refs #779 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * fix(settings): stop simple mode reloading the engine four times per pick Reported against the installed build: choosing a language locked the window for a moment. Simple mode steers the engine, language, size and variant controls, and every one of those change handlers ends in _auto_apply_settings, so a single pick reconfigured the speech engine up to four times on the UI thread, reloading the model each time. Hold a guard across the steering and apply exactly once when it is done. The simple language box was also a plain list with no entry, so it could not be typed into — a step backwards from the advanced row, and unusable with over thirty languages. It now uses the same searchable combo, with the same commit and restore behaviour for text that resolves to nothing. Refs #779 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * refactor(settings): make simple the panel and Advanced a reveal The Simple/Advanced dropdown framed the two as alternatives, which put a mode decision in front of the user before any of the questions they can answer. They are not alternatives: simple is the panel, and advanced is more detail about the same choice. The mode selector is gone. The simple questions are always on screen, with an Advanced switch that slides the engine, size and specialization rows out underneath. Because simple mode steers those rows, they also work as a readout of what the simple answers resolved to, which the old either/or hid. The stored settings_mode becomes a show_advanced flag, remembering whether the detail was left open. Refs #779 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * refactor(settings): second language list, and Advanced in its own window Reworked to what the panel is actually for. The main language comes from the keyboard layout. Turning on "I also dictate whole texts in other languages" reveals a second list rather than silently flipping to detection, so the choice is visible. Advanced is a button that opens the detailed controls in their own window, instead of an inline reveal that pushed the page around. Naming a second language resolves to automatic detection, because whisper takes one language or none. The one case where both can still be honoured is two English entries — en-US plus en-IN — where English stays pinned and the English-only weights, same size and better at English, remain available. Also fixes a name collision the reveal had: advanced_revealer already belonged to the power-user toggle on the Advanced page, and the section built later won the attribute. Refs #779 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * feat(settings): use the filtering picker for both simple language lists The main language and the second language are the same thirty-odd-row lists as the advanced one, so they get the same dropdown filtering rather than a completion popup that only reacts while the entry has focus. Refs #779 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * fix(settings): make simple mode do what it says on screen Four things reported from the installed build, all real. The second language list never appeared: the row is flagged no_show_all, and show_all() is a no-op on such a widget, so the flag has to be cleared first — the dance _set_custom_shortcut_row_visible already does. There was no multilingual answer. "Any language (auto-detect)" is now the first entry of the second list and the default when the switch is turned on, so the switch alone visibly does something. Changing the priority showed nothing, because the info card — the only surface that says which model was picked and what it costs — had been moved into the advanced window. It is back on the page under the simple questions. Opening Advanced crashed the compositor. KWin 6.7.4 (Wayland) died with unbounded recursion in KWin::Window::findModal(), a cycle in its transient-for chain, when the advanced window — a long-lived toplevel transient for this dialog, hidden on close and shown again with present() — was opened. Coredumps at 2026-09-06 20:20:10 and 20:39:49; the crashing state is kept as tag kwin-crash-repro. The window now stands on its own, is built on every open and destroyed on close with the controls re-parented out first. KDE Bug 483265. Refs #779 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * fix(settings): bring the advanced window up in front of the dialog Reported: it opened behind Settings. Dropping the transient parent (which crashed KWin) also dropped the only reason the compositor had to stack it over the dialog, and on Wayland a toplevel mapped without an activation token lands behind the focused window. Present it with the timestamp of the click that opened it: that asks the compositor for focus and stacking, and needs no transient link. Refs #779 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * refactor(settings): Advanced as an island under the simple card, not a window A second toplevel went wrong twice on KWin 6.7 / Wayland. Transient for the dialog it crashed the compositor (unbounded recursion in KWin::Window::findModal, tag kwin-crash-repro, KDE Bug 483265). Standing on its own it could not be raised above the dialog at all, not even by hand. Advanced is now its own card under the simple questions: collapsed to a header by default, expanding in place, remembered in show_advanced. Both cards are on screen at once, so the expanded rows double as a readout of what the simple answers resolved to, and a change made in the advanced rows is read back into the simple ones — skipped while simple mode is itself steering them. The info card sits between the two, where a priority or language change shows its effect and its cost. Refs #779 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * fix(settings): let a same-size weight on disk stand in before downloading Reported: turning the "I also dictate whole texts in other languages" switch off went from base to base.en — same size, different weights — and sat the user through a modal 141 MB download for it, with the window blocked behind the progress dialog. Turning it on had done the same in the other direction. Simple mode now looks at the disk before settling on a variant. A downloaded weight of the same size that can serve the language stands in: English-only first when English is wanted, the plain multilingual next, quantized ones last. English-only weights never stand in for another language or for detection. When nothing of that size is on disk the derived variant stays and the download runs as before. The info card still offers the better variant to download; it just no longer forces it. Refs #779 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * fix(settings): keep Advanced language pin over stale simple multi _sync_simple_from_advanced restored the other-languages switch and persisted simple_second_language even when Advanced had a pinned language. The next simple edit then decoded to auto and silently replaced the pin. Clear/resync simple multi on a pin, refresh that readout when Advanced language changes, and drop the second-list Enter hook that double-applied through SearchablePicker. Refs #779 * fix(settings): keep two-English simple multi across Advanced pin sync Clearing every leftover second language on a pinned Advanced readout was too broad: en-US plus en-IN still pins English, and wiping that answer forgot the second English choice on the next open. Restore multi only when the stored second would keep the pin; otherwise drop it so a later simple edit cannot decode to auto. Refs #779 * fix(settings): restore simple multi only when it matches live language The pin-side keep left the auto branch restoring any stored second. Two English over Advanced auto still decodes to en-us, so the next simple edit silently replaced detection. Keep leftover multi only when it still decodes to the live Advanced language either way. Refs #779 * fix(settings): keep language-change tests honest after the main merge Mock dialogs treated _initializing as truthy and skipped the handler. Also call _refresh_simple_readout explicitly so the simple card updates. * fix(settings): type the language-entry GTK callbacks Greptile flagged missing param and return annotations on the new activate and focus-out handlers. Match the surrounding Any/bool style. Refs #779 * fix(settings): sort imports for isort after Parakeet merge --------- Co-authored-by: Kacper Paczos <23406555+kacperpaczos@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
8f4aeca2f1 |
feat(asr): add Parakeet TDT 0.6B engine via sherpa-onnx (#802)
* feat(asr): add Parakeet engine via sherpa-onnx Adds a "parakeet" engine backed by sherpa-onnx, running NVIDIA Parakeet TDT 0.6B int8 models on CPU: v3 (25 European languages, auto-detected) and v2 (English). Output is punctuated and capitalized. On an Intel Core Ultra 7 155H it decodes at about 12x realtime on CPU (RTF 0.08), roughly 8x faster than whisper.cpp large-v3-turbo over the same clips with its Vulkan GPU backend (RTF 0.59). Being a transducer it also has no 30s window to pad, so a short push-to-talk utterance returns in ~0.3s instead of ~2.4s. Follows the existing engine layout: utils/parakeet_model_info.py holds the catalog and the get_model_path / is_model_downloaded / list_downloaded_models / delete_model helpers, while _init_parakeet / _transcribe_with_parakeet / _download_parakeet_model follow the same shape as their whisper.cpp counterparts and reuse _stream_model_download for progress and cancellation. Registered in the engine dispatch, the settings engine/model/language pickers, the model info card, the download prompts, the unused-downloads list and install.sh. Models download to <models_dir>/parakeet/<model>/ via temp files renamed on success; is_model_downloaded requires all four files, so an interrupted download is retried rather than loaded. Requires the new "parakeet" extra (sherpa-onnx). * fix(install): write only the engine key for parakeet The parakeet branch duplicated a full default config.json. ConfigManager deep-merges DEFAULT_CONFIG on load, so every other key was redundant: a config holding only "engine": "parakeet" still resolves parakeet_model_size to v3-european along with the VAD, shortcut and text-injection defaults. * fix(install): keep push-to-talk defaults and switch existing configs Writing only the engine key left config.json without a shortcuts section, which _migrate_shortcuts_config reads as a pre-push-to-talk upgrade and pins back to ctrl+ctrl/toggle - contradicting the installer's own "Hold Right Alt while you speak" message. Seed that section too. Point the config at parakeet with set_configured_engine rather than only on first create, so --engine=parakeet also switches a machine that already has a config.json, and give print_welcome_message a parakeet arm so the summary is not blank. The interactive picker overwrites SELECTED_ENGINE, so document the flag combination that reaches this branch. * fix(asr): pin Parakeet bundles and report one progress bar Pin each bundle to a Hugging Face commit instead of resolve/main, so a file replaced upstream cannot change what an install downloads. This is the same approach whispercpp_model_info takes with its pinned revision. Remap each file's progress into its slice of the bundle. The four files were reported through the single-file downloader independently, so the fraction ran to 1.0 and restarted four times; it now advances once and names the file being fetched. * fix(asr): reject a truncated Parakeet download _stream_model_download only rejects an empty body, so a truncated but nonempty response was renamed into the model directory and then reported as installed, because is_model_downloaded checks existence. Record the byte size of each file at the pinned revision and check it before the rename, the same order whisper.cpp verifies its digest in. A short file is a truncated transfer rather than an upstream change, since the revision is pinned, so it raises and the temp file is removed. * style(asr): annotate the Parakeet model catalog mypy inferred the catalog values as object, so reading file_sizes out of an entry raised attr-defined and no-any-return under `just typecheck`. * fix(asr): pin Parakeet digests in model_checksums.txt AGENTS.md requires every speech model to be verified against a pinned digest before install, failing closed on an unpinned one. Parakeet was checking only the byte size. Teach scripts/generate-model-checksums.py to emit Parakeet entries: the ONNX files are LFS so Hugging Face publishes their sha256, and tokens.txt is hashed from the downloaded bytes the way VOSK archives are. Several bundles ship the same file names, so a key carries the model name. _download_parakeet_model now calls verify_model_file before the rename and catches ChecksumError alongside the other download failures, matching the whisper.cpp downloader. * fix(asr): verify on-disk Parakeet models before load Existing bundle files were trusted by path alone, so a leftover or copied-in tree could reach sherpa-onnx unchecked. Hash each file against its pin on download skip and on init, and cover the Parakeet pins in CI. * fix(ui): hide Parakeet language picker that ignores the setting Parakeet language coverage is the model choice, not a catalog language. Hide the picker and persist auto so Settings does not write a lying value. * fix(asr): normalize Parakeet language to auto at startup CLI --language and stale saved languages still reached SpeechRecognitionManager even though Parakeet ignores catalog language. Force auto on the shared helper used by main and the manager so stored settings stay honest. * fix(ui): restore language after leaving Parakeet Remember the prior Whisper/cpp language across a Parakeet visit so switching back does not leave recognition stuck on auto. Keep Parakeet forced to auto and the picker hidden. Also satisfy black on the startup normalizer path. * fix(ui): keep language memory across Vosk fallback Vosk's unsupported-language en-us coercion was writing into _last_non_parakeet_language, so Whisper→Parakeet→Vosk→Whisper restored en-us instead of the user's Greek (or other) preference. Remember the pre-coercion catalog language and prefer it when leaving Vosk. * fix(ui): keep language memory when entering Parakeet from Vosk Vosk's coerced en-us fallback was still written into _last_non_parakeet_language on Parakeet entry, so Vosk→Parakeet→Whisper restored en-us instead of the pre-coercion preference (Greek or auto). Share one coerced-fallback predicate across entry, leave-Vosk, and language sync, and cover those paths with regression tests. * fix(tests): set initializing flags false on language-change stubs Plain Mock attributes are truthy and were short-circuiting the handler. --------- Co-authored-by: Alex Thunder <AlexThunder@gmail.com> |