d53300841d323a892a11bfc408f2be2de1a209ac
106
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
7bdbedce0b |
feat(language): follow the active keyboard layout while dictating (#837)
* feat(language): follow the active keyboard layout while dictating Adds a "Follow keyboard layout" switch to Settings -> Speech Model -> Advanced. With it on, the active keyboard layout is re-read at the start of every dictation and the matching language is used, so someone who switches layouts to work in two languages gets the right one without opening Settings. #796 already seeded the language once on first run, but from the *configured* primary layout, which never changes as the user switches between them: a "fr,us" setup reports fr forever. detect_active_keyboard_ layout() reads GNOME's input sources instead. It takes mru-sources rather than the current index, because that index goes stale -- observed still reading 0 after a switch to the second layout -- which is why the IBus injection path already trusts the MRU list (#497, #738). An active source that is an IBus engine rather than an xkb layout is a real answer that cannot be mapped, so the language is left to the engine's own auto-detect. Borrowing the configured xkb layout there would pin English for someone typing Japanese through mozc-jp. The mode is a switch over the language picker rather than an entry inside it, since it is a mode and not a language. While on, the picker is disabled and shows what the layout resolves to; turning it off pins that. A language change no longer re-initialises the engine when the loaded model takes the language per utterance, which is the whole whisper family, so following a layout costs a settings read rather than a model reload. VOSK loads one model per language, Parakeet ignores the picker, and an English-only model has no non-English weights, so Settings refuses the mode for all three rather than offering a switch that does nothing. Fixes #821 * fix(language): keep follow mode armed and map IBus engines Swap English-only whisper.cpp weights to the multilingual sibling while passing the layout sentinel into reconfigure, so follow mode stays armed. Map common GNOME IBus engines onto catalogue languages instead of leaving them unmapped. * fix(language): stop IBus hyphen false-maps and gate the sibling swap The last-token hyphen heuristic mapped mozc-us and foo-us to English. Restrict it to *-jp/*-kr and name Mozc's extra engines as Japanese. Only whisper.cpp may consult the whisper.cpp download catalog. * fix(language): keep multilingual weights while following layout The picker shows the active layout while follow is on, often en-us. Deriving whisper.cpp .en from that display made Settings refuse the mode as English-only, then the next auto-apply persisted a concrete language and disarmed follow. Use LANGUAGE_FOLLOWS_LAYOUT for .en vs multilingual derivation instead, including on the load path before the switch is synced. * fix(language): type follow helpers and log sibling-swap failures * fix(test): stub follow-layout model rec so mocked psutil does not blow up follow-mode English-only guard calls get_recommended_whispercpp_model, which reads RAM through psutil. Full CI leaves a MagicMock in sys.modules["psutil"], so ram_gb >= 16 raises TypeError. Patch the settings_dialog alias to return a multilingual tiny rec so the assertion still holds without real RAM. * fix(language): catch specific errors on layout lookup and sibling swap * fix(language): swallow more sibling-swap reconfigure failures ImportError, TypeError, AttributeError, and ChecksumError from the multilingual swap must not abort dictation or disarm follow mode. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
f4e86f6325 | fix(deps): clear open dependency advisories (#869) | ||
|
|
5b4e2c3572 |
fix(snap): add hardware-observe so hotkeys can read input devices (#858)
Snap confinement denies /proc/bus/input/devices unless hardware-observe is connected. Declare the plug, tell users to connect it next to raw-input, and update the in-app hints. Fixes #857 |
||
|
|
acd90fdd05 |
fix(installer): install hash-pinned dependencies and build tools (#856)
* fix(installer): install hash-pinned dependencies and build tools * fix(installer): add cairo-devel for the pinned pycairo build * fix(installer): reject missing or empty bootstrap exports * fix(installer): preserve remote behavior for older tags --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
1f121546fb |
fix(release): attach the snap to GitHub Releases (#839)
* fix(release): attach the snap to GitHub Releases v0.17.0 packed the snap, then Store review of uinput failed the job, so the file never reached the tag. Build and attach first. A Store hold is a warning, not a failed release. SHA256SUMS covers the .snap. snap-backfill.yml rebuilds an existing tag, appends the snap to the manifest, and attests only that file. * fix(release): keep snap tags out of the shell and fail real Store errors Pass the tag through the environment like verify-release.yml. Treat only an allow-installation review as a successful Store publish. snap-backfill runs verify_release.py after it rewrites SHA256SUMS. * style(test): format snap-backfill shell-injection guard with black |
||
|
|
26f90b6998 |
chore(release): prepare v0.17.0
Minor on the stable line. Faster Whisper and Parakeet extras, Speech Model simple setup, Snap uinput, Flatpak bundles on the tag, and QA fixes from the release branch. Fixes tracked in the PR. Post-release follow-ups: #834. |
||
|
|
4b6e81d441 |
fix(snap): type into native Wayland apps with ydotool (#823)
* fix(snap): inject into native Wayland apps via ydotool Edge rev 7 only ships wtype and xdotool. GNOME rejects the virtual keyboard protocol, so the snap falls back to xdotool and types only into XWayland windows. Native GTK/Qt apps get nothing. Stage ydotool, request the uinput plug, and prefer ydotool when wtype is rejected. Distro ydotool 0.1.x still needs a writable /dev/uinput; log `snap connect vocalinux:uinput` when the Snap cannot open it. * docs(snap): v0.16.2 edge has no uinput plug The store snap (0.16.2, latest/edge rev 7) does not declare uinput. `sudo snap connect vocalinux:uinput` fails with "has no plug named uinput" and dictation only reaches XWayland apps. Spell that out in README, INSTALL, UPDATE, the website changelog, and the store copy so people do not run the 0.17 connect command against rev 7. * docs(snap): drop the unconfirmed 0.17 version from uinput notes Greptile P2: the tree is still 0.16.2 and the release process does not name the next tag. Tell people to refresh until `snap info` lists the uinput plug, instead of waiting on a version we have not cut. |
||
|
|
f48bdda525 |
docs: improve project documentation for a clearer public surface (#634)
* docs: polish project documentation for a clearer public surface Rewrite the README and supporting docs for a more professional first impression: less emoji and badge noise, install-first structure, and a real documentation index. Add a Contributor Covenant, tighten SECURITY and GitHub templates, and make distro/update guides read as product docs instead of internal status notes. * docs: split install guide and add changelog plus support pages Break INSTALL.md into a short primary guide, manual/PyPI package lists, and troubleshooting. Add root CHANGELOG.md and SUPPORT.md, wire them into the docs index and README, and drop the outdated screenshot lag note now that v0.15 assets are on main. * docs: humanize PR copy and remove em dashes Strip em/en dashes from the documentation rewritten in this PR, fix mid-sentence colon fallout from that pass, and smooth a few stiff phrases so the public docs read more natural. * docs: list Faster Whisper after rebase onto main Installer and CLI already accept faster_whisper. Document it next to the other engines, drop the outdated screenshot lag note, and keep the on-device wording honest for local vs remote. |
||
|
|
7664266823 |
feat(commands): localize punctuation voice commands (#640) (#642)
* feat(commands): localize punctuation voice commands by language Voice command phrases were English-only even when recognition language was set to Italian, French, etc. Merge locale punctuation/line-break aliases from the recognition language into CommandProcessor, keep English phrases available, and document the behavior. Fixes #640 Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * fix(commands): keep line breaks and French apostrophe aliases Attach-left punctuation now eats only horizontal whitespace, so a localized "new line" followed by a period or comma still inserts a newline. French aliases include ASCII, U+2019, and backtick apostrophes, and longest-match still wins so "point" cannot fire inside "point d'interrogation". --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
2c6acbc15e |
fix(inject): layout-aware clipboard paste for Neo and friends (#788)
* fix(inject): resolve paste chord for non-QWERTY layouts ydotool was sending physical KEY_V (47) for Ctrl+V. On German Neo that key types p, so paste opened Print instead. Prefer wtype keysyms when usable; otherwise resolve the evdev key for Latin v via xkbcommon, and read KDE kxkbrc so Plasma Wayland Neo users are covered. Fixes #787 * fix(inject): read Plasma active layout from real session sources kxkbrc has no CurrentLayout/LayoutIndex, so multi-layout Neo always fell back to us at index 0 and paste still hit KEY_V. Prefer D-Bus getLayout or layout_memory.xml for the active layout, keep kxkbrc for the list only, and prefer KDE over leftover GNOME on Plasma. |
||
|
|
46c7914639 |
feat(ci): verify the published release, not just the workflow (#791)
* feat(ci): verify the published release, not just the workflow * fix(ci): filter attestations by predicate, name a missing distribution Two findings from review on #791. The attestations query accepted any predicate, so an SBOM would have read as build provenance while `gh attestation verify`, the command the notes hand users, rejected the artifact. It now sends the same `predicate_type=https://slsa.dev/provenance/v1`. Checked on a real digest: the SLSA filter returns the bundle, a foreign predicate returns 404. check_pypi compared whatever distributions were left, so a release that lost its sdist passed on the wheel alone. Each expected suffix is now named when absent. The lazy urllib import moved past the early return, where it belonged. The other two findings do not hold. `workflow_run.head_branch` carries the tag for a tag-triggered run: the last four release runs report v0.16.2, v0.16.1, v0.16.0 and v0.15.0. And `host_env()` is for the app spawning host binaries out of an AppImage; `scripts/` is in no `packages` entry and the AppImage build never references it, so this cannot run from a bundle. * fix(ci): read release digests from the REST API gh release view --json assets omits digest on gh 2.46, which is what many distros ship and what just verify-release often runs, so every asset failed as undigested. Pull the release through gh api instead, treat a null body as notes FAIL, and prefer removeprefix for the tag. * fix(ci): fail verify when PyPI has an extra distribution Matching release digests alone left a differently named wheel or sdist on PyPI unchecked. Reject those extras too, same suffixes. * fix(test): clear tempfile mock before urllib import Other suites leave a MagicMock in sys.modules['tempfile'], which breaks urllib.request import with a metaclass conflict in the extra PyPI distribution check. Drop the stub (and stale urllib modules) first, and assert the full problem list. * fix(ci): restore real tempfile before urllib in verify test The MagicMock tempfile left by recognition_manager still broke urllib.error on CI. Put the real module back and drop cached urllib packages before the import. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
50ea1109d7 |
feat(snap): Snap packaging recipe and Snap Store publish strategy (#519)
* feat(snap): add Snap packaging recipe and store publish strategy Add a strict-confinement snapcraft.yaml aligned with the current app version and Flatpak sandbox lessons (mic, desktop, network models, xdotool/xsel injection). Document human-only Snap Store steps and mark install docs as not published yet. Refs #48 * fix(snap): adopt-info so craftctl can set version from version.py snapcraft pack failed: craftctl set version requires adopt-info on the part. * fix(snap): stop staging GTK/GLib over the gnome platform Prepending snap-staged GI typelibs and lib paths caused a segfault on from gi.repository import Gtk. Use platform AppIndicator/GTK and only stage PortAudio plus xdotool/clipboard helpers. * docs(snap): note platform GTK path to avoid GI segfault * fix(snap): route ALSA through Pulse so Settings does not abort PortAudio enumerates ALSA devices when Settings opens. Missing /usr/share/alsa/alsa.conf inside the snap triggered a PortAudio assert. Ship an asound.conf that uses the pulse plugin, stage libasound2-plugins, and bind ALSA paths via layout. * fix(snap): keep ALSA conf under snap/local for snapcraft Move asound.conf out of snap/alsa so snapcraft stops warning about non-snapcraft paths in the snap directory. * fix(snap): request raw-input for global keyboard shortcuts Strict confinement blocks /proc/bus/input and /dev/input, so Wayland evdev hotkeys never start. Declare the raw-input plug (manual connect) and print snap-specific connect instructions instead of host input-group hints. * fix(hotkeys): discover keyboards via evdev when /proc is blocked Snap raw-input grants /dev/input/event* but still denies /proc/bus/input/devices, so find_keyboard_devices() returned empty and hotkeys never started. Fall back to evdev.list_devices() when /proc cannot be read. * fix(snap): restore UI sounds and prefer wtype for Wayland inject Stage paplay (pulseaudio-utils) and wtype; teach audio_feedback about pw-play/canberra. xdotool-only injection often hits the wrong window on native Wayland even when it reports success. * fix(snap): sync 0.16.2/AGPL metadata and AppImage-safe audio env Align snapcraft fallback version and store metadata with current main (VocaHQ URLs, AGPL-3.0), keep docs pointing at #48 on VocaHQ, pass host_env to pw-play/canberra paths, and document ordered Snap Store human deploy steps. * test(snap): harden audio host_env unit test against suite pollution Patch os.path.exists like sibling audio_feedback tests instead of relying on a real tempfile path that can collide with other suite mocks. * style(snap): black-format new audio and snap packaging tests * docs(snap): treat Store listing as live; edge→candidate→stable refresh Name vocalinux is already on snapcraft.io (edge @ 0.14.0-beta rev 6). Document upload/release without re-register, add snap-store-refresh.sh helper, and point INSTALL/README/DISTRO at the live listing. * fix(snap): move to core24 and close evdev capability FDs core22 only ships Python 3.10, which fails requires-python >=3.11, so the snap base is now core24 (Python 3.12) with the gnome-46 platform and the Ubuntu 24.04 libasound2t64 package. Also close InputDevice after failed capability scans and annotate the proc parser with TextIO. * Snap: align store listing metadata and icon Match snapcraft.yaml summary, description, license, website, and store icon to the live Snap Store listing. * Snap: trim packaging docs, script, and unused SVG Drop store refresh wrapper, long packaging README, duplicate snap/gui SVG, and speculative pw-play/canberra paths; keep recipe and confinement fixes. * ci: publish snap to edge and candidate on release tags * Snap: set grade stable for candidate channel CI * Snap: skip pip PyGObject; use gnome extension GI |
||
|
|
bb2b098193 |
chore(release): prepare v0.16.2 (#782)
Bump version metadata and series-aware notes for the 0.16.2 patch: KDE leftover IBus, Wayland/IBus shortcuts, BackSpace delete that, installer glslc, nightly stamp, release integrity, and CI/docs gates. |
||
|
|
a46000e249 | chore(ci): test the distros the docs promise, and fix the docs that drifted (#773) | ||
|
|
e61133e246 |
feat(ci): gate AUR PKGBUILD builds on every PR (#772)
* feat(ci): gate AUR PKGBUILD builds on every PR Nothing ever ran makepkg on the PKGBUILD before a tag pushed it to AUR users, and the channel broke twice that way (#736, #757) — a PKGBUILD change did not even start a CI job, because no paths filter matched packaging/aur/**. The gate (packaging/aur/build-test.sh, run in archlinux:latest — the AUR is a rolling channel, so the image stays deliberately unpinned): - swaps the tag-archive source= for a git archive of the checkout under the same filename and prefix, so build()/package() run unchanged and the gate answers "does this commit build on Arch" - installs depends/makedepends from the repos; python-pywhispercpp (virtual, provided by the AUR backends, #579) and python-pynput (AUR) are satisfied by a stub so makepkg still verifies the full depends set, and pip-installed from the hash-pinned requirements/runtime.txt for the import smoke - runs namcap on the PKGBUILD and the built package, failing on errors only (its warnings are noise around lazy imports and virtual depends) - installs the package and imports the whole UI/recognition stack, plus the vocalinux --version entry point Part of #701 (phase 6). * fix(ci): stop partial-upgrading in the AUR gate and correct the #736 attribution |
||
|
|
0ba029b015 |
chore(release): make a release verifiable, and pin what builds it (epic #701, phase 5) (#759)
* chore(release): build once, and publish sha256sums and provenance (epic #701, phase 5) * chore(ci): pin every third-party action by commit sha (epic #701, phase 5) * test(ci): anchor the dependabot guard on the ecosystem it must cover |
||
|
|
21b8ee8d05 |
fix(aur): build on Arch setuptools 84 and skip 1.4 context_params (#757)
AUR --no-isolation uses extra python-setuptools 84, so the <82 cap blocked the 0.16.0 rebuild. AUR pywhispercpp backends are still 1.4 and crash if we pass context_params; skip that argument below 1.5. |
||
|
|
68b00bd7d8 |
chore(release): prepare v0.16.1 (#755)
Bump the stable line from 0.16.0 to 0.16.1. README and UPDATE keep the 0.16 series highlights and add the patch-only fixes. Website changelog, schema, AppStream, and AUR PKGBUILD follow. |
||
|
|
65381b4582 |
fix(appimage): build against a glibc floor users actually have (#743)
* build(appimage): pin the interpreter, packages and tooling * ci(appimage): build inside a pinned ubuntu 22.04 image * test(appimage): guard the pins and the base image * docs: state the AppImage glibc floor * fix(appimage): bundle libibus with its typelib * ci(appimage): let the container run the isolated GI smoke test * fix(appimage): keep the bundle's libraries out of host binaries * fix(appimage): bundle libgpg-error with the libgcrypt that needs it * docs(agents): require host_env() for spawned binaries * ci: run the AppImage jobs when its other inputs change * fix(appimage): check every bundled typelib, not just the seeded ones * fix(ibus): keep the bundle when respawning our own interpreter * fix(injection): pass host_env() to setxkbmap and notify-send * test(ibus): wait for the IBus warmup thread before asserting |
||
|
|
008fd3138e |
fix(injection): use Ctrl+Shift+V when pasting into terminals (#734)
* fix(injection): use Ctrl+Shift+V when pasting into terminals Clipboard injection always sent Ctrl+V, which most terminal emulators ignore. Detect standalone terminal windows and send Ctrl+Shift+V, with a Settings override for nested IDE panels that window-class detection cannot see. * fix(injection): fail closed when terminal window probe is mocked Clipboard tests patch text_injector.subprocess.run, which replaces stdlib subprocess.run. Window detection now ignores non-string probe output so injection still uses Ctrl+V instead of raising TypeError. * docs: fix overclaimed terminal auto-detect in USER_GUIDE Auto-detect only runs on X11 and on Hyprland, Sway, and niri. GNOME and KDE Wayland stay on Ctrl+V unless you set Clipboard Paste Shortcut to Ctrl+Shift+V. |
||
|
|
582d494633 |
chore(build): uv tooling, a Python 3.11 floor, and verified model downloads (epic #701, phases 2.5 & 5) (#713)
* chore(build): migrate justfile tooling to uv All recipes now run through 'uv run' against .venv, with a shared DEV_EXTRAS variable (dev + vad) because uv syncs the environment exactly and would otherwise uninstall the dev tools between recipes. 'deps' installs dev+vad; new 'deps-all' covers every extra. 'build' switches to 'uv build'; 'version' uses --no-sync. Bump [tool.uv] required-version to >=0.12,<0.13 for uv 0.12.x, and raise the mypy python_version target to 3.12: mypy 1.19 in the lock cannot target 3.9 and numpy 2.x stubs need a 3.12 target to parse (mypy is not run in CI or pre-commit; this only affects 'just typecheck'). * fix(tests): isolate test runs from real XDG state; fix Arch model-path crash Local test runs failed en masse (185 failures) while CI stayed green: tests picked up the developer's real ~/.local/share/vocalinux models via the default XDG paths and switched IBus/keyboard code paths through the desktop session env (XDG_CURRENT_DESKTOP/ XDG_SESSION_TYPE on GNOME/Wayland, issue #478 branches). conftest now points XDG base dirs at a throwaway session-wide directory and clears session variables at import time, mirroring a fresh CI runner (paths like MODELS_DIR freeze at first import during collection, so fixture-time isolation is too late). Also guard the Arch branch of _get_system_model_paths(): it removed /usr/local/share/vocalinux/models unconditionally and crashed with ValueError on Arch hosts when a custom XDG_DATA_DIRS never added that path. Add regression tests, and make the lock-file test assert the actual $XDG_DATA_HOME/vocalinux contract instead of the hardcoded ~/.local/share default. * fix(tests): keep the IBus engine double a plain class Python 3.14's unittest.mock re-instantiates a user subclass when it builds child mocks (klass(name=...)). VocalinuxEngine inherits from whatever IBus.Engine resolves to, and the test module bound that to the MagicMock class itself, so every engine instantiation raised TypeError: VocalinuxEngine.__init__() got an unexpected keyword argument 'name' on 3.14 while passing on 3.13. Replace it with a plain stand-in class that still hands out per-instance MagicMocks for the IBus API surface the engine touches. The same pattern remains in test_ibus_engine.py, test_ibus_engine_utils.py, test_ibus_active_engine_guard.py and test_ibus_xkb_wayland.py; those never instantiate the engine, so they are unaffected for now. * fix(install): build the venv from the system Python install.sh looked up a bare `python3` from PATH. Started from an activated virtualenv — a shell left in uv's .venv after `just deps` — it built venv/ from that interpreter, where distro PyGObject is invisible even with --system-site-packages, and the run aborted with Distro PyGObject (python3-gi / python3-gobject) is not importable in the venv. A mismatched venv/ then survived every retry, because the installer reuses whatever venv it finds. - drop an inherited VIRTUAL_ENV (and its bin from PATH) before any interpreter lookup - select the interpreter explicitly: $SYSTEM_PYTHON (default /usr/bin/python3) or PATH python3, preferring one that can import gi, and create the venv with it instead of `python3` - recreate venv/ when it was built by a different interpreter - name the interpreter in the PyGObject guard's error Documents the two environments (uv's .venv for tooling, venv/ for the installed app) in CONTRIBUTING, the justfile header and AGENTS.md, and adds the mismatch symptom to the INSTALL troubleshooting section. * chore(python): raise the supported floor to 3.11 requires-python promised 3.9 while the CI matrix ran 3.9/3.10/3.11/3.13, the classifiers advertised up to 3.14, mypy targeted 3.12 and install.sh enforced 3.9 — five different answers to "which Python does this support". Settle on 3.11 everywhere: pyproject (requires-python, classifiers, black targets), install.sh's MIN_VERSION, check-system-deps.sh, the distro package map, the CI matrix (now 3.11-3.14, matching the classifiers), the release and nightly workflows, and the docs. mypy now targets the floor instead of 3.12, so type checking covers what we promise; numpy 2.x ships stubs in 3.12 syntax that a 3.11 target cannot parse, so its imports are skipped. (mypy has 71 pre-existing errors either way; no CI job gates on it.) uv.lock and the requirements/* exports are regenerated — dropping 3.9/3.10 removes most of the version-conditional resolution branches. tests/test_python_version_policy.py pins the invariant: install.sh's floor, requires-python, the classifiers and the CI matrix must agree. * ci: run lint and tests from uv.lock The lock file and the hash-pinned exports were not enforced anywhere: CI still built a venv by hand and installed unpinned `PyGObject` plus `-e ".[dev,vad]"`, so the tool versions it checked against had nothing to do with the ones `just lint` uses locally. Both Python jobs now use astral-sh/setup-uv and run through `uv sync --locked` / `uv run --locked`, which also fails the build when uv.lock drifts from pyproject.toml. Partially covers phase 5 of #701 (tests install from the lock, lock freshness gate); trusted publishing, artifact checksums and action SHA pinning are still open there. * ci: lint from a dedicated dependency group The lint job failed on this branch: `uv run --extra dev` syncs the whole project, so it tried to build pyaudio from source on a runner that installs no system dependencies. src/pyaudio/device_api.c:9:10: fatal error: portaudio.h: No such file or directory Move black/isort/flake8 out of the `dev` extra into a `lint` dependency group. CI runs `uv run --locked --only-group lint`, which installs the three pure-Python tools and skips the project entirely — no portaudio or girepository headers needed. `just deps` and DEV_EXTRAS request the group alongside the dev extras, so the local environment still carries test and lint tooling at once and `just lint` matches CI version for version. pre-commit is unaffected: its hooks pin their own tool versions upstream. * fix(install): never configure an engine the venv cannot import vosk became an optional extra in #705, but the installer still writes `"engine": "vosk"` into config.json on the whisper.cpp fallback path even when installing the extra only produced a warning. config.json also survives reinstalls — every writer guards on "file does not exist" — so an engine picked by an earlier attempt outlives the venv that supported it. Either way Vocalinux dies at startup: ModuleNotFoundError: No module named 'vosk' - the fallback now writes the vosk config only when `import vosk` actually succeeds, and otherwise leaves the engine configuration alone - a new verify_configured_engine() runs near the end of the install: if config.json selects vosk and the venv cannot import it, it says so and prints both fixes instead of leaving a broken install behind * feat(models): verify every model download against a pinned digest Pins a sha256/md5 per downloadable speech model in src/vocalinux/utils/model_checksums.txt and verifies against it in both install.sh and the runtime downloaders, before the file is moved into place. Verification fails closed. Covers phase 2.5 of #701; see the PR for details. * docs(agents): rebase AGENTS.md onto the #727 rewrite * fix(install): hash models that are already on disk instead of trusting them * refactor(models): one download path per engine, and fix Whisper's * docs(install): drop distros that cannot meet the 3.11 floor * docs: retire the distros and Python versions the 3.11 floor rules out * fix(install): abort instead of warning when Python is below the floor * fix(install): fall back to the system Python, not the one on PATH * fix(install): match an existing venv by installation, not by X.Y * fix(install): repair a config naming an engine the venv cannot import * fix(models): pin VOSK by the sha256 of bytes we fetched, not upstream's md5 * fix(justfile): request the dev set in every recipe that syncs * test(version): scan the docs and website trees for a stale Python floor * docs: correct the stale model-checksum and distro claims * fix(models): stamp the VOSK tree the app extracts, as the installer does * fix(install): replace an unverified VOSK model only once its successor is ready * docs(install): align the Fedora floor with the rest of the tree * test(install): drop an f-string with no placeholders * test(models): stub the downloader probe the VOSK installer test relies on * docs: drop the long dashes from two model-checksum comments * fix(install): gate on the interpreter, not on the distro release label * fix(justfile): export the lint group into requirements/dev.txt * fix(models): unpack the VOSK tree into staging and stamp it before the swap * fix(whisper): adopt a checkpoint an earlier release stored under the catalog name * docs(settings): state what a stray Whisper copy really means for deletion * fix(models): hash a whisper.cpp model found on disk before it reaches ctypes * docs(contributing): stop telling readers to uninstall their linters * fix(install): stop warning off distros the project documents as supported * docs(distros): file the supported derivatives under supported |
||
|
|
ba9cca844b |
chore(release): prepare v0.16.0
Stable minor since v0.15.0. Bumps version to 0.16.0 across app, web, AUR, and Flatpak AppStream, and refreshes docs, changelog, and SECURITY. Tag v0.16.0 after this lands on main. |
||
|
|
994dda10a6 |
fix(install): leftover review notes from #700 and #705 (#706)
* fix(install): leftover review notes from #700 and #705 The scratch dir now holds pip logs and model download temps, so a failed run has something to inspect. Dep, venv, leftover-process, and pip failures use exit codes 2/3/4 instead of a blanket 1. --help says the transcript is written during installation, and the tag hint is --tag=<release>. AGENTS.md no longer claims the hash-pinned exports are consumed (they wait for phases 2/3/5). Docs add a just install line and vocalinux[vosk] next to the PyPI commands. Settings points at --engine=whisper. The whisper.cpp fallback installs ".[vosk]". Fixes leftover review notes from #700 and #705. Refs #701. * fix(install): do not let pip build PyGObject from sdist Fresh --auto --skip-system-deps died on pygobject-3.56.3 even with distro python3-gi present. Install the other runtime deps from pyproject.toml, then pip install --no-deps. Extra installs use the extra's packages only, not pip install ".[extra]". Fixes leftover review notes from #700 and #705. Refs #701. * fix(install): tolerate unset XDG_SESSION_TYPE under set -u --auto with DISPLAY but no login session died on XDG_SESSION_TYPE: unbound variable. The session probe now uses ${XDG_SESSION_TYPE:-}, ${WAYLAND_DISPLAY:-}, and ${DISPLAY:-}. |
||
|
|
95d135ebbf |
chore(deps): lock dependencies with uv and pin every build input
Phases 0-1 of #701. Drops dead deps, moves vosk to an optional extra, adds uv.lock and hash-pinned requirements exports, pins pywhispercpp to 1.5.0. Closes #705. Refs #701. |
||
|
|
71c84fe86b |
chore(build): harden installer script and replace Makefile with Justfile
Stacked bottom of #701. Replaces Makefile with justfile and hardens install.sh (strict mode, logging, HTTP probes, curl -f on model downloads, no v0.10.1-beta fallback). Closes #700. |
||
|
|
279d99fea0 |
fix(whisper): honor bundled GPU libs and skip software Vulkan devices (#674)
* fix(whisper): use bundled GPU libs and skip software Vulkan devices AppImage 0.15.0 shipped a CPU-only pywhispercpp wheel, so the Settings GPU picker and host vulkaninfo never moved inference off the CPU. Rebuild pywhispercpp with Vulkan in the AppImage, log the libraries that actually load, skip llvmpipe in auto-select, and document how to verify. Fixes #604 * fix(appimage): pin gcc/g++ for Vulkan pywhispercpp rebuild Hosts that default cc/c++ to clang fail CMAKE's C++ test because clang does not search gcc's libstdc++ path. Default the rebuild to gcc/g++. |
||
|
|
32a17a9659 |
chore: point GitHub URLs at VocaHQ (#696)
Repo links, install one-liners, and release checks now use github.com/VocaHQ/vocalinux. The site mentions VocaHQ without a redesign. The Codeberg mirror, personal email, and X handle are unchanged. |
||
|
|
ecd5839a13 |
feat(settings): delete unused downloaded speech models (#671)
* feat(settings): delete unused downloaded speech models Add a Remove Model dropdown on the Speech Model page so whisper.cpp, Whisper, and VOSK downloads can be deleted from Settings. The currently selected model is marked in use and cannot be removed until you switch away from it. Fixes #650 * fix(settings): move unused model deletion below the picker Unused downloads is its own group under the model info card. Only leftover files appear, each with a Delete button. The group hides when empty. * fix(settings): collapse unused downloads into an expander Keep leftover models behind one row so a long list does not stretch the Speech Model page. Expand to delete files one at a time; the list scrolls once it grows past a few entries. * fix(settings): keep leftover model list from clipping in the expander Wrap the unused-downloads list so the expander can show a few rows and scroll the rest, instead of painting only the first leftover. |
||
|
|
c337d8f56d |
feat(shortcuts): default to hold Right Alt (push-to-talk) for new installs (#648)
* feat(shortcuts): default to hold Right Alt push-to-talk on first install Double-tap Ctrl conflicts with many desktop and app shortcuts, so new installs now use hold Right Alt (Option) in push-to-talk mode. Existing config.json values are preserved unchanged. Updates README, user guide, AGENTS.md, first-run dialog, website copy, and tests to match the new defaults. * refactor(shortcuts): trim redundant default-change test and comments Drop the fresh-install assertion that duplicated DEFAULT_CONFIG coverage, remove the duplicated Right-Alt rationale from config_manager, and drop a vacuous source-string check from the settings shortcuts test. * fix(shortcuts): align install.sh seeds and preserve legacy toggle mode install.sh still wrote ctrl+ctrl without mode, so merges with the new push_to_talk default became hold-Ctrl. Seed right_alt + push_to_talk for new installs, and migrate existing shortcut-only configs to mode=toggle so prior behavior is unchanged. * style(config): fix black formatting after shortcuts migration * fix(shortcuts): preserve legacy defaults when shortcuts section is missing Existing config files without a shortcuts section previously inherited ctrl+ctrl toggle from DEFAULT_CONFIG. Pin those historical defaults on upgrade so the new first-install Right Alt push-to-talk defaults do not silently change behavior. * docs(agents): unwrap hard-broken dictation control paragraph Keep the AGENTS.md paragraph as a single line like the surrounding prose. |
||
|
|
06e8809c2a |
feat(updates): notify when a newer GitHub release is available (#645)
* feat(updates): notify when a newer GitHub release is available Check the configured release channel shortly after startup and every six hours. When an update exists, show a tray menu item and a green New badge on Settings → About, plus one desktop notification per version, so users can open release notes and update themselves. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * refactor(updates): slim UpdateMonitor after ponytail review Drop unused APIs (tick/check_now/version injector/last_* state), integer-only GLib scheduling, and the local demo launcher. Keep tray notify-send local to avoid coupling to recognition_manager. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * fix(updates): address Bugbot and harden notify persistence Persist last_notified_version only after notify-send spawns successfully, and sync the tray Update Available item from About-page checks without re-notifying. Also merge latest main and cover the new paths in tests. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * test(tray): pin config mock for update-notification assertions CI on 3.9/3.10 showed empty MagicMock set call history after a successful notify-send spawn. Pin tray.config_manager to the test mock and record set() via side_effect so persistence checks are reliable across Python versions. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * fix(updates): clear pending About update on failed lookup A failed About check hid the New badge but left _pending_update set, so clearing settings search could revive the badge while status still showed the failed lookup. Clear dialog pending state on failure (tray unchanged) and restore the badge via _set_about_update_badge. * fix(updates): ignore stale UpdateMonitor results after channel switch Bind the channel for each background check and discard the callback when the configured channel changed mid-flight, then re-check immediately — matching About-page stale-channel handling so the tray cannot show the wrong channel's update state. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
67dde781f5 |
docs(user-guide): note CUDA device 0 limit for dual NVIDIA (#644)
Document that CUDA-backed pywhispercpp always uses device 0, and point to CUDA_VISIBLE_DEVICES or a Vulkan build as workarounds. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
9c340029e1 |
docs(packaging): prefer Ayatana AppIndicator in Fedora/Arch hints (#638)
* docs(packaging): prefer Ayatana AppIndicator in Fedora/Arch hints Follow-up to #621: align docs, AUR depends, package map, dependency check messages, and troubleshooting copy with the Ayatana-first install path so manual installs and AUR users get a working KDE tray icon. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * style(main): black-format Ayatana install hint lines CI black --check failed on the multi-line logger.error calls for the Fedora/Arch package hints. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
fbb81238f5 |
chore(release): prepare v0.15.0 (#609)
* chore(release): prepare v0.15.0 Bump version across app, web, AUR, and Flatpak metainfo. Document GitHub release-notes rules, refresh README/UPDATE/INSTALL/USER_GUIDE for the 0.15 series (sidebar settings, AppImage, auto-pause/keepalive, Vulkan device selection), and align website product surfaces with shipped features. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * docs(release): fold latest main into v0.15.0 notes Include dictation capitalization/trailing-space, ibus-wayland injection, IBus teardown, and settings notice polish landed on main after the initial release prep rebase. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * docs(release): include Bluetooth mic crash fix in v0.15.0 notes Fold #599 (PortAudio Bluetooth SCO probe heap corruption) into README, UPDATE, changelog, AppStream, and release history after rebasing onto main. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * docs(release): include language catalog expansion in v0.15.0 notes Fold #616 (Hungarian and expanded speech languages, Fixes #565) into README, UPDATE, changelog, AppStream, PRODUCT.md, and release history after rebasing onto latest main. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * docs(release): fold settings footer and language follow-ups into v0.15.0 Include #618 sidebar dictation footer, #619 Custom Shortcut Record/Set restore, and #617 English (India) Whisper language mapping after rebasing onto latest main. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
40546e7f07 |
fix(uninstall): remove IBus data dir & stop app by PID file not pgrep -f (#597)
* fix(uninstall): remove IBus runtime data directory on uninstall Discussion #559 reported ~/.local/share/vocalinux-ibus surviving uninstall. PR #569 fixed launcher wrappers but not this IBus IPC directory (socket, PID file, stderr log). Stop the engine via engine.pid when present, remove the directory regardless of --keep-data, and verify it in post-checks. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * fix(scripts): stop Vocalinux by PID file, not pgrep -f pgrep -f vocalinux matches any process whose argv mentions the repo path (editors, shells, test runners) and can kill unrelated local work. AGENTS.md already warns against this. - Prefer PIDs from instance.lock and vocalinux-ibus/engine.pid - Verify each candidate via is_vocalinux_process (python -m vocalinux.main, ibus_engine.py under vocalinux, or argv0 basename vocalinux/vocalinux-gui) - Narrow pgrep fallback only for those entrypoint patterns - Drop the redundant stop_ibus_engine helper (engine covered via PID file) - Still remove ~/.local/share/vocalinux-ibus on uninstall (#559) - Align install.sh and correct the pkill tip in docs/UPDATE.md Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * fix(scripts): detect setuptools console-script Vocalinux processes Real installs run as `python …/bin/vocalinux`, so argv0 is python and the previous argv0-basename check missed the live app (instance.lock PID was skipped). Scan all cmdline args for …/bin/vocalinux or …/bin/vocalinux-gui instead, which still avoids matching a bare repo directory path. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
2a0e11e077 | fix: correct ydotool service setup guidance (#560) |