295386f08b7bbdda5714b6c95fa8d6c3dd29576f
578
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
295386f08b |
fix(text-injection): end option parsing before typed text (#922)
* fix(text-injection): end option parsing before typed text A 20-char xdotool chunk, or a 200-char wtype/ydotool chunk, that starts with '-' (a word hyphenated across the chunk boundary) is parsed as a flag: 'xdotool type --clearmodifiers -stickation' exits 1 with 'type: unrecognized option' and the rest of the dictation is dropped. Pass '--' before the text argument in the xdotool, wtype, and ydotool type invocations so the remaining argument is always typed literally. Fixes #921 * test(portal-fallback): expect the wtype option terminator The partial-type fallback assertion predates the -- terminator and still expected the bare ['wtype', 'llo'] argv. Refs #921 --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
d021e64813 |
fix(dictation-pad): keep pad window out of the compositor ghost state (#912)
* fix(dictation-pad): keep pad window out of the compositor ghost state On GNOME Wayland an idle, fully covered toplevel gets shelved by the compositor: it stays in the window list but renders nothing and takes no input. Keep the pad floating above other windows and pinned to all workspaces so it cannot be shelved, track iconify/obscure state events, deiconify on show, and repaint when visibility returns. Fixes #896 * test(dictation-pad): scope Window return_value patch in shelving test * fix(dictation-pad): detect Wayland shelving and recreate dead surface set_keep_above is a stub in GTK3's Wayland backend, so a covered idle pad still gets shelved by the compositor on GNOME/Wayland: it stays in the window list but draws nothing and takes no input, and present() cannot revive it. Add a frame-callback watchdog: each tick callback stamps the clock and a GLib timeout flags a mapped window whose frames went cold as shelved. A shelved pad is then recreated (hide + show_all) in show_pad, the same path the user's manual Close + tray reopen takes. Keep keep_above/stick for X11 where they are honored, plus the iconify and visibility-event handling from the previous commit. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
06f475cf95 |
fix(evdev): require a real keyboard key in /proc keyboard discovery (#918)
* fix(evdev): require a real keyboard key in /proc keyboard discovery Any nonzero B: KEY bit qualified a device as a keyboard, so power/sleep buttons, the ACPI video bus, vendor hotkey blocks and pointer-button interfaces were all grabbed and given (vocalinux) uinput clones, leaving duplicate logical devices that confuse compositor input settings. Require a modifier or KEY_A in the KEY bitmap — the same rule the evdev fallback path already applies — and update the test fixtures to a real keyboard bitmap so they exercise the intended gate. Fixes #915 * fix(evdev): read /proc bitmaps at the kernel's word width Capability bitmap words are unsigned longs — 32 bits on 32-bit kernels (armv7l, i686), not 64. Parse the whole bitmap through _bitmap_to_int() sized by platform.machine() so high key codes like KEY_RIGHTMETA land correctly on both widths. Give pointer-test mocks real keyboard keys so the pointer check — not the keyboard-key gate — drops them. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> Co-authored-by: Jatin K Malik <jk.malik@yahoo.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
ecd4ae8388 |
fix(evdev): skip multitouch-only touchpads in keyboard discovery (#917)
* fix(evdev): skip multitouch-only touchpads in keyboard discovery Devices reporting only ABS_MT_POSITION_X/Y (no single-touch ABS_X/Y) evaded the pointer-axis check, so multitouch touchpads like the Goodix GXTP5100 were grabbed and cloned — breaking pointer input while running. Fixes #914 * fix(evdev): read /proc bitmaps at the kernel's word width Capability bitmap words are unsigned longs — 32 bits on 32-bit kernels (armv7l, i686), not 64. Parse the whole bitmap through _bitmap_to_int() sized by platform.machine() so MT axis bits and any future high bits land correctly on both widths. Give the fallback MT-mock a real key so the MT-axes check is the only thing that can drop it. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> Co-authored-by: Jatin K Malik <jk.malik@yahoo.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
622d0e054d |
fix(text-injection): detect KWin 6 VirtualKeyboard property (#919)
* fix(text-injection): detect KWin 6 VirtualKeyboard property On KDE Plasma 6 the org.kde.kwin.VirtualKeyboard 'enabled' property no longer exists; the setting is reported as 'available'. Query 'available' first, keep 'enabled' as the Plasma 5 fallback, and accept both the real (<true>,) gdbus output and the doubly wrapped (<<true>>,) form. With the bridge check working, KDE sessions with Virtual Keyboard set to "IBus Wayland" can safely use scoped IBus injection again: the leftover-daemon trap of #752 is exactly what a confirmed VK bridge disambiguates. KDE without a confirmed VK stays excluded. KWin's RemoteDesktop portal scrambles letter case, so in auto mode on KDE sessions an installed ydotool is preferred over the portal -- the ordering used before the portal backend existed. Fixes #911 * fix(text-injection): keep KDE portal fallback ahead of daemonless ydotool On KDE, when ydotool is installed but cannot run (no daemon, no /dev/uinput access) and wtype is absent, the daemonless-ydotool branch fired before the portal fallback -- selecting a backend that fails at injection time while a working portal sat unused. The portal fallback now precedes the daemonless branch; working ydotool still outranks the portal on KDE. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
9eba80b53d |
fix(packaging): declare python3-pysocks in deb and rpm deps (#920)
* fix(packaging): declare python3-pysocks in deb and rpm deps #909 added PySocks as a runtime dependency for SOCKS5 proxy support but only covered the AUR PKGBUILD — the nfpm deb and rpm packages never declared python3-pysocks, so test_every_project_dependency_is_covered_in_both_formats fails on main and on every PR's merge check. Add it to both depends lists and the test's DEB/RPM_PROVIDES maps. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(packaging): deb name is python3-socks Debian/Ubuntu package PySocks as python3-socks (1.7.1+dfsg); the rpm name stays python3-pysocks — Fedora's python3-socks is a different library (python-socks). Verified in debian:12, ubuntu:24.04 and fedora:42 containers. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Jatin K Malik <jk.malik@yahoo.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
1869064203 |
fix(ci): attach snap with --repo, keep checksums on attach failure (#899)
* fix(ci): attach snap with --repo, keep checksums on attach failure The Attach Snap job had no checkout and no --repo flag, so gh release upload failed with 'not a git repository' on v0.18.0. Every other release upload passes --repo; do the same here. Publish Snap to Store matched 'will need manual review', but current snapcraft prints 'human review required', so the expected allow-installation review hold reported as a hard failure. A failed attach-snap also skipped Checksums and Provenance even though it hashes build artifacts, not release state. Run it unless cancelled and guard that all expected artifacts exist before manifesting. * fix(ci): keep literal review phrase in snap publish grep test_release_attaches_snap_then_tries_the_store asserts the literal 'will need manual review' stays in the publish-snap block. Keep it as the first alternation so the existing test documents the matcher. * fix(ci): require both native package sets in checksum guard Merged main added build-native and build-native-arm64 (.deb/.rpm per arch) to publish-checksums' needs and the SHA256SUMS glob. The artifact guard now requires two .deb and two .rpm files so a failed native build cannot publish an incomplete manifest. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
329a7d7f6a |
fix(settings): stage Advanced model spec until Download is confirmed (#908)
* fix(settings): stage Advanced model spec until Download is confirmed Picker changes in Speech Model -> Advanced staged a spec but immediately triggered a model download for every intermediate selection. Gate the auto-apply download path so a missing model only stages: a Download action on the model info card now runs apply_settings() as the single explicit confirmation. Simple-mode questions keep the instant apply-and-download flow. Fixes #894 * fix(settings): repaint model info card after a completed download The post-download paths repopulated pickers and resynced the engine but never repainted the info card, so a finished download left the amber status and the Download action visible for a model already on disk. The cancel path already repainted via the idle resync; give success its own. * fix(settings): keep staged spec through picker rebuilds and save non-model edits * fix(settings): persist non-model edits without a model-reloading reconfigure * fix(settings): live-apply non-model edits, keep whispercpp params saved-only while staged * fix(settings): live-set changed VAD/timeout attrs without reconfigure while staged --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
775863fd30 |
feat(settings): add proxy support for model downloads and update checks (#909)
* feat(settings): add proxy support for model downloads and update checks Add a Proxy page to Settings (above About) with Off / System / Manual modes. Manual mode supports SOCKS5 and HTTPS (HTTP CONNECT) proxies with optional username/password auth; the password is stored in config.json in plaintext like the other settings. System mode honors the *_proxy environment variables and falls back to the GNOME org.gnome.system.proxy manual configuration. The resolved proxy is applied to every in-app outbound path that fetches models or checks updates: the shared requests download path in recognition_manager (_stream_model_download, used by the whisper.cpp, whisper, Vosk, Parakeet and faster-whisper downloaders) and the release checks in update_checker. A small Test Connection button on the Proxy page verifies reachability through the configured proxy. SOCKS5 needs PySocks, added as a runtime dependency; uv.lock, the requirements/*.txt exports and the Flatpak dependency manifest are regenerated by just lock (with a fix in sync_flatpak_deps.py for packages like PySocks that publish more than one universal wheel). Fixes #655 * fix(proxy): bracket IPv6 hosts, escape test errors, declare pysocks for AUR --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
442a646a57 |
feat(history): add opt-in JSONL persistence for transcripts (#907)
* feat(history): add opt-in JSONL persistence for transcripts Adds history.persist (default off) mirroring the bounded snippet list to history.jsonl under the XDG data directory so transcripts survive restarts. Entries carry timestamp, duration, language and model when known. Clearing history truncates the file, turning persistence off deletes it, and disabling history removes it so nothing is retained anywhere. Settings gains a Keep on Disk switch and a Clear button in the Transcription History group. Fixes #758 * test(history): rebind real tempfile when siblings mock sys.modules test_recognition_manager and test_speech_recognition replace sys.modules["tempfile"] with a MagicMock at import time, so the new persistence tests were sharing one mock-named store path in full-suite runs. Guard the TemporaryDirectory binding the same way test_whisper_model_info already does. * fix(history): sweep stale temp files, survive damaged stores, and keep session fields on late commits * fix(history): reject lone-surrogate records and survive encode errors on write --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
ff57244dce |
ci(install): gate the remote install path end to end (#906)
* ci(install): gate the remote install path end to end A curl|bash user takes a different path than the local install gate covers: bootstrap outside a checkout, tag selection, clone/fetch of the selected tag, then handoff to the tagged installer with the original arguments. Add scripts/install-remote-test.sh, which publishes HEAD as a tag in a local mirror, runs install.sh against it twice (fresh clone, then fetch+reset of the existing clone), and fails closed when the clone's HEAD is not the selected tag commit. install.sh gains a VOCALINUX_REPO_URL override so the mirror can stand in for the public remote. Wire a single-distro job into the distro matrix and a just remote-install-gate recipe. Refs #701 * ci(install): publish the gate tag as an orphan from HEAD's tree CI checks out with fetch-depth 1, and git refuses to push or fetch a shallow boundary commit into a ref. Build the tag on an orphan commit created from HEAD's tree instead, and make tree equality the fail-closed check that installer, modules and exports are this commit's files. * test(install): force the re-run arm to prove fetch and reset Review feedback on the remote-install gate: the second run reused a clone already on the tag, so a broken fetch or reset would still pass. Move the clone onto a descendant commit and delete its local tag ref before the re-run, so only a real fetch + reset restores them. Also list the new recipe in AGENTS.md's command guide. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
6a6cc93919 |
feat(tray): remove Start on Login and About from the tray menu (#905)
* feat(tray): remove Start on Login and About from the tray menu
Both entries already live in Settings: the autostart switch is in the
general page and About is a dedicated sidebar page, so the tray items
are redundant. View Logs stays.
Removes the Start on Login checkbox item along with its
_update_autostart_checkbox/_on_autostart_toggled wiring and the now
unused _add_menu_checkbox helper, plus the About item and
_on_about_clicked. _show_settings_page("about") stays for the Update
Available item. autostart_manager is untouched since Settings uses it.
Fixes #654
* fix(settings): sync autostart switch with the real desktop entry
The tray's removed _update_autostart_checkbox also reconciled
general.autostart with the actual login desktop entry at startup. Move
that check into the settings load so the Start on Login switch still
reflects reality when the entry changes outside the app.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
|
||
|
|
26c4f9052b |
fix(settings): place test dictation output above its controls (#904)
Reveal the transcription readout at the top of the sidebar footer so it grows into the empty sidebar space above the Idle row instead of pushing the Test Dictation button down when it appears. Fixes #677 Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
4b7d57c077 |
feat(packaging): build thin .deb and .rpm and attach them to releases (#913)
* feat(packaging): build thin .deb and .rpm with nfpm and attach them to releases Adds packaging/native: an nfpm config that produces a .deb and an .rpm from the release wheel. The packages stay thin - Python, GTK, PyGObject and the typelibs are distro Depends; only pywhispercpp and pynput are vendored because the first is unpackaged on both distros and the second is missing on Fedora. The Debian 12 build image and the nfpm download are pinned in tool_checksums.txt. just native-packages builds both in the pinned container, and native-smoke installs the built package per distro: debian:12 and ubuntu:24.04 exercise the .deb, fedora:42 the .rpm, then each runs vocalinux --version plus an import smoke of the app and the vendored extension. unified-pipeline.yml gates on the same three-distro matrix, and release.yml attaches both arches to the release and covers them in SHA256SUMS and the provenance manifest. Refs #600 * fix(packaging): preserve launcher args and document Wayland input group The launcher joined $* into an unquoted exec, re-splitting file paths that contain spaces before argparse saw them. Exec "$@" directly and re-quote each argument for the inner shell of 'sg input -c'. On Wayland the packages could not grant the 'input' group membership evdev hotkeys need; name the usermod step and its caveat in INSTALL.md and the release notes. * test(packaging): add return types to the native packaging tests --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
d53300841d |
fix(appimage): reuse the Vulkan pywhispercpp build across CI runs (#903)
* fix(appimage): reuse the Vulkan pywhispercpp build across CI runs The x86_64 Build AppImage step spends most of its time recompiling pywhispercpp with Vulkan. That compile depends on the base image, tool_checksums.txt, and the pywhispercpp pin, not on the commit. Cache the native tree under one key shared by CI, Nightly, and Release. * fix(appimage): reject a partial Vulkan cache and key the build tools A failed copy could leave a tree that still contained libggml-vulkan.so, and the next run would reuse it. Check each copy, and keep the previous cache until the new one is in place. The cache id now also covers the pinned cmake and ninja export and the Vulkan compile flags. The hit path is what the build calls, and a test runs it with a stub compile command. * chore(ci): rerun AppImage against the saved Vulkan cache The review fix changed the native cache id, so run 37586979294 compiled pywhispercpp again and saved appimage-native-X64-ebf478b87986e2f6b9d80c97c2f5869fe3120c2bcd48f5d8f5a65fe06ec03015. This commit leaves the base image, tool checksums, pywhispercpp pin, installer tool pins, and Vulkan compile flags alone. |
||
|
|
af1029dc60 |
fix(evdev): skip pointer-motion devices in keyboard discovery (#902)
* fix(evdev): skip pointer-motion devices in keyboard discovery find_keyboard_devices() treated any device with a nonzero EV_KEY bitmap as a keyboard, which includes Logitech mice (G502, MX Master 3) whose button interfaces report key bits. The hub then grabbed them (EVIOCGRAB) and re-emitted every event, including EV_REL pointer motion, through a paired uinput clone — piping all cursor movement through the reader thread and showing up as periodic cursor stutter. Discovery now excludes devices that report pointer motion (REL_X+REL_Y or ABS_X+ABS_Y) in both the /proc/bus/input/devices parser and the evdev.list_devices() fallback. Wheel-only devices (REL_WHEEL) and real keyboards are unaffected, and a mouse's separate keyboard interface node keeps working for shortcuts. Fixes #900 * test(evdev): annotate new device-discovery tests * chore: drop accidentally committed pr_body.md --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
758f640787 |
fix(uninstall): stop deleting files in the caller's directory (#901)
* fix(uninstall): stop deleting files in the caller's directory The curl uninstall command runs uninstall.sh from $PWD, and the script treated that directory as a source checkout. It deleted venv, build, dist, and Python bytecode there, then reported success. Those paths are removed only when uninstall.sh itself lives in a Vocalinux checkout, and only inside that checkout. Fixes #897 * fix(uninstall): keep dev metadata and drop the false cleanup error Tests put a pgrep and kill stub first on PATH, so they cannot signal a Vocalinux process that is already running. egg-info cleanup stays under src/ and the checkout root, and the bytecode walk prunes .venv. Artifact cleanup runs before the curl clone is deleted, so an uninstall from ~/.local/share/vocalinux-install no longer prints an error and then success. |
||
|
|
1d4dbe33cc |
chore(release): prepare v0.18.0 (#895)
* chore(release): prepare v0.18.0 Bump version surfaces for the v0.18.0 minor release: version.py, web/package.json + lockfile, softwareVersion schema, SECURITY.md supported-versions table, AUR pkgver/_tag (source and -bin), AppStream metainfo release entry, and the release-history table in RELEASE_PROCESS.md. Docs and site copy for the delta since v0.17.0: README blurb, snap sideload versions in README/INSTALL/UPDATE, CHANGELOG current-stable entry, docs/UPDATE.md What's New with the 0.18 highlights table and per-PR credits, website changelog + desktop-reliability entries, and PRODUCT.md capability lines for Dictation Pad, per-language shortcuts, tray history, overlay, ducking, PipeWire, portal injection, D-Bus activation, custom dictionary, and file transcription. * fix(release): address review findings on the v0.18.0 prep - Keep the checked-in vocalinux-bin PKGBUILD on v0.17.0 so its real AppImage digests stay consistent with its sources; release.yml bumps pkgver/_tag and pins both-arch digests atomically at publish - Qualify the Flatpak release note: PipeWire system-audio capture and RemoteDesktop portal injection are native-install paths - Refresh dateModified on the changelog and desktop-reliability pages - Restore the release-history table order in RELEASE_PROCESS.md - Revert the build-regenerated web/next-env.d.ts route-types reference --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
96e012d9bd |
fix(evdev): keep key suppression when /dev/uinput is write-only (#893)
* fix(evdev): keep key suppression when /dev/uinput is write-only python-evdev opens /dev/uinput O_RDWR, but the kernel only needs write access for the uinput setup ioctls and event injection. Installs that still carry the original 80-dotool.rules line (MODE=0620, write-only for the input group) therefore fail UInput() with EACCES and fall back to ungrabbed monitoring, where the dictation shortcut is detected but also reaches the focused application. Retry the clone on an O_WRONLY descriptor via a small _WriteOnlyUInput subclass so suppression keeps working there (refs #871). Also populate _withheld_modifier lazily in _event_is_shortcut: the AltGr-replay machinery was dead code because nothing created the per-fd map, so a withheld modifier press was never replayed and composition (e.g. RightAlt+e) lost its modifier. * fix(evdev): byte-truncate clone names and cover the write-only path - _clone_device_name now truncates the encoded name by bytes: the kernel limit is 79 bytes, and a long multibyte device name could push the " (vocalinux)" suffix past the cut so rescan would not skip the clone. - _WriteOnlyUInput closes the fd explicitly when _prepare_events fails before the _uinput calls; the helpers close it themselves on any later failure. - Tests now instantiate the real subclass with mocked OS/native calls (open flags, setup sequence, write/write_event/syn ordering, fd cleanup, name truncation). --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
4497bfae6c |
fix(logs): let the View Logs dialog close via titlebar-X (#892)
* fix(logs): let the View Logs dialog close via titlebar-X _on_response only destroyed on ResponseType.CLOSE and ignored DELETE_EVENT, so the Logs window could not be dismissed with the window-manager close button — unlike Settings, which handles both. A window force-destroyed behind the dialog then crashed quit with a BadWindow error instead of an orderly shutdown. * test(logs): cover _on_response behavior; annotate its signature --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
d11bd1bf0d |
feat(post-processing): pipe transcriptions through an optional postprocessing script (#479)
* feat(post-processing): pipe transcriptions through user-defined script Add a configurable post-processing step that passes each transcription result to an external executable via stdin and uses its stdout as the text to type. Enables custom transforms (formatting, LLM rewriting, etc.) without modifying Vocalinux. Falls back to original text on script failure or timeout. * test(post-processor): add unit tests and extract apply_post_processing Move config-reading logic from main.py closure into apply_post_processing() in post_processor.py so it can be tested independently. Add 11 unit tests covering PostProcessor.process() and apply_post_processing(). * docs(post-processing): document post-processing script feature * fix formatting of one file * fix(post-processing): run the user script with a stripped host environment Rebase compat: the host_process AST guard requires every subprocess spawned for a host binary to pass env=host_env() so an AppImage's LD_LIBRARY_PATH/GI_TYPELIB_PATH/PYTHONHOME do not leak in. Also give apply_post_processing() a typed ConfigManager parameter and keep text_callback_wrapper's local variable typed as str for mypy. * fix(post-processing): preserve paragraph breaks and tighten script handling Take script stdout verbatim instead of rstripping every trailing newline, so the "new paragraph" voice command's break survives a passthrough script. A single trailing newline is still dropped, but only when the transcription had none — it is almost always just the line terminator echo/print-style tools append. Also stop writing transformed output to the log (it can carry sensitive dictation into retained logs), catch only the expected process/OS/encoding failures instead of every Exception, and add the missing signature annotations. * fix(main): run the post-processing script off the recognition thread A user script may take seconds per segment, and text callbacks run on the recognition thread that drains the bounded audio-segment queue; stalling it drops queued dictation once the queue fills. Submit post-processing and injection to a dedicated single-worker executor so the backlog waits instead of losing speech, while keeping injection order. On timeout or failure the worker falls back to the original text, and an unexpected error is logged with a traceback and still injects the original rather than silently dropping the segment. Also pin the callback tests' get_str mock to no-script so they exercise the synchronous path deterministically, and add a callback test proving transformed text (including preserved paragraph breaks) reaches the injector. * docs(post-processing): annotate new callbacks and clarify stdout contract Add the missing type annotations on the new Settings page callbacks per the repository guide, and document that script stdout is used verbatim (a lone trailing newline added by line-oriented tools is dropped only when the transcription had none) and that scripts run on a dedicated worker. * test(main): annotate new post-processing callback tests * fix(main): bind queued dictation to its target app and drain worker on quit Route every transcription segment through the single post-processing worker (a pass-through when no script is configured) so clearing the script path mid-queue can no longer let a later segment overtake an earlier one still running. A segment that can sit in the backlog — a script is configured or a job is still running — is bound to the application focused when it was dictated; the worker drops it if focus has verifiably moved to another application, so delayed dictation cannot land in the wrong app. Wire the worker into the tray quit path via an on_quit hook: it clears the acceptance flag, cancels pending jobs, and waits — bounded by the script's own timeout — so nothing injects after the injector stops. * fix(main): keep voice actions ordered and probe focus off the recognition thread - Queue voice-command actions on the post-processing worker so an action can no longer overtake dictated text still waiting to be injected. - Move the submit-time focus probe onto a dedicated executor; the recognition thread only submits the probe, and the job joins the result at inject time, so compositor tools can no longer stall consumption of the bounded audio-segment queue. - Re-check the quit flag after the inject-time focus re-probe, then shut both workers down without waiting on a running script so quitting does not freeze on the script timeout. * fix(main): capture submit-time focus on dedicated threads and serialize quit - Replace the shared focus-probe executor with a fresh daemon thread per submission so a slow earlier probe cannot delay a later one past the focus change it is meant to capture; jobs read the result off a one-slot queue at inject time. - Give queued voice actions the same submit-time focus binding as text segments so a queued shortcut is dropped instead of fired into a newly focused application. - Hold injection_lock across the accept-check plus inject/action call so quit waits only for an in-flight injection instead of racing it. - Queue the IDLE reset of last_injected_text behind pending worker jobs so a queued 'delete that' still sees the text it targets. * fix(main): keep actions instant on a free worker and bound quit wait - An action now starts a submit-time focus probe only when it actually queues behind other jobs; with a free worker there is no delay window to bind against, so the probe's compositor calls are skipped and the action stays immediate even when a script is configured. - Probe and executor submission now happen inside pending_jobs_lock so queue order matches callback order — a job that saw an empty queue can no longer slip behind one that arrived while it was submitting. - Every inject-path subprocess call (xdotool window/key/type, wtype and ydotool type) is bounded by a timeout, and the quit hook's injection_lock wait is capped, so quitting can no longer freeze on a stalled injector subprocess. * fix(main): bind every action to submit-time focus without slowing it down - Every voice action now starts a submit-time focus probe: submission never guarantees immediate execution, so skipping the binding left a focus-change window in which a shortcut could fire into another app. The worker waits on the probe for at most one second — milliseconds on a healthy desktop — so immediate actions stay immediate while a genuinely stalled probe falls back to the injector's own targeting. - Quit waits for an in-flight injection without a cap again: every inject-path subprocess is individually bounded, so the wait always ends with the injection and a long transcription is never cut off. - The wtype/ydotool typing timeout is now derived from the configured per-key delay (a generous multiple), so a legitimately slow type can never be cut mid-text; a timed-out type no longer falls through to the clipboard fallback, which would duplicate the partial fragment. * fix(main): drop unverified actions and make quit-time injection abortable - An action whose submit-time focus probe cannot answer inside its one-second bound is now dropped instead of running unverified: a stalled probe can no longer leave a shortcut pointing at whatever application happens to be focused. - Quit asks the injector to abort in-flight injections, and injection paths honour it: the xdotool chunk loop stops between chunks, the wtype/ydotool type call runs as a polling Popen that can be killed, and paste/shortcut/backspace entry points exit early. The quit hook still waits for an in-flight injection, but the wait is bounded by at most one already-running subprocess call — never the injection's full length — and no subprocess is ever stopped mid-call by a timeout cap on the wait itself. - Annotate the remaining untyped test helpers. * fix(main): wait out slow focus probes; restore run() in wayland typing - An action no longer races its submit-time focus probe on a one-second bound: the job waits for the probe's own answer, whose compositor calls already carry per-call timeouts and short-circuit absent tools. A slow but living probe delays the command instead of discarding it, so a valid "undo" can no longer be lost to timing alone, and focus verification still decides every drop. - Wayland typing goes back to subprocess.run so tests keep intercepting the same entry point. Abortability is preserved by typing in 200-character chunks with the shutdown check between calls — a whole-string call could not be interrupted, but no single chunk holds the injector for more than its own scaled budget. - The abort flag gains a class-level default so injectors built without __init__ in tests still answer the checks; each real instance gets its own event in __init__. - Update call-shape assertions for the typed-call timeout and finish annotating the test methods touched by the callback refactor. * fix(inject): never retype text a failed chunked call already sent - _PartiallyTyped carries the confirmed-delivered prefix length out of the chunked type loops, so every downstream fallback continues from the untyped remainder: the Wayland-to-xdotool switch types only the tail, and the clipboard fallback copies only the tail, never the whole string a second time. - The xdotool retry loop gains the same accounting — a retry now resumes at the first untyped chunk instead of restarting at the top and duplicating everything the failed attempt already delivered. - Annotate the remaining test signature the repository guide requires. * fix(inject): report partial typing as failure even when clipboard lands A chunked type call that dies mid-text leaves only a prefix on screen, and the remainder-fallback keeps it that way: it copies the tail for a manual paste but now returns False, so the caller no longer records the full transcription as injected and a following "delete that" cannot erase text that was never typed. * fix(inject): keep deletion state aligned with delivered text inject_text now records how many characters a call confirmed on screen as last_typed_count: the whole length on success, the typed prefix after a partial failure, -1 when the delivered amount is unknowable. The transcription caller uses it to keep last_injected_text honest — a following "delete that" erases the typed prefix or nothing, never the previous segment or text that never arrived. * fix(inject): never replay input a timed-out portal job may have delivered - RemoteDesktopPortalError.delivered=None marks an unreported job's count as unknowable; all three portal fallbacks demote and skip instead of replaying, and text injection surfaces as TimeoutExpired so no full-text clipboard copy is offered - _PartiallyTyped counts raised after a portal demote are rebased onto the original text (typed_prefix) so the caller clips the real prefix - evdev: a consumed pure-modifier press is withheld and replayed ahead of the first non-target press, so holding RightAlt for AltGr composition still reaches the application - history: extend_latest gains an expected_latest CAS guard so a late segment from an older worker merges only into its own session's snippet, never into a newer session's entry - quit path aborts in-flight injections before waiting on the lock so quitting cannot freeze behind a long transcription's typing budget - tests: stop _ensure_ydotoold leaking a real Popen in Wayland tests * fix(shortcut): replay withheld modifiers across devices and after SYN_DROPPED - Replay withheld modifier presses from every device, not only the fd the character arrived on: split keyboards deliver AltGr and the character on separate devices, and a per-fd check forwarded a plain character. - Let the post-SYN_DROPPED resync run when only withheld modifiers exist for the fd, so a press whose release the dropped burst ate is pruned instead of replaying a stale modifier on the next ordinary key. * fix(history,pads): keep late segments pad-bound deliveries and pad deletions correct - Track the worker behind the ended session's snippet so late segments from an older worker cannot merge into a newer session's entry - Skip the focus-change drop for dictation-pad segments and pad-bound editing commands, which never target the focused application - Refuse 'delete that' when a manual pad edit blurred the segment boundaries instead of deleting the stale recorded length * style: drop unneeded nonlocal for mutated worker set * fix(shortcut): share one evdev device layer across shortcut listeners Each KeyboardShortcutManager built its own EvdevKeyboardBackend, and on evdev every backend opened and grabbed the same physical keyboards: the first grabber received all events and the rest received none, so per-language shortcuts could never fire on Wayland. All backends now bind to a process-wide EvdevDeviceHub that opens each keyboard once, owns the grabs and the paired uinput clones, and fans every event out to each registered engine. An event is re-emitted to applications only when no engine consumed it, so suppression semantics stay the same while per-engine matching state (combo progress, withheld modifiers, swallowed presses) remains independent. * fix(history): own late segments per worker and decide pad routing once - ended_worker_snippets maps each ended session's worker to its own (snippet, commit epoch): late deliveries merge into their session's entry however many sessions have committed since, so one session's stragglers never split across Recent Snippets entries. - The leftover-segment commit reads the live history epoch inside the session lock: a mid-session clear() no longer makes the stale ended epoch refuse valid post-clear dictation. - The pad-bound routing decision is captured once per job and passed to the injector/action handler, so a toggle flip between the focus check and delivery cannot reroute text or shortcuts into the focused app. - Dead recognition workers are pruned from the tracking set and map so long runs don't accumulate finished threads. - The custom-terms switch now suppresses GTK's own state-set on save failure so it shows the value that actually persisted. * fix(shortcut): serialize evdev device layer teardown against restart The last listener's unregister released the engine lock before joining the monitor thread and closing devices, so a new listener registering in that gap opened keyboards the stale teardown then closed — it stayed active but dead, and briefly grabbed keyboards stopped forwarding input. A lifecycle lock now covers the whole close/open transition in register, unregister, and the monitor's unexpected-exit teardown, so a cold start can only open devices after the previous generation finished closing (also making the new generation's grabs succeed). A generation stamp tells a monitor thread that outlived its join() its incarnation is over even when running has gone True again. * fix(injection): log focus-probe failures and narrow clipboard fallback catch A crashed focus probe turned into an unknown result with no logged error, indistinguishable from unavailable focus information; the partial-typing clipboard fallback caught every exception. Log the probe failure with context and narrow the clipboard catch to the exceptions the fallback path can actually raise. * fix(shortcut): join the evdev monitor outside the lifecycle lock If the monitor exited unexpectedly while the last listener unregistered, its cleanup queued on the lifecycle lock that unregister() held for the join — stalling shutdown for the join's full 2s timeout. The close now finishes inside the lock and the join waits after it is released, so the monitor's own cleanup can run and exit instead of queueing behind it. Closing before the join also dies the monitor's read/select on the closed fds right away. A _closed gate on _open_keyboard_device keeps a rescan from opening a device between detach and close that nothing would ever close. * fix(injection): narrow focus-probe catch to the raisable exception set get_focused_window() reports Optional and never raises; the catch now covers only what can realistically escape the queue write and logs it with context, keeping the waiting job released. * fix(shortcut): tag evdev device fds with the generation that opened them A monitor that outlived join() could still touch a newer generation's devices: the loop re-checked the generation only at the top, so mid-iteration it could dispatch from, forward to, or remove devices a restarted listener had just opened. Every fd is now tagged with the generation that opened it, and the tag is verified at each point a monitor could mutate shared state — fd lookup for dispatch, clone forwarding, forwarder release, device removal, and rescan opens — while dispatch re-checks the generation between events and the per-fd loop breaks on it. Cold starts bump the generation before opening so fresh devices are tagged for the new generation. Devices an older generation opened stay tagged stale, so its reader leaves them alone. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
dca937fc21 |
feat(dictionary): combined custom dictionary with corrections (#890)
* feat(dictionary): combined custom dictionary with corrections Rebased onto latest main (conflict-resolution squash for PR #818). * fix(dictionary): stabilize the terms and corrections file contracts - corrections_path() stays at the fixed config_dir() location promised by the file contract instead of following a custom or --dictionary-file terms path, which could strand or duplicate corrections; a leftover file beside the terms file is still read once and copied over. - _atomic_write() preserves the replaced file's permission mode instead of resetting it to the process umask. - build_initial_prompt() streams terms and stops at the configured cap instead of reading and processing the whole file for every segment. * fix(settings): restore the selected dictionary pane after a search Searching can auto-switch the Custom Dictionary management pane to reveal a match, but clearing the query only restored widget visibility. Record the pane selection in the search baseline snapshot and restore it alongside the previous page so users return to the pane they chose. * fix(dictionary): close the remaining custom dictionary file-contract gaps - A --dictionary-file session override no longer imports a corrections file found beside the override into the standard config directory; the override scopes to terms only. - Terms reads validate the whole file before yielding: corrupt UTF-8 mid-file now yields no terms at all, matching the invalid status Settings reports instead of leaking a partial dictionary to recognition. - The terms read is bounded by MAX_TERMS_FILE_BYTES (1 MiB); a larger file is invalid everywhere with a dedicated status instead of being read on every segment. * fix(dictionary): keep usable terms prefix on oversized files and legacy corrections under overrides * fix(dictionary): cap yielded terms and keep added terms visible on oversized files * fix(dictionary): keep added terms visible without dropping scanner-owned lines * fix(dictionary): keep oversized terms manageable and honor mid-search pane switches - terms list gains a Show all / Show first 500 toggle; adding a term beyond the cap auto-expands so the new row renders with a Remove button instead of being invisible until the file is hand-edited - a manual dictionary pane switch during an active search drops the saved pre-search pane so clearing the query no longer rolls back the user's newer selection - annotate the new settings tests and type model_info so the parakeet branch stops adding fresh mypy errors * fix(settings): do not auto-expand terms on add past display cap Rebuilding up to thousands of term rows on the UI thread stalls the dialog; the overflow button already shows the true count, so the added term stays one click away. * test(dictionary): stub segment_callbacks on bare manager for history dispatch * fix(dictionary): stream oversized terms files when building the prompt build_initial_prompt decoded the whole 1 MiB read window before every transcription segment even though it only needs the first max_words terms. _iter_terms_for_prompt now reads oversized files line by line and stops at the prompt limit, keeping the same contract: complete leading lines only, invalid UTF-8 inside the usable window yields nothing. * fix(history): only the ended session's worker may extend its snippet A late segment could extend the newest history entry regardless of which session produced it, so a worker finishing after the next dictation session leaked its text into that session's entry. Track the worker that owned the ended session and require late segments to arrive on the same thread before merging; anything else forms its own snippet. * fix(speech): transcribe queued segments the reload worker never drained The recognition worker can exit during stop_recognition's bounded audio-thread join, so the released key's final buffer lands in the queue after the worker is gone and was silently dropped when the reload teardown replaced the queue. Drain whatever remains before swapping it — except on capture failure or an explicit cancel, which still discard the session's leftovers. * fix(settings): build term rows in idle slices and pin hidden additions Two problems shared one cause. Rebuilding up to 4096 rows synchronously stalled the dialog, and a term appended past the 500-row cap reported success while staying invisible. Rows now build in bounded GLib idle slices guarded by a refresh token, and a saved term outside the visible window is pinned to the top of the capped list until it disappears or becomes visible on its own. Adding under "show all" scrolls to the new term instead. * test(engine): annotate new test method return types * fix(ci): match a candidate row that leads the latest track block snapcraft status normally prints candidate as a continuation line under the latest track (channel in column 1). If candidate ever leads the track block it rides the track line instead (channel in column 3), and the resolver missed it and refused to promote. Match both layouts in the resolve and the pre-release re-verify. * fix(dictionary): bound prompt reads and scroll to landed terms - _iter_terms_for_prompt now reads each line via readline() capped by the remaining byte window, so an overlong line can never pull more than MAX_TERMS_FILE_BYTES into memory - the prompt path keeps decoding every complete line inside the usable window after the term limit is collected, so invalid UTF-8 anywhere in the window still yields no terms and matches the reader's invalid status - under "show all" Settings scrolls to the index where an added term landed (prepended or appended), verified against the row's label, instead of always scrolling to the end * fix(history,reload): keep late-segment ownership and drain stamped tuples - Track session_workers_seen so a worker from an earlier session can never merge into a newer snippet while a session is open - Unpack (segment, language) queue tuples in _transcribe_queued_segments; passing the tuple as audio raised TypeError and lost final speech - Refuse 'delete that' when a manual pad edit blurred segment boundaries - Translate term-row coordinates into the scrolled card's space so the scroll actually reaches rows below the fold - Type _on_dictionary_pane_changed's varargs * style: drop unneeded nonlocal for mutated worker set --------- Co-authored-by: Graham <graham@thegeeky.ninja> |
||
|
|
047b379bd2 |
feat(audio): add PipeWire capture path with system-audio sources (#889)
* feat(audio): add PipeWire capture path with system-audio sources Enumerate PipeWire sinks via pw-dump and expose their monitors as "System audio: <name>" capture devices at negative device indices (-2..), alongside the existing PortAudio microphones. Recording, device test, and stream-reconnection paths route to a pw-record-backed stream shaped like a PyAudio stream, so the VAD and silence-detection loop is unchanged. Playback ducking is skipped when the selected source is a monitor, since ducking the default sink would also duck the captured audio. Closes #751 * test(audio): restore stashed numpy instead of re-importing it test_audio_input imports numpy lazily; deleting the MagicMock that earlier test modules leave in sys.modules re-imported a second numpy module object whose lazy submodule attributes (linalg, testing) were unpopulated while sys.modules kept the first object's entries. The next test file to reach for numpy.testing hit a __getattr__ import loop (RecursionError in TestDownmixToMono on CI). Restore the conftest-stashed _vocalinux_real_numpy instead, matching the pattern tests/test_recognition_manager_device_config.py already uses. * fix(pipewire): drop PyAudio requirement for PipeWire sources, rebind source after reconnect * docs(pipewire): restore PipeWireCaptureSource class docstring * fix(audio): initialize PyAudio for mic reconnection after PipeWire session A session started on a PipeWire sink never creates a PyAudio instance; when capture later reconnects onto a PortAudio device the source needs one to reopen, and a failed open dereferenced None calling terminate. * fix(audio): harden PipeWire source resolution, reads, and cleanup - resolve stored names by node.name/description; a stored name that no longer resolves never falls back to a positional index - require pw-record before listing system-audio sources in the picker - bound pw-record chunk reads with a stall watchdog so stopping cannot block on a wedged stream - close the reconnection probe stream when its verification read fails - log cleanup failures instead of swallowing them; narrow exception handlers in enumeration and PyAudio reconnection init - annotate test helpers and signatures per repo style --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
9898bb8dd9 |
feat(ui): in-app Dictation Pad for Wayland-safe dictation (#887)
* feat(ui): add in-app Dictation Pad for Wayland-safe dictation Dictated text can now be routed into a text box inside Vocalinux's own window instead of being injected into other applications — an explicit fallback for desktops where cross-app injection is unreliable or blocked (notably Wayland). The pad opens from the tray menu, captures dictation when enabled via Settings or its own checkbox, and lets the user copy text out by hand. 'delete that' removes the last segment in-pad. Closes #726. * fix(ui): consult live config for pad auto-reveal and Copy All The reveal gate used the controller's enabled flag, which only syncs on pad show or its own checkbox — enabling capture via the Settings switch while the pad was closed silently buffered dictation with no reveal. _apply_append now reads dictate_to_pad from the shared config. Also: Copy All copies the widget text (dictation plus in-pad edits), and re-showing the pad no longer does a wholesale set_text — the view stays incrementally in sync, so manual edits survive hide/reopen. * fix(ui): keep dictation pad capture and commands inside the pad Address all Greptile findings on PR #887: - Read the capture toggle from the shared ConfigManager instead of re-parsing config.json per segment, so a torn mid-write read can no longer fail-open and send dictation to the focused app - Sync edits made directly in the pad widget back into the controller, keeping delete semantics consistent with the visible text - Stamp queued widget operations with a generation tag so a Clear drops appends still sitting in the GTK idle queue - Route undo/redo, select all/line/word/paragraph and cut/copy/paste to pad-local implementations while capturing, so their shortcuts never reach whichever application holds focus - Track where the last segment was delivered so "delete that" follows the text rather than the current capture toggle - Annotate every new signature and add callback-level tests that pin the routing, queue, and undo behavior * fix(ui): keep pad controller, queued ops, and delete target in sync Manual edits could overwrite queued dictation in the controller because GTK input events outrank idle callbacks; the changed handler now replays pending widget ops before syncing the view. The controller also tracks dictated segments across undo snapshots, so a pad undo pops the "delete that" target instead of leaving a stale length. Catches in the pad are narrowed to the GTK/config error set per repo style, and missing type annotations are added. * fix(ui): close flush races in pad idle queue and delete retarget Flushed idle ops stayed scheduled with GLib and ran a second time, duplicating appends; each queued op now only acts while its entry is still pending. A queued full-view refresh could also erase a manual edit that landed first, so the changed-handler flush now drops stale set-text ops instead of replaying them. And clearing the pad destination on undo-to-empty kept a redo from restoring the "delete that" target; the destination flag is preserved so redo re-arms it. * fix(ui): replay pad idle ops before edits land, not after Flushing in the changed handler still let queued appends leapfrog the keystroke that fired it, and a stale dispatch could cancel a later queued op with identical text. Pending entries now match by identity, and the replay moved to insert-text/delete-range — signals that fire before the edit is applied — so queued dictation and deletes keep true chronological order against manual edits. * fix(history): target late segments to their own snippet by id A straggler from a just-ended session could merge into whichever snippet was newest when it arrived, and audio captured before a session opened was attributed to that session while decoding on its worker. - TranscriptionHistory.add returns the snippet id and the new extend_entry() extends a specific entry, replacing the positional extend_latest target in the late-segment path - session_started_floor stamps when a session opens; in-session attribution also requires the capture to postdate it - test_capture_ceiling closes the mic-test window at callback restore so test speech still decoding can no longer leak into history * fix(settings): keep deferred edits from overwriting a newer choice The deferred-persist filter treated "config still equals the baseline" as untouched, so a reopened dialog that applied a key back to its baseline value could still lose it to the closed dialog worker. - Record the generation of the apply that last wrote each advanced key; a pending edit now survives only when no newer apply wrote that key - A worker apply whose snapshot a newer apply predates now skips its reconfigure and save entirely, so collection order — not lock order — decides which snapshot wins * fix(settings): skip stale applies before stopping dictation The superseded-apply check ran after stop_recognition, so a stale worker still interrupted a live dictation session. Recognition now stops inside the apply lock, after the generation check. * fix(shortcuts): keep language edits from breaking dictation Two per-language shortcut fixes (#805): - A binding edit landing while a session is live no longer stops recognition. The dialog's per-keystroke persist still saves the new binding, but the tray defers the listener rebuild to the next IDLE transition, so a held push-to-talk keeps its release callback and a toggle session is never cut off mid-utterance. - A row edited onto a key another row holds can no longer displace that row's binding. Each row's remembered shortcut is reserved for its own row before claims run in row order, so the stealing edit is rejected and the held binding survives. - Persists that resolve to the already-saved bindings (mid-edit keystrokes, rejected duplicates, reverted fields) skip the save and the listener rebuild entirely. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
dbe0215aad |
feat(shortcuts): add language switching shortcuts, fix PTT races, and add dictation history (#880)
* feat(shortcuts): per-language dictation shortcuts (#805) Bind extra keys that start dictation in a specific catalog language for one utterance, then restore the configured language. Bindings live in shortcuts.language_shortcuts as [{shortcut, language}], normalized against the language catalog and the backend's shortcut parser. The tray spawns one KeyboardShortcutManager per binding in the active mode; toggle presses route through start_recognition_with_language, and push-to-talk arms press/release the same way. The manager keeps the override one-shot: it applies before the utterance starts (outranking follow-keyboard-layout) and unwinds on the next IDLE/ERROR transition or when a mid-dictation reconfigure picks a new language. Engines that cannot re-language without a reload (VOSK, Parakeet, English-only whisper.cpp) refuse with the error cue and a notification instead of dictating in the wrong language. Settings gains a Language Shortcuts group under Shortcuts: each row is a language picker, an editable key entry, a Record button that retargets the shared key-capture, and a remove button; edits persist immediately and rebuild the live listeners. A non-dict shortcuts section now rebuilds instead of crashing config load. * fix(shortcuts): class-level default for language shortcut listeners TrayIndicator stubs built with __new__ (e.g. test_suspend_handler) skip __init__ but still run _quit(); give _language_shortcut_managers a class-level default so teardown iterates an empty list. * Fix Greptile review findings on per-language shortcuts (#805) - Bind each dictation session's language for transcription workers so the one-shot restore cannot rewrite a queued segment's language - Judge a shortcut's capability against the model the layout refresh will serve (whisper.cpp sibling swap) and refuse requests the engine drops - Add _oneshot_language_restore to the reconfigure snapshot and settle a revived pending restore on failure - Dedupe language bindings by gesture (modifier reordering, enter/return alias, pure-modifier side sets) instead of by string - Stop per-language listeners under external activation and never re-arm - Gate push-to-talk release on the binding that started the session, and stop a held hold before rebuilding listeners - Keep a row's last valid binding during partial edits, report rejected duplicates, cancel a capture aimed at a removed row, disarm the previous Record target, persist a recorded key once, and add the missing type hints * fix(shortcuts): resolve remaining Greptile race findings on PR #880 - Stamp each queued audio segment with the session language it was recorded under, so a worker that outlives stop_recognition's joins transcribes each utterance in its own language rather than whatever a newer session stored in _session_language. - Serialize push-to-talk press/release behind _ptt_lock: a release landing while start_recognition runs now waits for the owner claim instead of being dropped, so a quick key tap cannot leave the microphone running. - Add the missing return type hint on _stop_language_shortcut_managers. * test(shortcuts): stub _segment_started_at on bare manager stubs * fix(shortcuts): carry segment language as attribute, default language on bare managers * fix(shortcuts): keep (segment, language) queue tuples, stamp segment.language too * fix(settings): apply history toggle and limit to the live store --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
ca40f7d8b0 |
fix(text-injection): keep Alt+Shift/Win+Space layout switching on GNOME Wayland (#876)
* fix(text-injection): restore full GNOME XKB map on XWayland after inject sync_xwayland_layout_from_gnome wrote only the single active input source via 'setxkbmap -layout <one>'. That left XWayland with a one-group keymap: Alt+Shift 'grp:' toggles and the group index Mutter drives for Win+Space became no-ops for every X11/XWayland client until relogin. Apply GNOME's complete XKB map instead: every xkb input source in 'sources' order plus all 'xkb-options', and only when the live rules differ (which also keeps inject from resetting the active group). Refs #848 * fix(text-injection): harden XWayland repair against bad reads and non-GNOME sessions Three Greptile P1s on f005b6c: - _read_gnome_input_sources_key now returns [] for a successfully read empty list, reserving None for unreadable keys. _get_gnome_xkb_keymap bails when the xkb-options read fails instead of treating it as an empty list, so a transient gsettings failure can no longer make the repair clear live XWayland options via 'setxkbmap -option ""'. - _get_gnome_xkb_keymap restores the GNOME-session guard the previous source reader had, so a stale org.gnome.desktop.input-sources schema on another Wayland desktop is never applied to that desktop's XWayland server. - _query_xserver_xkb_rules catches OSError, not just FileNotFoundError: launch failures like PermissionError run inside inject_text's finally and must not surface after a successful commit. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
8ebc4ce2ba |
feat(diarization): per-speaker transcripts via TinyDiarize + --transcribe-file (#884)
* feat(transcribe): add TinyDiarize file transcription surface Catalog small.en-tdrz (hosted at akashmjn/tinydiarize-whisper.cpp, pinned commit) while keeping it out of every dictation-model selection path: its [SPEAKER_TURN] markup would type noise into the focused window. The checksum generator resolves side-repo models at their own pinned revision, parakeet-style. Add speech_recognition.diarization (strict-WAV fast path with ffmpeg fallback, tdrz transcription, speaker-turn parsing, timestamped formatting) and surface it through a tray Transcribe Audio File flow with on-demand model download plus a headless --transcribe-file CLI. TranscriptDialog offers copy and save of the diarized transcript. Closes #756 * docs(user-guide): document --transcribe-file * fix(transcribe): pass tdrz_enable as a pywhispercpp kwarg pywhispercpp's Model takes decode params as keyword arguments; a params= dict crashes on the C-level whisper_full_params object. Also let --transcribe-file report unexpected failures cleanly instead of dumping a traceback. * fix(tray): dismiss the tdrz download dialog on response * test(diarization): pin real wave/numpy/tempfile around sibling mocks Several test modules replace sys.modules entries with MagicMocks at import time and never restore them, so diarization's wave/numpy/ tempfile bindings resolved to mocks when this file ran after them in CI. An autouse fixture reimports the real stdlib modules per test, the same remedy test_audio_feedback already applies for wave. * fix(transcribe): verify tdrz model, preload libs, gate formats on ffmpeg - Hash the on-disk TinyDiarize model against the pinned digest before native code maps it, matching the dictation load path - Extract the bundled-lib preloader to utils/pywhispercpp_loader.py and preload before the lazy pywhispercpp import so the headless --transcribe-file path works on source-built installs without RPATH - Offer only WAV in the file picker when ffmpeg is absent; name the dependency in the filter label - Type the new tray/diarization callbacks; document --transcribe-file and the new modules in AGENTS.md * style(audio): finish type annotations on capture helpers and download path _setup_alsa_error_handler now declares Optional[Any] and its ctypes callback gets concrete parameter types; the remaining capture helpers take audio: Any (PyAudio has no importable type) and test_audio_input's device_index becomes Optional[int], which also clears the preexisting implicit-Optional error. _download_whispercpp_model gains -> None and its HF url is narrowed to str so the concatenation and download call stop adding new mypy findings. * style(tests): wrap long monkeypatch line in preload test * refactor(main): narrow transcribe-file errors and add file context * style(ui): annotate TranscriptDialog constructor * fix(diarize): log worker tracebacks, isort test helpers, document --transcribe-file --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
b4babcaeca |
feat(packaging): AUR -bin package publishing with per-arch digests (#879)
* feat(aur): add vocalinux-bin PKGBUILD shipping the AppImage Closes #817 * fix(aur): hard-dep injection tools, pin real digests, gate the -bin package * fix(aur): pin both-arch -bin digests and build the binpkg as builder updpkgsums in the publish action only regenerates the x86_64 sum array, leaving aarch64 stale after every tag; release.yml now pins both AppImage digests itself and publishes with updpkgsums off. The AUR gate also created $WORKDIR/binpkg as root after handing WORKDIR to builder, so makepkg could not write there. AUR.md now matches the published -bin package. * fix(ci): wait for aarch64 AppImage before publishing vocalinux-bin publish-aur only needed build-and-release, so on a tag the digest-pinning step could curl the aarch64 AppImage before build-appimage-arm64 attached it — a 404 that failed the job between the source and -bin publishes. Split the -bin bump/pin/publish steps into publish-aur-bin, which needs [publish-aur, build-appimage-arm64]: the source package still ships as early as before while the binary package only runs once both AppImages exist on the release. * fix(ci): ship -bin local sources and wait for the aarch64 asset on AUR publish Split the -bin bump/pin/publish into publish-aur-bin needing [publish-aur, build-appimage-arm64] so the digest-pinning step can no longer curl a 404 for the not-yet-attached aarch64 AppImage mid-release, and publish with asset_dir so the PKGBUILD's local sources (vocalinux.desktop, vocalinux.svg, LICENSE) reach the AUR repo instead of a lone PKGBUILD users cannot build. * style(tests): apply black formatting to aur packaging test --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
5c031e64fb |
feat(settings): bilingual dictation support with deferred settings edits (#424)
* feat: add configurable Whisper language candidates * fix(settings): defer language candidate edits and tolerate list config The candidates entry applied settings on every keystroke, restarting the whisper.cpp model per character, and edits typed while an apply held the guard were dropped so the saved value could differ from the entry. Defer application like the initial prompt: mark edits dirty and apply on page change or close; _finish_auto_apply re-applies edits that landed mid-apply, and _persist_pending_text_edits saves them if the dialog closed first. A JSON list in config also crashed the dialog on open because Gtk.Entry.set_text needs a string; populate the entry through the engine's _normalize_language_candidates so both stored forms render the same text. * fix(recognition): validate whisper.cpp language candidates before selection auto_detect_language's probability map names every language the model can return, but candidate selection scored configured codes against it with a 0.0 default, so a typo or "auto" could still win the max() and reach transcription. Only codes present in the probability map are eligible now; unsupported codes are logged and skipped, and when none qualify the unrestricted detection result is used instead of forcing a code the model never scored. Also add the missing input annotation on _normalize_language_candidates. * fix(settings): persist deferred text edits off the GTK main loop _finish_auto_apply runs on the main loop and called _persist_pending_text_edits synchronously, so reconfigure() restarted the model in-process and froze the UI when the dialog closed mid-apply. Run the reconfigure + config save on a worker thread, matching the normal apply path. Also annotate _on_language_candidates_changed's signature. * fix(settings): guard deferred saves and annotate new candidate tests * fix(settings): reconcile the engine to winning settings after a mid-restart apply * fix(settings): drop superseded deferred edits instead of saving them A dialog closed mid-apply stashes its pending text edits for a worker to persist after the running apply finishes. The worker's mtime check could not tell the in-flight apply from a newer one started in a reopened dialog, so the older snapshot could reconfigure the shared engine and save over the settings that won. Serialize applies and the deferred persist on a shared lock and order them with a generation counter bumped when an apply begins. The worker now skips reconfigure and the save entirely when a newer apply has begun, and skips the restart when the running apply already landed the same values. * fix(settings): merge deferred text edits per key instead of dropping them Closing the dialog mid-apply stashes the typed prompt/candidate values with the apply's generation. When a reopened dialog applies newer settings first, the generation check used to discard the whole stash — losing the closed dialog's edits even where the newer apply never touched those keys. Persist keeps a pre-edit baseline per key: after a newer apply lands, only keys it actually changed are dropped; the rest still reconfigure the engine and save, so a stale value can never reach the shared engine while untouched edits are no longer lost. * refactor(settings): narrow deferred-edit persist exception with context * chore: retrigger ci --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
b71164a214 |
feat(tray): add recent dictation snippets history menu 🤖🤖🤖 (#487)
* feat(tray): add recent dictation snippets history menu Add a "Recent Snippets" submenu to the tray indicator that lists recent dictation sessions. Clicking a snippet copies its full text to the clipboard for easy re-pasting; a "Clear History" entry empties the list. Snippets are recorded on recognition, independent of whether text injection succeeds, so text can be recovered even when injection silently no-ops (e.g. on some Wayland compositors). One snippet corresponds to one dictation session (everything said between start and stop); voice commands are not recorded since they take a separate callback path. History is kept in memory only (newest-first, capped, never written to disk) and clears on quit — a deliberate privacy choice for a tool that sees everything dictated. Adds a "history" config section (enabled, max_items) and matching Settings UI under General. Includes unit tests for the new TranscriptionHistory store. * fix(history): sanitize saved max_items to keep startup robust A hand-edited config.json can store a non-numeric or out-of-range history.max_items. Coerce the value on load (and in TranscriptionHistory itself) with a positive-int fallback so a malformed preference warns and falls back to the default instead of aborting startup or crashing the Settings dialog spin button. * fix(history): fold late-arriving segments into their originating session stop_recognition() only waits on the recognition worker for a bounded time before reporting IDLE, so a slow final segment can still fire its text callback afterwards — landing in the next session's pending snippet, or starting a phantom entry of its own. Track the open session (state transitions, its worker thread, and its pending segments) under a lock and file each segment by origin: while a session is open only its own worker's segments accumulate into it; a segment arriving after finalization extends that session's committed snippet via the new TranscriptionHistory.extend_latest(), and a segment from a session that produced nothing forms its own entry instead of polluting the next one. ERROR now also finalizes the session, so a partial result is still recorded and the tracker never wedges open. Covered by new tests driving the callback and state wiring directly: mid-session LISTENING/PROCESSING transitions, back-to-back sessions, late callbacks from a lingering worker thread, disabled history, and invalid saved limits. * style(ui): type the new history handlers in tray and settings * fix(history): keep cleared snippets from reappearing via late segments The recognition worker can still emit a final segment after the manager's bounded stop wait reported IDLE. Once the user cleared the history, that late text could not extend the now-empty history and was appended as a new entry — resurrecting snippets the user explicitly removed. Give TranscriptionHistory a clear epoch: clear() (and disabling, which clears) bumps it even when the store is already empty. add() and extend_latest() accept an expected_epoch and refuse stale epochs atomically under the store lock, so the check cannot race the clear. main.py tracks the epoch each session opened under and the epoch of the most recently ended session. Session commits and every late-segment write are guarded by them, so text produced before a clear — whether a session's whole snippet, its straggler segments, or leftovers committed on the next session's start — cannot re-enter history afterwards. * test(history): cover clear-epoch rejection and type new signatures Add regression tests for the cleared-history race: a late segment after clear() must not reappear, a session ending after a mid-session clear leaves no snippet, and add/extend_latest refuse a stale expected_epoch while unguarded writes keep working. Annotate the new signatures per the repo guide: _boot's extra_settings and return tuple, every test in the new TestSessionHistoryRecording and history-menu tray classes, and all methods of the history store tests. * fix(main): keep post-clear dictation by rebinding the session epoch * fix(history): drop the boundary segment when a clear lands mid-session * fix: scope dictation history to post-clear audio via capture-time stamps The engine now tags each queued audio segment with the monotonic time capture began and reports it to a new segment_callbacks hook. History recording drops segments whose capture started before the last clear, so pre-clear audio that decodes late can no longer resurrect cleared entries or leak into a later session, while text dictated after a mid-session clear is kept instead of being discarded by a stale epoch check. * fix(settings): keep Test Dictation speech out of transcription history The mic test swaps text callbacks for its preview, but history recording runs on segment callbacks, so test speech still landed in Recent Snippets. The test now saves and clears the segment callback list alongside the text list — restoring both on success and on the start-failure path. register/unregister_segment_callback also gain the missing return annotations and the engine gets symmetric get/set_segment_callbacks accessors. * fix(settings): floor test-dictation speech instead of swapping callbacks Instead of clearing the segment callback list during the mic test — which would have dropped leftovers from a still-finishing dictation — the engine exposes test_capture_floor: segments captured at or after the stamp are test speech and stay out of Recent Snippets, while earlier-session leftovers continue to file normally. * refactor(history): narrow change-callback exception surface --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
f36c3f6ae5 |
feat(text_injection): add RemoteDesktop portal injection backend (#885)
* feat(text_injection): add RemoteDesktop portal injection backend Wayland offers exactly one sanctioned key-injection path: org.freedesktop.portal.RemoteDesktop's NotifyKeyboardKeysym. It reaches native clients on compositors with no input-method-v2 bridge, works from inside the Flatpak sandbox without /dev/uinput or a helper daemon, and delivers keysyms so non-US layouts survive -- unlike ydotool's evdev keycodes. RemoteDesktopPortal is a synchronous client over a dedicated worker thread holding a private bus connection and GLib context, which keeps Request::Response delivery deterministic. Sessions are created lazily on first use so startup probing only reads the interface version; v2 portal sessions persist a restore token (mode 0600 under data_dir) so the permission prompt appears once. Backend selection now prefers the portal on Wayland: it is probed at runtime ahead of ydotool/wtype/xdotool in autodetection, is pinnable via VOCALINUX_FORCE_BACKEND=portal or text_injection.backend, and demotes to the previous order on runtime failure. Flatpak still needs --device=all for evdev hotkeys, but uinput is only a fallback path now. Closes #750 * fix(text_injection): resolve Greptile findings on RemoteDesktop portal injection - Move persist_mode/restore_token to SelectDevices options (portal v2 sends them there, not CreateSession); a bad token is ignored rather than sinking the session, so drop the stale-token retry paths that could issue a second Start after a user cancel - Track delivered units on RemoteDesktopPortalError so Wayland fallbacks retry only the undelivered tail instead of retyping the prefix (text chars, backspace taps, shortcut steps) - Give queued portal jobs a single deadline budget across CreateSession/SelectDevices/Start waits; on submit timeout the job is cancelled via a Gio.Cancellable plus abort checkpoints so the worker cannot keep emitting keys after the caller gave up - Close sessions whose Start never succeeded - Release held shortcut modifiers on mid-step failure - Skip the portal probe when an installed wtype/ydotool backend is explicitly pinned - Type annotations on the new signatures and touched tests --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
24fddc4344 |
chore(release): add snap-promote workflow for gated stable releases (#881)
* chore(release): add snap-promote workflow for gated stable releases Issue #783 option 2 (safer): v* tags keep auto-publishing the snap to edge + candidate in release.yml, and latest/stable is promoted by a manual snap-promote.yml dispatch after candidate QA. The dispatch resolves the revision on latest/candidate and refuses to promote unless its version matches the tag, so a typo or stale candidate fails the run instead of shipping the wrong revision. Declaring the snap-stable environment with required reviewers adds an approval gate on top of the dispatch. Fixes #783 * fix(ci): serialize snap promotions and re-verify candidate awk used the gawk-only \S which mawk reads literally, so the latest-track candidate row never matched. The promote step also trusted a revision resolved minutes earlier, letting an overlapping promotion downgrade stable. Serializes dispatches and re-resolves the candidate right before release. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
5e531cbac0 |
refactor(audio): extract capture source from recognition manager (#883)
* refactor(audio): extract capture source from recognition manager Move audio capture out of the silence-segmented dictation loop into vocalinux.audio.capture: a reusable PortAudioCaptureSource that owns device resolution, format negotiation, downmixing, resampling, and stream lifecycle (open/read/reopen/close). The dictation loop now consumes 16 kHz mono chunks and keeps only segmentation policy (VAD, silence counters, buffer limits, reconnection backoff). This unblocks alternate capture sources (PipeWire system-audio capture for #751, tdrz long-form output for #756) which need the same capture mechanics without dictation's silence segmentation. Behavior is identical: the same helpers run in the same order, moved verbatim. They are re-exported from recognition_manager for existing imports, and a CaptureSource protocol is declared in common_types.py. Tests repoint their patches of the moved helpers to the new module. Closes #760 * fix(audio): annotate capture API and keep a superseded session's source * fix(audio): reset retry counters when a new capture session starts * fix(audio): keep last-error timestamp across capture sessions The new-session reset wiped _last_audio_error_time, so an error within five seconds of the previous session's error always took the reconnect path instead of failing fast. Only the attempt count is per-session; the timestamp is also the rapid-failure throttle. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
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> |
||
|
|
7b69ac2f46 |
fix(site): Phone chip, Free forever Cost copy, and privacy GA disclosure (#868)
* fix(site): align Phone chip, Cost copy, and privacy with GA Use the official Android beta / iOS TestFlight chip on #ecosystem. Replace Free forever with Free and open source on privacy and open-source. Disclose vocalinux.com Google Analytics and scope no-telemetry claims to the installed app. Fixes #867 * fix(site): make privacy GA disclosure match the live gtag Spell out page views, the client-ID cookie, and IP/device processing for G-7NBBNJNNQ7. Align Data Collection with PRODUCT.md, drop the hidden telemetry weasel, and correct config.json plus the voice-storage row. |
||
|
|
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> |