100 Commits
Author SHA1 Message Date
Jatin K MalikandGitHub 0a4d637f5f fix(overlay): detect Wayland by GType so the fallback overlay stays compact (#1030) (#1035)
* fix(overlay): detect Wayland by GType so the fallback overlay stays compact (#1030)

_is_wayland() checked isinstance(display, Gdk.WaylandDisplay), but GTK3's
Gdk typelib has no WaylandDisplay class, so it always returned False. On
Wayland without gtk-layer-shell, _layout_bottom_strip() therefore sized the
prewarmed fallback window to the full monitor width (the X11 strip) instead
of the compact centered window meant for Wayland, which on KDE showed up as
a wide empty "Vocalinux Dictation" window listed in the taskbar.

Drop _is_wayland() and use _display_is_wayland(), which checks the GType
name and already gates prewarm, so both decisions agree.

Also list gtk-layer-shell as an optdepend of the source AUR package: with
it KWin and wlroots compositors get a layer surface that is never listed
in the taskbar and never needs the prewarmed toplevel. The AppImage (and
so vocalinux-bin) already bundles it.

* docs(overlay): describe the compact Wayland fallback in the overlay docstrings

The class docstring still said the overlay is always a full-width strip, which no longer holds on Wayland without layer shell (#1030).

* fix(overlay): keep the full-width strip on GNOME Wayland

Mutter ignores CENTER_ALWAYS for a plain Wayland toplevel, so the compact
fallback window landed in the top-left corner on GNOME (tested on GNOME
Shell 46 with the Ubuntu Dock). The full-width strip at least keeps the
orb horizontally centered there. Use the compact window only on KDE,
where KWin listed the strip in the taskbar as a wide empty window (#1030).
2026-11-02 19:17:27 -08:00
Jatin K MalikandGitHub f70357f2b8 fix(overlay): quit Vocalinux when the dock closes the overlay window (#1028) (#1036)
* fix(overlay): quit Vocalinux when the dock closes the overlay window (#1028)

On GNOME Wayland without layer shell, the prewarmed dictation overlay is a
regular toplevel that the dock lists as the app's window. Since #1017 the
overlay refuses close requests so it is never destroyed (#1010), which made
"Quit" from the dock silently do nothing.

The overlay still keeps its window, but now forwards the close request to
an optional on_close_request callback, scheduled once from an idle source.
The tray passes its _quit(), so the dock's Quit behaves like the tray's Quit.

* fix(overlay): drop a queued dock close once the app is quitting

A dock close request is forwarded through an idle source. If the tray's
Quit or SIGTERM tears the overlay down before that idle runs, the
callback would run _quit() a second time. destroy() now drops the
callback, and the pending flag is cleared when the request runs so a
later close can retry if quitting failed.

Refs #1028
2026-11-02 19:17:21 -08:00
Jatin K MalikandGitHub 61bfb1e58e fix(ui): escape device names and test errors in the audio status label (#1043)
The Settings audio status label is set with Pango markup. A microphone
named with '&' or '<' (or an audio test error carrying them) made the
markup invalid, so GTK dropped the text. Escape both with
GLib.markup_escape_text.

Also correct the v0.17.0 notes in docs/UPDATE.md: the Snap never
bundled VOSK; it installs the project without extras.
2026-11-02 18:46:31 -08:00
Jatin K MalikandGitHub f3913fb6bf fix(ui): show the selected microphone's full name in the device tooltip (#1029) (#1037)
The Input Device dropdown has a fixed width and cuts labels off at about
18 characters. Raw ALSA names such as "sof-hda-dsp: - (hw:0,6)" therefore
all look like "sof-hda-dsp: - (h…", and the tooltip always showed the same
generic text, so there was no way to tell which entry was selected.

The tooltip now shows the full label of the selected device. It updates
when the list is filled and every time the selection changes, and goes
back to the generic hint for System Default.
2026-11-02 12:31:02 -08:00
Jatin K MalikandGitHub 122a5e69e6 feat(web): Linux-flavoured 404 page that sends visitors back (#1040)
* feat(web): Linux-flavoured 404 page that sends visitors back

The static export served Next's default "404: This page could not be
found." with no way back into the site.

- "This page is not in your $PATH" heading, plus three true facts about
  404 (chmod 404, exit 404 -> $? = 148, 0x194 / 0o624 / 0b110010100).
- "Did you mean ...?" suggests the closest page the site links to, by edit
  distance after lowercasing. Catches typos and case slips, which matter
  because GitHub Pages paths are case-sensitive.
- Back to home and Install buttons, plus quick links to the install
  guides, FAQ, engine comparison, troubleshooting and changelog.
- Title "Page not found | Vocalinux", noindex.

* fix(web): keep the 404 page working on malformed URLs

decodeURIComponent throws on escapes like /%FF/, which replaced the 404
page with an error screen. Fall back to the raw path, and add mounted
tests for the suggestion and the malformed case.
2026-11-01 01:44:17 -07:00
Jatin K MalikandGitHub f7147bdb44 fix(web): SEO fixes from Search Console and Lighthouse audit (#1039)
* fix(web): SEO fixes from Search Console and Lighthouse audit

Technical:
- Emit BreadcrumbList JSON-LD from the same trail as the visual breadcrumbs
  on every SeoSubpageShell page.
- absoluteUrl() no longer appends a trailing slash to file paths. The
  Organization and publisher logo URLs pointed at /vocalinux.png/, a 404.
- Fetch the GitHub star count once at build time in next.config.js and
  drop the client-side api.github.com fetch and preconnect (Lighthouse
  "third-party code" on every page). Falls back to the "GitHub" label.
- Fix h1 -> h3 heading skips on 11 pages by promoting the leading card
  headings to h2; new .subpage-card-title keeps the old h3 sizing.
- Header and footer logo get alt="" next to the visible "Vocalinux" text.
- Footer links every guide, distro and feature page, so the pages added in
  #1038 and /vs-nerd-dictation/ (Discovered, not indexed) and /writers/
  (no inbound links) are linked sitewide.

Titles and descriptions:
- Rewrite titles/descriptions for pages with high impressions and low CTR
  in Search Console (home, FAQ, compare, languages, open-source, GPU,
  alternatives, offline, install) to lead with the queries they rank for.
- Distro pages use "<Distro> Speech to Text: Offline Voice Typing Setup"
  as the search title; the on-page h1 is unchanged.
- Remove brand repetition and shorten 75+ character titles.
- Bump sitemap lastmod for the rewritten pages.

* fix(web): keep home install card titles styled

The home page's install cards still use h3; only /install/ moved to h2.
Style both so the home cards keep their size and spacing.
2026-11-01 01:39:01 -07:00
Jatin K MalikandGitHub 6389e81d45 docs(web): 16 new SEO pages for v0.17–v0.18 features and more distros (#1038)
* fix(web): stop doubled title suffix and correct stale page claims

- buildPageMetadata now emits titles that already end in the brand
  (e.g. "... | Vocalinux") as absolute, so the layout template no longer
  renders "| Vocalinux | Vocalinux" on 15 existing pages
- autostart: Start on Login moved out of the tray menu in v0.18.1; point
  readers at Settings -> General and the first-run prompt
- wayland: align compositor support with docs/DISTRO_COMPATIBILITY.md
  (sway good, Hyprland experimental, river untested)

* docs(web): add guides for v0.17-v0.18 features and more distros

New pages for features that only had changelog one-liners:
/parakeet/, /faster-whisper/, /dictation-pad/, /transcribe-audio-files/,
/custom-dictionary/, /multilingual-dictation/, /pipewire/,
/global-shortcuts/, /hyprland-sway/, /local-gateway/, /flatpak/,
/install/nixos/, plus /install/debian/, /install/opensuse/,
/install/linux-mint/ and /install/pop-os/ via the distro route.

Each page is checked against src/ and docs/ at v0.18.5, carries
TechArticle/HowTo/FAQPage JSON-LD built from the visible content, and
links to existing guides. The distro route now lists support levels from
the compatibility matrix instead of unsourced "tested on" claims, uses
the documented install commands, adds AUR and native package options,
and drops its duplicate breadcrumb.

Registration: sitemap entries, breadcrumb titles, a Features footer
group, links from the install hub, homepage engine chips and the
reliability page, and a test that every static route is in the sitemap.
The homepage VocaPhone card now says Android is on Google Play.
2026-11-01 00:30:07 -07:00
Jatin K MalikandGitHub 750d64dc9b fix(tray): prefer libayatana-appindicator-glib when available (#1019)
Use the maintained AyatanaAppIndicatorGlib 2.0 indicator when it is installed, mirroring the existing Gtk.Menu into a Gio.Menu whose actions keep stable, never-reused names and fire the original items. Fall back to the GTK 3 indicators otherwise; the AppImage stays on its bundled GTK 3 indicator, and the .deb recommends the glib typelib.

Fixes #1013
2026-10-25 14:49:34 -07:00
Jatin K MalikandGitHub 9b4aded3a2 fix(snap): list PulseAudio microphones and explain CPU-only whisper.cpp (#1018)
Inside the snap, list the sound server's capture sources with pactl and record from the chosen one by scoping PULSE_SOURCE to each PortAudio open (serialized behind one lock). Settings saves the stable source name, keeps each dropdown entry's server name so selection never queries the server from the GTK thread, and the snap reports whisper.cpp as CPU-only instead of probing host GPU tools.

Fixes #1011
2026-10-25 14:49:25 -07:00
Jatin K MalikandGitHub ed29ddf84b fix(overlay): keep the Wayland fallback overlay alive when the dock closes it (#1017)
Refuse close requests on the pre-mapped Wayland overlay so closing its GNOME dock entry no longer destroys it and brings back the first-recording focus loss, and unmap the overlay when it is turned off so the dock entry goes away immediately.

Fixes #1010
2026-10-25 14:49:15 -07:00
Jatin K MalikandGitHub 138a233892 feat(recognition): cancel an in-progress transcription with the recording hotkey (#1004)
* feat(recognition): cancel in-progress transcription with hotkey

Adds per-session cancellation across the manager, tray, and Test Dictation.

Fixes #991

* fix(settings): keep Test Dictation cancellation responsive

Fixes #991

* fix(recognition): keep late workers cancellable (#991)

* test(tray): avoid early tray import in cancellation tests

* fix(recognition): keep a cancelled capture thread out of the next session

A cancel returns to IDLE without joining the capture thread, so a thread
still in its reconnection backoff could wake inside a newer recording,
reopen that session's device and write into its buffer. Tie the capture
loop and the reconnection to the session's cancel token: a cancelled or
replaced session stops reading, skips the reopen and exits without
raising an error.
2026-10-21 01:33:58 -07:00
Jatin K MalikandGitHub 9b5cdf686f fix(dictation-pad): keep Test Dictation results and move pad routing to Settings (#1005)
* fix(dictation-pad): keep Test Dictation results and move pad routing to Settings

Keep late recognition callbacks on the test path.
Move Dictation Pad routing to Settings.

Fixes #983
Fixes #986

* fix(recognition): ignore stop sentinels in pending transcription

Ignore stop signals when checking queued transcription work.
Add regression coverage for a real stop path leaving a sentinel behind.

Fixes #983

* fix(recognition): don't count orphaned queued audio as pending transcription

* fix(recognition): count superseded workers as pending transcription

* test(auto-pause): give the bare manager its worker-tracking state
2026-10-21 01:20:14 -07:00
Jatin K MalikandGitHub f013b7ff88 fix(text-injection): route XWayland clients without IBus through xdotool (#1003)
* fix(text-injection): route XWayland clients without IBus through xdotool

On KDE Plasma Wayland with the KWin IBus bridge enabled, Vocalinux injects through IBus. An X11 client under XWayland only reaches IBus when it loaded an IBus input method module from GTK_IM_MODULE, QT_IM_MODULE or XMODIFIERS. Without one, the engine commit lands in no input context and still reports OK, so the text was lost with no fallback.

Before each WAYLAND_IBUS injection, check XWayland's active window. A real client whose own environment does not select IBus gets that segment through the WAYLAND_XDOTOOL path, and the backend returns to IBus afterwards so native Wayland apps keep using it.

Fixes #972

* fix(text-injection): honour IBus pins and the client toolkit in the XWayland detour

Keep IBus when the user pinned it, judge a GTK or Qt client only by its own toolkit's IM variable (read from the libraries it maps), and have the clipboard restore after an XWayland paste read the X11 clipboard it wrote even after the backend switched back to IBus.

* fix(text-injection): keep a non-text X11 clipboard on the XWayland paste route

WAYLAND_XDOTOOL pasted through the X11 clipboard even when it held an image or file, which it cannot save and restore. Check the clipboard targets there as the X11 route already does, and type the text instead when they hold no text.
2026-10-21 00:40:22 -07:00
Jatin K MalikandGitHub e53fc2ceb0 fix(updates): follow the build channel and allow turning update checks off (#999)
Use the running build channel for background checks and add a preference to disable them.

Fixes #977
2026-10-21 00:26:09 -07:00
Jatin K MalikandGitHub 1c299d2dc7 fix(whispercpp): map GPU picker to ggml devices and size threads by effective backend (#1001)
* fix(whispercpp): map GPU picker to ggml devices and size threads by effective backend

The GPU picker and auto selection used vulkaninfo indices, but whisper.cpp's
gpu_device is an ordinal over the GPU/IGPU devices ggml registered. ggml skips
Vulkan devices it cannot use (for example a Haswell iGPU), so the indices shift
and a selected NVIDIA card could fall back to CPU while logs claimed Vulkan.

Read ggml's device registry through the loaded pywhispercpp extension, select
and label devices by that ordinal, run on CPU explicitly when ggml has no GPU,
log the effective backend after load, and pick n_threads from the effective
backend instead of the presence of GPU libraries.

Fixes #987
Fixes #990

* test(whispercpp): move new ggml probe tests into their test class
2026-10-21 00:07:01 -07:00
Jatin K MalikandGitHub 5fa0db6a47 fix(injection): give every RemoteDesktop portal request a unique handle_token (#996)
* fix(injection): give every RemoteDesktop portal request a unique handle_token

Subscribe to predicted Request paths before calling the portal and retain the RemoteDesktop portal in Flatpak when ydotoold is unavailable.

Fixes #993

* refactor(injection): share the portal close in _demote_portal_backend

* fix(injection): recreate the portal session and keep partial counts under Flatpak
2026-10-21 00:02:28 -07:00
Jatin K MalikandGitHub 566a228858 fix(installer): pin sherpa-onnx-core so Parakeet installs import (#1000)
sherpa-onnx 1.13.7 wheels require sherpa-onnx-core, which ships
libonnxruntime.so, but the armv7l wheels omit that metadata and uv.lock
recorded no dependency. install.sh installs the export with --no-deps, so
sherpa_onnx failed to import, Settings hid Parakeet and the installer
quietly switched config.json to whisper.cpp while still reporting Parakeet.

List sherpa-onnx-core in the parakeet extra, verify engine imports right
after installing them, and keep SELECTED_ENGINE in sync when the config
is repaired so the summary names the engine actually configured.

Fixes #973
2026-10-20 23:58:38 -07:00
Jatin K MalikandGitHub 545b6d0feb fix(overlay): map the dictation overlay at startup on Wayland so the first recording keeps focus (#998)
* fix(overlay): map the dictation overlay at startup on Wayland

Mutter activates newly mapped Wayland toplevels, which can take focus from the editor.
Map the fallback overlay transparently at startup so its first recording does not remap it.

Fixes #992

* fix(overlay): detect the Wayland display by GType name for the prewarm

Detect Wayland from the display GType because GTK3 omits GdkWaylandDisplay from its typelib.
2026-10-20 23:45:57 -07:00
Jatin K MalikandGitHub d59395b5c9 fix(hotkeys): use the X11 listener for Flatpak on native X11 sessions (#997)
* fix(hotkeys): use the X11 listener for Flatpak on native X11 sessions

Use evdev for Flatpak Wayland/XWayland while preserving the GNOME X11 fallback.

Fixes #971

* docs(flatpak): note that GNOME X11 still needs /dev/input for hotkeys
2026-10-20 23:41:07 -07:00
Jatin K MalikandGitHub 3abc106fdb feat(languages): add Slovenian (sl) to the supported languages (#995)
Slovenian was missing from SUPPORTED_LANGUAGES, so the Settings picker did not offer it and normalize_language_shortcuts dropped any shortcut bound to it. Add it as a Whisper-only entry (whisper code sl; alphacephei publishes no Slovenian VOSK model), map the Slovenian XKB layout si to it for follow-keyboard-layout, and list it on the website languages page.

Fixes #970
2026-10-20 23:29:47 -07:00
Jatin K MalikandGitHub 65de3427fb perf: faster Settings, typing and dictation; mypy in CI, engine interface, architecture docs (#968)
* fix(perf): reduce startup and inference overhead

* fix(perf): report effective whisper.cpp fallback settings

* fix(injection): add opt-in X11 paste for long dictations

* perf(vad): lower default silence timeout to 1.2s

* fix(types): resolve mypy errors across application source

* ci: run mypy in the lint job

* ci: include mypy in lint dependency group

* fix(types): narrow evdev timestamp comparison

* refactor(recognition): add SpeechEngine protocol and engine dispatch table

* refactor(settings): move About section and engine tables out of settings_dialog

* docs: add ARCHITECTURE.md and refresh docs index and AGENTS layout

* fix(settings): preserve GTK dialog compatibility after extraction

* style: format touched Python modules

* fix(types): keep local typecheck from syncing dependencies

* test(vad): align main startup expectation with new default

* style: sort engine registry and protocol test imports

* test(settings): follow extracted About builder attribute alias

* test(settings): isolate GTK dialog base in About test

* test(settings): isolate About color scheme build test

* test(settings): restore GTK base after About scheme test

* test(settings): isolate About scheme check from GTK mocks

* test(settings): support extracted dialog mixin methods

* docs: add before/after performance benchmark charts

* fix(injection): type instead of pasting when the clipboard holds non-text data
2026-10-20 20:23:08 -07:00
Jatin K MalikandGitHub 6c3a905825 Remove 'PRs Welcome' badge from README
Removed the 'PRs Welcome' badge from the README.
2026-10-16 19:19:49 -07:00
Jatin K MalikandGitHub d53300841d fix(appimage): reuse the Vulkan pywhispercpp build across CI runs (#903)
* fix(appimage): reuse the Vulkan pywhispercpp build across CI runs

The x86_64 Build AppImage step spends most of its time recompiling
pywhispercpp with Vulkan. That compile depends on the base image,
tool_checksums.txt, and the pywhispercpp pin, not on the commit.
Cache the native tree under one key shared by CI, Nightly, and Release.

* fix(appimage): reject a partial Vulkan cache and key the build tools

A failed copy could leave a tree that still contained libggml-vulkan.so,
and the next run would reuse it. Check each copy, and keep the previous
cache until the new one is in place.

The cache id now also covers the pinned cmake and ninja export and the
Vulkan compile flags. The hit path is what the build calls, and a test
runs it with a stub compile command.

* chore(ci): rerun AppImage against the saved Vulkan cache

The review fix changed the native cache id, so run 37586979294
compiled pywhispercpp again and saved
appimage-native-X64-ebf478b87986e2f6b9d80c97c2f5869fe3120c2bcd48f5d8f5a65fe06ec03015.
This commit leaves the base image, tool checksums, pywhispercpp pin,
installer tool pins, and Vulkan compile flags alone.
2026-10-07 01:04:18 -07:00
Jatin K MalikandGitHub 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.
2026-10-07 00:06:37 -07:00
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>
2026-09-30 02:33:49 -07:00
Jatin K MalikandGitHub 77def71354 fix(audio): stop passing explicit index when zero devices enumerate (#891)
* fix(audio): stop passing explicit index when zero devices enumerate

With zero audio devices, _resolve_valid_input_device returned the saved preferred_index, so _open_capture_stream opened input_device_index=<stale> — a device that cannot exist — and Pa_OpenStream aborted the whole process (SIGSEGV) before the clean IOError->ERROR path could run. Zero enumerated devices now returns None so PyAudio falls back to the system default and fails catchably; a negative count (enumeration failure) still passes the preferred index through.

* test(audio): cover capture path omitting device index

* test(audio): cover saved-device zero-enumeration capture path

* test(audio): annotate new resolver tests
2026-09-29 22:26:16 -07:00
Jatin K MalikandGitHub 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
2026-09-29 15:33:27 -07:00
Jatin K MalikandGitHub c2654609ac feat(ui): floating glowing dictation overlay (#516)
* feat(ui): add floating glowing dictation overlay

Show a bottom-center non-focus overlay while recognition is LISTENING
or PROCESSING so users get a clear visual cue beyond the tray icon.
Optional GtkLayerShell on Wayland; plain always-on-top GTK fallback
elsewhere. Configurable via ui.show_overlay (default on) and Settings.

* fix(test): isolate dictation overlay tests from GTK pollution

Overlay unit tests no longer construct real GTK windows or import
tray_indicator, which was loading gi.repository early and breaking
later tests that mock GI (single-instance and suspend GLib checks).

* docs: add dictation overlay screenshots

Capture the floating glowing orb in listening (green) and processing
(amber) states for the PR and README screenshots section.

* test(ui): raise dictation overlay coverage for Codecov patch gate

Exercise GTK init, draw/animation, positioning, layer-shell fallbacks,
tray overlay wiring, settings toggle, and config edge cases so patch
coverage on the floating overlay clears the 80% Codecov target.

* test(tray): call overlay helpers via unbound methods

Avoid instance-level MagicMock shadowing of set_overlay_enabled under
some import/mock orderings (seen on CI Python 3.10).

* test(tray): inject explicit config mock for overlay helpers

Assign a MagicMock config_manager on the instance so assertions do not
depend on the constructor patch (which can leave a real ConfigManager
under some Python versions / import orders).

* refactor(ui): ponytail pass on dictation overlay

Drop the hand-drawn mic glyph, simplify the Cairo clear/pulse path,
reuse ConfigManager get_bool/set, tighten tray overlay helpers, and
delete string-grep structural tests that proved nothing.

* fix(ui): pin overlay to bottom strip and stop stealing focus

Use a full-width bottom strip (layer-shell left/right/bottom anchors, or
resize+move fallback) so the orb is bottom-center on Wayland. Map the
window once and toggle opacity instead of hide/show so compositors do not
re-activate it and yank focus out of the editor (which broke IBus inject).

* fix(appimage): seed GtkLayerShell typelib for dictation overlay

* fix(ui): address greptile findings on dictation overlay

* fix(appimage): keep layer-shell typelib optional for direct builds
2026-09-29 14:24:31 -07:00
Jatin K MalikandGitHub 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
2026-09-29 14:24:28 -07:00
Jatin K MalikandGitHub 7b69ac2f46 fix(site): Phone chip, Free forever Cost copy, and privacy GA disclosure (#868)
* fix(site): align Phone chip, Cost copy, and privacy with GA

Use the official Android beta / iOS TestFlight chip on #ecosystem.
Replace Free forever with Free and open source on privacy and open-source.
Disclose vocalinux.com Google Analytics and scope no-telemetry claims to the installed app.

Fixes #867

* fix(site): make privacy GA disclosure match the live gtag

Spell out page views, the client-ID cookie, and IP/device processing for
G-7NBBNJNNQ7. Align Data Collection with PRODUCT.md, drop the hidden
telemetry weasel, and correct config.json plus the voice-storage row.
2026-09-29 01:07:51 -07:00
Jatin K MalikandGitHub 9646011e77 fix(settings): speech model follow-ups from QA (#836)
* fix(settings): speech model follow-ups from #834

Keep the Unused downloads expander in sync with search, recommend Faster
Whisper .en weights for English, parse nvidia-smi MiB VRAM, stop simple
mode from clobbering Faster Whisper/Parakeet, and ceil RAM so ~7.4 GiB
counts as 8.

* fix(settings): keep FW .en picker ids and test unused search

Represent Faster Whisper English catalog variants in the Settings size
combo so tray-saved small.en is not rejected and overwritten. Add
behavioral unused-downloads search coverage for match/nonmatch/clear.
2026-09-29 01:06:08 -07:00
Jatin K MalikandGitHub 06f049ac76 fix(shortcuts): record unmapped F19/F24 and XF86-aliased F13–F23 (#844)
* fix(shortcuts): record unmapped F19/F24 and XF86-aliased F13–F23

* fix(shortcuts): keep split-keyboard modifiers while recording

The recorder only looked at the keyboard that sent the function key, so a modifier held on the other half was dropped. A dead device also stayed in the watch list, which left F19/F24 uncapturable until Record was restarted.

Union held keys across every open keyboard before committing, and drop a device on hangup or read error while scanning for a replacement.

* fix(shortcuts): keep capture tests off real input devices

CI opened real /dev/input paths because the harness patched a stale evdev module, so io watches never attached. Patch the live module, and move the capture device protocol to common_types.

* fix(tests): patch the recorder's own GLib globals

test_settings_dialog reimports the dialog module and leaves the replacement in sys.modules. Recorder methods still close over the original dict, so the harness never recorded timeouts or io watches.
2026-09-29 01:05:46 -07:00
Jatin K MalikandGitHub 74d6f505d4 fix(update): fall back when GitHub API rate-limits (#846)
Shared NAT hits the unauthenticated api.github.com quota and the
update check logged a WARNING that looked like a failure. On 403
or 429, look up the latest stable tag from the releases page
JSON, and stay quiet if that also fails. Nightly still refuses
to treat the stable latest tag as a nightly.

Fixes #845
2026-09-29 01:05:02 -07:00
Jatin K MalikandGitHub 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
2026-09-23 00:09:54 -07:00
Jatin K MalikandGitHub 4df612dd7a fix(settings): drop Test Dictation scrolled-window stripe (#853)
* fix(settings): drop Test Dictation scrolled-window stripe

GTK3 ScrolledWindow defaults to an IN etched shadow, which GNOME/Wayland
renders as a thin vertical stripe on the left of the Test Dictation
revealer even when the buffer is empty. Call set_shadow_type(NONE) to
match the other ScrolledWindows in this dialog.

Fixes #847

* Add missing test return annotation
2026-09-23 00:09:43 -07:00
Jatin K MalikandGitHub 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
2026-09-16 19:40:13 -07:00
Jatin K MalikandGitHub 47dd983def docs(readme): drop ecosystem status and the VocaWin SmartScreen note (#835)
The family table was going stale on every sibling release. Keep project
links only. Also say honestly that the 0.17.0 snap is still waiting on
Store review of the uinput plug.
2026-09-16 01:03:46 -07:00
Jatin K MalikandGitHub 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.
2026-09-16 00:18:09 -07:00
Jatin K MalikandGitHub 092dde105e fix(audio): open 3-8ch HDA mics at native layout (#813) (#829)
* fix(audio): open 3-8ch HDA mics at native layout (#813)

HDA analog capture often reports 4 channels. Opening below that native
count can abort PortAudio after read with free(): corrupted unsorted
chunks (same class of bug as #666/#673).

Try the reported channel count first when it is between 3 and 8, then
2ch and 1ch. Pulse devices that advertise 32/128 channels still open
stereo-first. Downmix N channels to mono so engines keep seeing 1ch.

Virtual-device filtering for pipewire/default (#629) is unchanged.

* fix(audio): downmix HDA capture from first channel pair

Averaging all N HDA channels halves speech when only the front pair is live.
Keep stereo mean-of-both, and for 3+ channels average channels 0 and 1.

* fix(audio): pick loudest channel for N-ch downmix

First-pair-only downmix silenced mics on ch2+. For 3+ channels, select
the loudest channel by per-buffer mean-square so speech stays at full
level whether it lands on the front or rear pair. Stereo still averages
both channels.

* fix(audio): sticky N-ch downmix channel for open stream

Pin the loudest-channel pick for the life of a capture stream so a
noise burst on another input cannot switch the mic mid-utterance.

* fix(audio): gate sticky downmix lock on speech energy

Don't pin the N-ch sticky channel from a silent/ambient first buffer;
only lock once the loudest channel clears a documented mean-square floor.

* style: isort sticky-lock import in device config tests
2026-09-14 00:21:17 -07:00
Jatin K MalikandGitHub 5e8a08fbb8 fix(ui): follow OS dark/light on native GTK installs (#830)
* fix(ui): follow OS dark/light on native GTK installs

AppImage already maps portal/gsettings color-scheme into GTK_THEME.
AUR and install.sh use system GTK3, which ignores prefer-dark. Set
gtk-application-prefer-dark-theme from the same signals, and skip when
GTK_THEME is already set so AppImage and user overrides stay in charge.

Fixes #816

* docs: portal no-preference still checks gsettings

The helper already falls back after a portal 0 or a failed Read.
The module doc said that left GTK defaults, which is only true
after gsettings also comes up empty.
2026-09-14 00:16:50 -07:00
Jatin K MalikandGitHub 79df154292 ci(flatpak): restore previous builder cache and skip src-only PRs (#825)
The builder action restore-keys is `flatpak-builder-${arch}`. Our key
was `flatpak-builder-${hash}`, which the action stores as
`flatpak-builder-${hash}-x86_64`. That does not start with
`flatpak-builder-x86_64`, so a packaging hash change (a lock refresh,
or merging one) discarded the whole .flatpak-builder cache and
recompiled numpy and pywhispercpp from source, ~18 min instead of ~4.

Put ${{ matrix.arch }} in the key first so the fallback matches. Then
flatpak-builder skips unchanged modules and only rebuilds what the
hash change actually touched.

PRs no longer watch src/**. python-test already covers those, and the
expensive compile does not. push to main still does, so a sandbox
install break fails the default branch.
2026-09-12 13:42:42 -07:00
Jatin K MalikandGitHub 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.
2026-09-12 12:58:35 -07:00
Jatin K MalikandGitHub 70d5ce7688 fix(snap): put gnome-platform first so SVG icons load (#822)
core24 desktop-launch rebuilds gdk-pixbuf-loaders.cache from the first
LD_LIBRARY_PATH directory that ends in the arch triplet. Stage-packages
drop a gdk-pixbuf tree without libpixbufloader_svg.so at
$SNAP/usr/lib/<triplet>, ahead of gnome-platform, so About-page SVGs
fail with "Image type svg is not supported".

Fixes the same core24 GTK3 gap documented at:
https://forum.snapcraft.io/t/missing-svg-support-for-gtk3-in-core24/48851
2026-09-12 12:43:39 -07:00
Jatin K MalikandGitHub 0ef1f09cec fix(web): bump homepage VocaWin card to v0.1.1-beta (#812)
Homepage family strip still pinned the old Win tag. Match PRODUCT.md and
the published VocaWin release. Keep the unsigned / SmartScreen line.

Fixes #811.
2026-09-12 12:04:24 -07:00
Jatin K MalikandGitHub 94c2eaac08 fix(tray): make PTT tray icon turn red on every hold (#809)
* fix(tray): refresh PTT icon when AppIndicator reuses a path

Right-Alt push-to-talk turned the tray red only on the first hold.
Later holds dictated fine but left the icon gray until restart.

StatusNotifier hosts often skip a redraw when IconName returns to an
absolute path already used in the session. Nudge IconThemePath before
set_icon_full so gray-red-gray-red actually turns red again.

* test(tray): assert alternating IconThemePath nudge args

Greptile P2: the PTT redraw regression only checked set_icon_theme_path
call count, so identical paths would still pass. Assert the alternating
ICON_DIR / ICON_DIR+os.sep arguments that drive the host reload.
2026-09-08 20:56:18 -07:00
Jatin K MalikandGitHub 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.
2026-09-07 22:51:14 -07:00
Jatin K MalikandGitHub cdfe541c25 feat(speech_recognition): add faster-whisper engine backend (#543)
* feat(speech_recognition): add EngineType enum and Engine Protocol

Introduce a typed EngineType enum and an Engine Protocol in common_types.py,
plus an engines/ registry so new speech recognition backends can be plugged in
without scattering string comparisons across the manager.

* feat(speech_recognition): add faster-whisper engine backend

Add a faster-whisper engine implementation using CTranslate2/INT8 for fast
CPU inference. Includes model metadata utility, wiring through the recognition
manager, CLI, settings UI, config, optional dependency, and unit tests. Also
updates the suspend-handler test fixture for the new engine attribute.

* style(ui): format faster-whisper imports for isort 8

* test(speech_recognition): expand faster-whisper coverage for codecov

* test(speech_recognition): fix failing faster-whisper coverage tests

- Import missing get_compute_type helper

- Add tests for English-only detection, cache exceptions, and import-error registry path

- Clear _has_torch_cuda lru_cache around exception test to avoid cached True

- Replace broad builtins.__import__ patch with a fake engine module for registry test

- Restore registry module state after test

* test(speech_recognition): add coverage for faster-whisper edge cases

Cover previously missing lines in:

- faster_whisper_model_info.py: HF cache path/none, hf_hub lookup, fallback traversal, torch CUDA true, psutil exception

- faster_whisper_engine.py: model None guard after is_ready()

- engine registry import-error path

* feat(install): add faster_whisper engine option to install.sh

- Add faster_whisper to --engine help text and examples

- Add faster-whisper as interactive option 5 with description

- Install faster-whisper package and verify import at install time

- Add fallback to whisper.cpp if faster-whisper installation fails

- Add smoke test for faster-whisper import

- Update welcome message engine display name

- Note: faster-whisper models download automatically on first use

* refactor(faster_whisper): apply ponytail review cleanups

- Inline FASTER_WHISPER_MODEL_INFO dict literal; drop unused AVAILABLE_MODELS/MODEL_SIZES

- Reuse _has_torch_cuda() in FasterWhisperEngine.device property

- Remove redundant model None guard after is_ready() verification

- Shrink faster-whisper default config in install.sh to engine + model_size

- Update device tests to manage _has_torch_cuda lru_cache

- Remove test for deleted dead-code guard

* fix(faster_whisper): integrate optional engine with current main

Keep whisper_cpp as default and Parakeet as an extra. Put engine selection
in the Advanced island, install faster-whisper via the optional extra, and
refuse unpinned Hugging Face downloads until model_checksums.txt pins them.

* fix(faster_whisper): download verified models and refresh uv.lock

Settings already opened a download dialog for uncached faster-whisper, but
init only raised ChecksumError. Wire a Parakeet-style downloader that stores
bundles under models_dir, verifies each file against model_checksums.txt, and
loads WhisperModel from that path. Regenerate uv.lock for the optional
faster-whisper extra, pin digests for all eleven Systran models, and add the
missing -> None return annotations.

* fix(faster_whisper): clear engine on unload and fix auto-pause fixture

Auto-pause tests stub __init__, so they never set _faster_whisper_engine.
reinitialize_after_resume then AttributeErrors once Test Python runs.
Mirror the suspend-handler fixture, and release the faster-whisper engine
inside unload_model the same way resume already does.

* fix(faster_whisper): restore previous engine after a failed switch

reconfigure() selected the new engine and released the live model before
init. A failed faster-whisper download then left the manager in ERROR on
the new backend while Settings reverted the pickers to the saved engine,
so dictation stayed dead until restart. Reload the previous engine from
disk (no download) before re-raising so the UI and live manager agree.

* fix(speech_recognition): restore all live settings after failed switch

Failed reconfigure already put the previous engine back, but VAD, device,
remote API, and whisper.cpp knobs stayed on the attempted values while
config on disk did not. Snapshot every field reconfigure writes and restore
the lot before reloading the old backend. Also bind the real
_init_selected_engine onto the MagicMock in the force_reinit test.

* fix(speech_recognition): restore every live field after failed reconfigure

A failed engine switch already reloaded the previous backend, but Greptile
could still miss that VAD, audio device, remote API, and whisper.cpp knobs
were part of the snapshot. Keep one explicit attr list for snapshot/restore,
including the derived voice-commands flag, so live state matches persisted
config after a failed Settings apply.

* chore(speech_recognition): sign tip after rollback follow-up
2026-09-07 20:47:56 -07:00
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>
2026-09-07 20:47:12 -07:00
Jatin K MalikandGitHub f90e4d5765 fix(injection): release ydotool Ctrl after a timed-out paste (#675)
subprocess.run(..., timeout=3) SIGKILLs the ydotool client. ydotoold
keeps any keys it already applied, so virtual Left Ctrl can stay held
until logout. Send a best-effort V-up/Ctrl-up (or a legacy ctrl tap)
after paste failure.

Fixes #658
2026-09-07 20:47:07 -07:00
Jatin K MalikandGitHub 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.
2026-09-07 18:10:43 -07:00
Jatin K MalikandGitHub d07c70dbc9 fix(audio): stop sink-wake from clipping start/stop cues (#804)
* fix(audio): prepend silent preroll so sink-wake does not clip cues

Short catalog tones start in a fresh paplay process while PipeWire is
still waking a suspended sink, so the first tens of milliseconds of the
cue are missing. Prepend 100ms of silence in the same WAV, cache the
result by source path, mtime, and preroll length, and fall back to the
original file if the preroll cannot be built. CI mock players keep the
raw path.

Fixes #800

* fix(audio): cache sink-wake preroll without tempfile.mkstemp

Some tests replace sys.modules["tempfile"] with a MagicMock, so
mkstemp unpacking fails and playback falls back to the raw cue.
Name the cache file from a short sha256 of the source path, mtime,
and preroll length, write a sibling .partial, then os.replace it
into place. Reload real tempfile and wave in the audio feedback
tests so they still pass after those mocks leak.

Fixes #800

* fix(tests): pin real wave for preroll without reloading tempfile

Popping tempfile out of sys.modules in the audio feedback fixture
made preroll tests pass, but later whisper tests then bound
TemporaryDirectory from a MagicMock. Import real tempfile and wave
at module top, pin only wave while audio_feedback is reimported,
and bind TemporaryDirectory from the real stdlib if a sibling test
already replaced tempfile.

Fixes #800

* fix(audio): catch specific preroll errors and type test helpers

The new preroll path swallowed every exception, which hid bugs and
disagreed with AGENTS.md. Catch OSError, ValueError, and wave.Error
on the inner partial cleanup and the outer fallback so a bad WAV or
cache write still falls back to the original cue, and other errors
still propagate. Annotate the two test helpers the same review
flagged.

* fix(audio): fall back when truncated cues raise EOFError

wave.open can raise EOFError on a truncated RIFF before playback.
Catch it with the other preroll failures so start/stop still play the
original file.

Fixes #800
2026-09-07 18:10:38 -07:00
Jatin K MalikandGitHub dcd8d06077 fix(asr): keep Parakeet recognition alive after decode errors (#803)
* fix(asr): keep Parakeet recognition alive after decode errors

sherpa-onnx create/decode exceptions were escaping the recognition
thread. Catch them like whisper.cpp, annotate the Parakeet helpers, and
cover create_stream failure.

* fix(asr): narrow Parakeet transcription exception handling

Keep create_stream/decode survival, but stop swallowing unrelated
bugs. Same RuntimeError/OSError/ValueError tuple as Whisper.
2026-09-07 18:10:33 -07:00
affbf6a9af feat(settings): simple setup for the speech model, with Advanced as an island (#801)
* feat(settings): make the language picker searchable while its list is open

Entry completion only ever covered typing into the closed entry. Open the
dropdown and the keyboard goes to GTK's own first-letter jump, so nothing typed
reaches a filter, and with thirty-odd languages scrolling is the only way
through. The first attempt at fixing this (a filtered ComboBox model) had the
same limitation for the same reason.

Replace the combo with a picker that opens a popover holding a search entry,
focused on open, above the list. Every keystroke narrows the rows, Enter takes
the first match, a click takes that row. It keeps the GtkComboBoxText methods
the dialog calls, mirrors the store's column order in base_model for the shared
row helpers, and emits "changed" only when the selection actually changes, so
re-selecting the saved language on open is not taken for a user edit.

Visibility uses the matcher the typed-text resolver already uses, so the list
and Enter cannot disagree. The completion helper and the combo styling skip
the picker: it filters its own list, and the ComboBox-only calls do not exist
on a Box.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT

* feat(settings): add a simple mode that derives the model from the language

The Speech Model panel asked four questions, but only two of them can be
answered without knowing the hardware: what language you speak and whether you
want it faster or more accurate. Size follows from the machine and the
specialization follows from the language, and the app already computed both —
it just printed the answer beside the pickers instead of using it.

Simple mode asks for the main language, whether whole texts are also dictated
in other languages, and a priority. There is deliberately no second language
field: whisper takes one language or none, so a pair cannot be passed, and with
a language pinned the multilingual weights already handle inserted foreign
words. The switch is the "none" case.

Priority moves relative to what hardware detection recommends rather than
naming an absolute size, so "most accurate" means something different on a
laptop than on a workstation. Opening simple mode reads the priority back from
the size already configured, so switching modes never changes the loaded model
on its own.

Simple mode steers the existing controls rather than writing the configuration
itself, so applying, downloading and the info card keep going through one path.
Advanced mode is the previous panel, unchanged.

Refs #779

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT

* fix(settings): stop simple mode reloading the engine four times per pick

Reported against the installed build: choosing a language locked the window
for a moment. Simple mode steers the engine, language, size and variant
controls, and every one of those change handlers ends in _auto_apply_settings,
so a single pick reconfigured the speech engine up to four times on the UI
thread, reloading the model each time. Hold a guard across the steering and
apply exactly once when it is done.

The simple language box was also a plain list with no entry, so it could not be
typed into — a step backwards from the advanced row, and unusable with over
thirty languages. It now uses the same searchable combo, with the same commit
and restore behaviour for text that resolves to nothing.

Refs #779

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT

* refactor(settings): make simple the panel and Advanced a reveal

The Simple/Advanced dropdown framed the two as alternatives, which put a mode
decision in front of the user before any of the questions they can answer. They
are not alternatives: simple is the panel, and advanced is more detail about
the same choice.

The mode selector is gone. The simple questions are always on screen, with an
Advanced switch that slides the engine, size and specialization rows out
underneath. Because simple mode steers those rows, they also work as a readout
of what the simple answers resolved to, which the old either/or hid.

The stored settings_mode becomes a show_advanced flag, remembering whether the
detail was left open.

Refs #779

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT

* refactor(settings): second language list, and Advanced in its own window

Reworked to what the panel is actually for. The main language comes from the
keyboard layout. Turning on "I also dictate whole texts in other languages"
reveals a second list rather than silently flipping to detection, so the choice
is visible. Advanced is a button that opens the detailed controls in their own
window, instead of an inline reveal that pushed the page around.

Naming a second language resolves to automatic detection, because whisper takes
one language or none. The one case where both can still be honoured is two
English entries — en-US plus en-IN — where English stays pinned and the
English-only weights, same size and better at English, remain available.

Also fixes a name collision the reveal had: advanced_revealer already belonged
to the power-user toggle on the Advanced page, and the section built later won
the attribute.

Refs #779

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT

* feat(settings): use the filtering picker for both simple language lists

The main language and the second language are the same thirty-odd-row lists as
the advanced one, so they get the same dropdown filtering rather than a
completion popup that only reacts while the entry has focus.

Refs #779

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT

* fix(settings): make simple mode do what it says on screen

Four things reported from the installed build, all real.

The second language list never appeared: the row is flagged no_show_all, and
show_all() is a no-op on such a widget, so the flag has to be cleared first —
the dance _set_custom_shortcut_row_visible already does.

There was no multilingual answer. "Any language (auto-detect)" is now the first
entry of the second list and the default when the switch is turned on, so the
switch alone visibly does something.

Changing the priority showed nothing, because the info card — the only surface
that says which model was picked and what it costs — had been moved into the
advanced window. It is back on the page under the simple questions.

Opening Advanced crashed the compositor. KWin 6.7.4 (Wayland) died with
unbounded recursion in KWin::Window::findModal(), a cycle in its transient-for
chain, when the advanced window — a long-lived toplevel transient for this
dialog, hidden on close and shown again with present() — was opened. Coredumps
at 2026-09-06 20:20:10 and 20:39:49; the crashing state is kept as tag
kwin-crash-repro. The window now stands on its own, is built on every open and
destroyed on close with the controls re-parented out first. KDE Bug 483265.

Refs #779

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT

* fix(settings): bring the advanced window up in front of the dialog

Reported: it opened behind Settings. Dropping the transient parent (which
crashed KWin) also dropped the only reason the compositor had to stack it over
the dialog, and on Wayland a toplevel mapped without an activation token lands
behind the focused window. Present it with the timestamp of the click that
opened it: that asks the compositor for focus and stacking, and needs no
transient link.

Refs #779

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT

* refactor(settings): Advanced as an island under the simple card, not a window

A second toplevel went wrong twice on KWin 6.7 / Wayland. Transient for the
dialog it crashed the compositor (unbounded recursion in
KWin::Window::findModal, tag kwin-crash-repro, KDE Bug 483265). Standing on its
own it could not be raised above the dialog at all, not even by hand.

Advanced is now its own card under the simple questions: collapsed to a header
by default, expanding in place, remembered in show_advanced. Both cards are on
screen at once, so the expanded rows double as a readout of what the simple
answers resolved to, and a change made in the advanced rows is read back into
the simple ones — skipped while simple mode is itself steering them.

The info card sits between the two, where a priority or language change shows
its effect and its cost.

Refs #779

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT

* fix(settings): let a same-size weight on disk stand in before downloading

Reported: turning the "I also dictate whole texts in other languages" switch
off went from base to base.en — same size, different weights — and sat the user
through a modal 141 MB download for it, with the window blocked behind the
progress dialog. Turning it on had done the same in the other direction.

Simple mode now looks at the disk before settling on a variant. A downloaded
weight of the same size that can serve the language stands in: English-only
first when English is wanted, the plain multilingual next, quantized ones last.
English-only weights never stand in for another language or for detection.
When nothing of that size is on disk the derived variant stays and the
download runs as before. The info card still offers the better variant to
download; it just no longer forces it.

Refs #779

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT

* fix(settings): keep Advanced language pin over stale simple multi

_sync_simple_from_advanced restored the other-languages switch and
persisted simple_second_language even when Advanced had a pinned
language. The next simple edit then decoded to auto and silently
replaced the pin. Clear/resync simple multi on a pin, refresh that
readout when Advanced language changes, and drop the second-list
Enter hook that double-applied through SearchablePicker.

Refs #779

* fix(settings): keep two-English simple multi across Advanced pin sync

Clearing every leftover second language on a pinned Advanced readout
was too broad: en-US plus en-IN still pins English, and wiping that
answer forgot the second English choice on the next open. Restore
multi only when the stored second would keep the pin; otherwise drop
it so a later simple edit cannot decode to auto.

Refs #779

* fix(settings): restore simple multi only when it matches live language

The pin-side keep left the auto branch restoring any stored second.
Two English over Advanced auto still decodes to en-us, so the next
simple edit silently replaced detection. Keep leftover multi only when
it still decodes to the live Advanced language either way.

Refs #779

* fix(settings): keep language-change tests honest after the main merge

Mock dialogs treated _initializing as truthy and skipped the handler.
Also call _refresh_simple_readout explicitly so the simple card updates.

* fix(settings): type the language-entry GTK callbacks

Greptile flagged missing param and return annotations on the new
activate and focus-out handlers. Match the surrounding Any/bool style.

Refs #779

* fix(settings): sort imports for isort after Parakeet merge

---------

Co-authored-by: Kacper Paczos <23406555+kacperpaczos@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 14:38:30 -07:00
8f4aeca2f1 feat(asr): add Parakeet TDT 0.6B engine via sherpa-onnx (#802)
* feat(asr): add Parakeet engine via sherpa-onnx

Adds a "parakeet" engine backed by sherpa-onnx, running NVIDIA Parakeet
TDT 0.6B int8 models on CPU: v3 (25 European languages, auto-detected)
and v2 (English). Output is punctuated and capitalized.

On an Intel Core Ultra 7 155H it decodes at about 12x realtime on CPU
(RTF 0.08), roughly 8x faster than whisper.cpp large-v3-turbo over the
same clips with its Vulkan GPU backend (RTF 0.59). Being a transducer it
also has no 30s window to pad, so a short push-to-talk utterance returns
in ~0.3s instead of ~2.4s.

Follows the existing engine layout: utils/parakeet_model_info.py holds
the catalog and the get_model_path / is_model_downloaded /
list_downloaded_models / delete_model helpers, while _init_parakeet /
_transcribe_with_parakeet / _download_parakeet_model follow the same shape as their
whisper.cpp counterparts and reuse _stream_model_download for progress
and cancellation.

Registered in the engine dispatch, the settings engine/model/language
pickers, the model info card, the download prompts, the unused-downloads
list and install.sh.

Models download to <models_dir>/parakeet/<model>/ via temp files renamed
on success; is_model_downloaded requires all four files, so an
interrupted download is retried rather than loaded.

Requires the new "parakeet" extra (sherpa-onnx).

* fix(install): write only the engine key for parakeet

The parakeet branch duplicated a full default config.json. ConfigManager
deep-merges DEFAULT_CONFIG on load, so every other key was redundant:
a config holding only "engine": "parakeet" still resolves
parakeet_model_size to v3-european along with the VAD, shortcut and
text-injection defaults.

* fix(install): keep push-to-talk defaults and switch existing configs

Writing only the engine key left config.json without a shortcuts
section, which _migrate_shortcuts_config reads as a pre-push-to-talk
upgrade and pins back to ctrl+ctrl/toggle - contradicting the
installer's own "Hold Right Alt while you speak" message. Seed that
section too.

Point the config at parakeet with set_configured_engine rather than only
on first create, so --engine=parakeet also switches a machine that
already has a config.json, and give print_welcome_message a parakeet arm
so the summary is not blank.

The interactive picker overwrites SELECTED_ENGINE, so document the flag
combination that reaches this branch.

* fix(asr): pin Parakeet bundles and report one progress bar

Pin each bundle to a Hugging Face commit instead of resolve/main, so a
file replaced upstream cannot change what an install downloads. This is
the same approach whispercpp_model_info takes with its pinned revision.

Remap each file's progress into its slice of the bundle. The four files
were reported through the single-file downloader independently, so the
fraction ran to 1.0 and restarted four times; it now advances once and
names the file being fetched.

* fix(asr): reject a truncated Parakeet download

_stream_model_download only rejects an empty body, so a truncated but
nonempty response was renamed into the model directory and then reported
as installed, because is_model_downloaded checks existence.

Record the byte size of each file at the pinned revision and check it
before the rename, the same order whisper.cpp verifies its digest in. A
short file is a truncated transfer rather than an upstream change, since
the revision is pinned, so it raises and the temp file is removed.

* style(asr): annotate the Parakeet model catalog

mypy inferred the catalog values as object, so reading file_sizes out of
an entry raised attr-defined and no-any-return under `just typecheck`.

* fix(asr): pin Parakeet digests in model_checksums.txt

AGENTS.md requires every speech model to be verified against a pinned
digest before install, failing closed on an unpinned one. Parakeet was
checking only the byte size.

Teach scripts/generate-model-checksums.py to emit Parakeet entries: the
ONNX files are LFS so Hugging Face publishes their sha256, and tokens.txt
is hashed from the downloaded bytes the way VOSK archives are. Several
bundles ship the same file names, so a key carries the model name.

_download_parakeet_model now calls verify_model_file before the rename
and catches ChecksumError alongside the other download failures, matching
the whisper.cpp downloader.

* fix(asr): verify on-disk Parakeet models before load

Existing bundle files were trusted by path alone, so a leftover or
copied-in tree could reach sherpa-onnx unchecked. Hash each file against
its pin on download skip and on init, and cover the Parakeet pins in CI.

* fix(ui): hide Parakeet language picker that ignores the setting

Parakeet language coverage is the model choice, not a catalog language.
Hide the picker and persist auto so Settings does not write a lying value.

* fix(asr): normalize Parakeet language to auto at startup

CLI --language and stale saved languages still reached SpeechRecognitionManager
even though Parakeet ignores catalog language. Force auto on the shared helper
used by main and the manager so stored settings stay honest.

* fix(ui): restore language after leaving Parakeet

Remember the prior Whisper/cpp language across a Parakeet visit so switching
back does not leave recognition stuck on auto. Keep Parakeet forced to auto
and the picker hidden. Also satisfy black on the startup normalizer path.

* fix(ui): keep language memory across Vosk fallback

Vosk's unsupported-language en-us coercion was writing into
_last_non_parakeet_language, so Whisper→Parakeet→Vosk→Whisper restored
en-us instead of the user's Greek (or other) preference. Remember the
pre-coercion catalog language and prefer it when leaving Vosk.

* fix(ui): keep language memory when entering Parakeet from Vosk

Vosk's coerced en-us fallback was still written into
_last_non_parakeet_language on Parakeet entry, so Vosk→Parakeet→Whisper
restored en-us instead of the pre-coercion preference (Greek or auto).
Share one coerced-fallback predicate across entry, leave-Vosk, and language
sync, and cover those paths with regression tests.

* fix(tests): set initializing flags false on language-change stubs

Plain Mock attributes are truthy and were short-circuiting the handler.

---------

Co-authored-by: Alex Thunder <AlexThunder@gmail.com>
2026-09-07 14:32:13 -07:00
Jatin K MalikandGitHub 40bc31bebf fix(settings): stop freezing when applying an already-downloaded model (#790)
* fix(settings): apply already-downloaded models off the GTK main loop

Changing Model Size, Specialization, or Language froze Settings when the
model was already on disk, because reconfigure ran in the changed handler.
Move that apply onto a worker thread (same shape as the download path) and
keep the apply-guard held until GLib.idle_add clears it so a second pick
cannot start a parallel load.

Fixes #789.

* fix(settings): guard Test/apply while downloaded-model worker runs

Block apply_settings and Test Dictation while _applying_settings is held
so a concurrent Test click cannot race the shared engine. Annotate the
new worker/helpers per AGENTS.md and cover the no-op/block behavior.

* fix(settings): resync pickers when a second pick lands mid-apply

While an already-downloaded model worker was running, Model Size /
Specialization / Language could still move. The apply guard blocked
saving the new pick, then finish only cleared the flag, so the UI
could show B with dictation still on A. On finish, if the selection
no longer matches the saved config, put the pickers back. Idle
resyncs from workers now skip a destroyed dialog the same way update
check does.

* fix(settings): always resync pickers after model apply finishes

A second Model Size pick on whisper.cpp could move the size combo
while the apply guard skipped rebuilding specialization, so selected
settings still looked like the saved variant and finish skipped the
resync. Always put the pickers back on the saved config when the
worker ends, and restore language from that config too.

* test(settings): prove mid-apply picker handlers round-trip on finish

Call the real size, specialization, and language handlers while the
apply guard is held, and run finish through the real resync path so
whisper.cpp size and language cannot silently stick on the second pick.
2026-09-07 14:11:54 -07:00
Jatin K MalikandGitHub f02a6a7c66 fix(settings): flip whisper.cpp specialization when language is English (#780)
Bare size names like tiny/small/medium are also the multilingual variant
ids, so a fresh config looks like a deliberate multilingual pick and the
.en preference never runs. Retarget those ambiguous ids (and matching
quantized pairs) when language implies English-only vs multilingual.

Fixes #776.
2026-09-07 14:11:44 -07:00
Jatin K MalikandGitHub 16175d9ea3 feat(release): attach Flatpak bundles to v* GitHub Releases (#786)
* feat(release): attach Flatpak bundles to v* GitHub Releases

On each version tag, build x86_64 and aarch64 .flatpak assets with the
same builder pins as flatpak.yml, upload them as workflow artifacts, and
attach Vocalinux-${VERSION}-{arch}.flatpak next to the AppImages.
SHA256SUMS now checksums *.flatpak as well.

The Flathub builder image has no GitHub CLI, so attach is a follow-up
job on ubuntu-latest using gh release upload --clobber. Snap jobs are
unchanged.

Fixes #784

* fix(ci): pin privileged Flatpak builder to an immutable digest

The gnome-50 tag is mutable; both release Flatpak jobs and flatpak.yml
now pull the image by OCI index digest. The integrity test requires
@sha256 rather than the tag alone.
2026-09-05 20:10:13 -07:00
Jatin K MalikandGitHub 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
2026-09-05 18:55:37 -07:00
Jatin K MalikandGitHub 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.
2026-09-05 15:04:48 -07:00
Jatin K MalikandGitHub 6684f6f584 fix(inject): skip leftover IBus on KDE so dictation actually types (#753)
* fix(inject): skip leftover IBus on KDE so dictation actually types

Test Dictation and clipboard paste worked. Inject claimed success and
nothing showed up in Kate, Obsidian, terminals, or browsers.

We started ibus-daemon on X11 even when the session never opted into
IBus, then treated that daemon plus a bare xkb engine as active.
commit_text() returns True. Qt/GTK on KDE never hear it.

Do not spawn the daemon unless GTK_IM_MODULE / QT_IM_MODULE /
XMODIFIERS already name IBus. On KDE, a running daemon without those
env vars is not the session IM. Prefer a live WAYLAND_DISPLAY over a
stale XDG_SESSION_TYPE=x11, and do not take the GNOME scoped-IBus
shortcut on KDE.

See #752

* style: wrap IBus helpers so black is happy

CI lint failed on two lines in the #753 inject fix. Same behavior.

* test(inject): leftover IBus on KDE should be skipped

The CheckDependencies case still expected IBus for a leftover daemon
on KDE. That is the #752 bug. It now expects skip and wtype instead.
2026-09-02 01:02:49 -07:00
Jatin K MalikandGitHub af6b372177 fix(install): Fedora and Arch need glslc, not glslang (#763)
* fix(install): Fedora and Arch need glslc, not glslang

ggml's Vulkan CMake looks for glslc and will not take glslangValidator.
install.sh was still installing glslang on Fedora, Arch, Alpine, Void,
Solus, and Gentoo, so the pywhispercpp source build fell back to a CPU
wheel. Ubuntu and Debian already pull glslc plus patchelf.

See #604.

* fix(install): Gentoo patchelf is under dev-util

sys-devel/patchelf does not exist. emerge fails and install.sh exits,
so a fresh Gentoo install never reaches the whisper.cpp build.

See #604.
2026-08-31 10:43:06 -07:00
Jatin K MalikandGitHub 3262696981 fix(site): VocaGateway family card is Beta (#765)
* fix(site): VocaGateway family card is Beta

PRODUCT.md and vocagateway.vocahq.com already say Beta. The homepage
chip still said Early.

See #764.

* fix(docs): README family table also says Gateway Beta

Same leftover as the homepage chip. Still optional self-hosted, not
on-device.

See #764.
2026-08-31 10:42:05 -07:00
Jatin K MalikandGitHub 71929b7768 Change Vocalinux logo image source
Updated logo image source in README.
2026-08-30 01:20:58 -07:00
Jatin K MalikandGitHub a85e6f7c44 Update README badges and privacy text
Updated badge links and improved privacy badge text in README.
2026-08-30 01:19:57 -07:00
Jatin K MalikandGitHub 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.
2026-08-30 01:12:48 -07:00
Jatin K MalikandGitHub 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.
2026-08-30 00:49:09 -07:00
Jatin K MalikandGitHub 7cdf5a7069 feat(settings): even out dropdowns and tidy the About page (#754)
* feat(settings): even out dropdowns and tidy the About page

Pin combos to a shared right-hand column so long labels cannot stretch a
row. Fold the dictation-tone preview into a two-stage Start/Stop button,
quiet the model status card, shorten Performance copy, and rebuild About
with combined links, channel+Check, family platform tiles, and Talk to us
moved up next to the license.

* fix(settings): keep tone preview on start if the cue fails

Do not flip Start/Stop when preview_tone_cue returns false, so a missing
file or silent player cannot skip the start cue. Also annotate
PreferenceRow.set_title / set_subtitle.

* fix(settings): paint About icons for dark dialogs

Dark ink on Adwaita/Yaru-dark buttons made the family tiles and Talk to us
marks disappear. Pick a light foreground on dark themes (and keep the
original ink on light ones) so the marks match the labels.

* fix(settings): show Android and iOS marks on VocaPhone

VocaPhone covers both, so the family tile now uses the Android robot and
the Apple mark instead of a single phone icon.

* fix(settings): cut repeated About copy

Name the AGPL once on the License row. The VocaHQ blurb is just the
private-dictation line. VocaPhone says TestFlight, not source-only, and
Gateway is self-hosted and headless.

* fix(settings): space About icons from their labels

Talk-to-us buttons and the family tiles were packing the mark against the
text. Give the image an end margin and a wider gap in the family tile.
2026-08-30 00:20:27 -07:00
Jatin K MalikandGitHub 158780fc38 fix(ui): use the canonical VocaHQ Discord invite (#749) 2026-08-29 15:51:27 -07:00
Jatin K MalikandGitHub 65ce9daa19 fix(ibus): sync XWayland layout from GNOME after scoped inject (#742)
Super+Space during recording on GNOME Wayland updates Mutter and IBus
but can leave XWayland on the previous map. Scoped inject restores the
IBus engine and skips setxkbmap (#474), so X apps keep the old layout.

After engine restore, copy GNOME's current XKB source onto XWayland
when DISPLAY is set. Do not replay an empty capture and do not call
restore_xkb_layout for this. Sessions with no X display are left alone.
2026-08-28 23:29:53 -07:00
Jatin K MalikandGitHub 9c3ed49317 fix(tray): stay idle after leftover transcription on toggle stop (#741)
* fix(tray): stay idle after leftover transcription on toggle stop

Toggle stop can time out while leftover audio is still transcribing.
The recognition thread then painted PROCESSING and never went idle, so
the tray stayed red until another Alt+F12.

Skip PROCESSING and LISTENING updates once recording has stopped, always
set IDLE when that thread exits, and paint the engine's current state in
the tray instead of a stale GLib.idle_add argument.

* fix(tray): leftover drain must not idle a new dictation session

stop_recognition already marks IDLE. Forcing IDLE when the leftover
worker exits can clobber a new LISTENING start during leftover drain.
2026-08-28 23:29:46 -07:00
Jatin K MalikandGitHub 7a4d050dba fix(tests): stop leaking Factory.new onto MagicMock (#748)
The IBus test stubs assigned Bus and Factory to the MagicMock class, then
set Factory.new. That attribute lands on unittest.mock.MagicMock itself,
so every mock in the pytest session shares the same .new.

Bus and Factory are instances now, matching Text. A regression test fails
if importing the module puts new on the class.
2026-08-28 23:29:38 -07:00
Jatin K MalikandGitHub 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.
2026-08-27 00:12:27 -07:00
Jatin K MalikandGitHub ffec466d74 docs: align README with live family status and on-device copy (#737)
Replace the 100% offline claims with on-device-after-model-download
language, mark Linux Available now, and add Phone and Gateway to the
family table with live sites and status.
2026-08-24 16:56:34 -07:00
Jatin K MalikandGitHub 00da803a9c fix(web): call the screenshots page v0.16 (#735)
The gallery still labeled the current UI as v0.15 after 0.16.0 shipped.
2026-08-24 11:51:35 -07:00
Jatin K MalikandGitHub 0ac361db19 fix(copy): VocaWin is Beta, still unsigned (#733)
* fix(copy): VocaWin is Beta, still unsigned

Call VocaWin Beta on the site family card, About blurb, and README table, same word as VocaMac. Keep the unsigned / SmartScreen wording. No version tag.

* fix(copy): cite VocaWin v0.1.0-beta.1, no release URL

The git tag exists on VocaHQ/vocawin. GitHub Releases does not. About, README, and the site family card name the tag. Links stay on vocawin.com and the repo.
2026-08-24 11:41:17 -07:00
f43f9dcd72 fix: leftover engine repair, just --no-sync, and ggml verify from #713 (#732)
* fix: leftover engine repair, just --no-sync, and ggml verify from #713

#713 only rewrote a broken config.json toward whisper_cpp, so a reinstall
that kept the old engine still died when vosk was the extra that installed.
verify_configured_engine now prefers SELECTED_ENGINE, then any working extra,
and the vosk fallback rewrites an existing file.

just recipes use uv run --no-sync so they do not prune extras from
just deps-all. CONTRIBUTING lists libgirepository-2.0-dev for Ubuntu 24.04.
An unverified ggml file is not loaded if unlink fails, and an unpinned name
is refused rather than deleted.

* fix(appimage): retry tooling downloads on curl reset

* fix(build): bootstrap .venv for --no-sync just recipes and cover the contributor path in CI

---------

Co-authored-by: Sergey Silaev <hello@8am.dev>
2026-08-24 10:42:52 -07:00
Jatin K MalikandGitHub 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.
2026-08-23 01:33:08 -07:00
a37469c8d5 feat(ui): About page for this app, VocaHQ, and talk to us (#718)
* feat(ui): About page for this app, VocaHQ, and talk to us

Settings About now has three groups. Vocalinux keeps the existing icon,
version, AGPL-3.0, updater, and What's New. The website button opens
vocalinux.com.

Part of VocaHQ lists the family sites with PRODUCT.md status. Talk to us
opens GitHub issues, Discord, X, and email. Official Design marks from
VocaHQ/.github are vendored as-is and painted with iron-white ink.

* fix(ui): unique About Open names and pin Design SVG hashes

Open buttons on About still say Open. Each one now uses its tooltip as
the accessible name so a screen reader can tell vocahq.com from
vocalinux.com from Latest Release.

The Talk-to-us SVG test pins sha256 of the four official marks from
VocaHQ/.github 61c8eee.

* fix(ui): circular app icon and About source code link

The launcher and About header used the square teal fill. Switch
both to the circular family logo (voca-logo.svg). Website stays
vocalinux.com; Source code next to it opens the GitHub repo.

* fix(appimage): pin pywhispercpp to the version install.sh declares

* fix(appimage): pin pywhispercpp at both install sites

* test(docs): match AGENTS.md uv.lock wording

#727 dropped "unused until those phases land". The test still
looked for that line, so any PR that runs Python tests against
current main failed. Pin the current contract instead: uv.lock
is authoritative, and requirements/*.txt are not hand-edited.

---------

Co-authored-by: Sergey Silaev <hello@8am.dev>
2026-08-23 00:50:40 -07:00
Jatin K MalikandGitHub a5e723e1bd web: stand flat Tux on the Open Graph card (#729)
Larry Ewing's Tux sits on the right of the family OG image, next
to the notes window. Same silhouette, filled with ink, paper, and
sun. No gradients.
2026-08-23 00:07:09 -07:00
Jatin K MalikandGitHub 25ceec017d web: restyle vocalinux.com to the Voca family workbench (#728)
* web: restyle vocalinux.com to the Voca family workbench

Bring the Linux marketing site onto the same paper, ink, and teal language
as VocaHQ, VocaMac, VocaPhone, and VocaGateway. Drop the site-wide dark
mode so the canvas stays paper-only, matching the other product sites.

* web: loop the home ribbon without a gap or jump

The ticker used a stretched full-width track, so a -50% slide left an empty
right edge before snapping back. Two equal groups, each wider than the
viewport, scroll by exactly one group width.

* web: match vocahq.com ribbon speed and teal band

The looped ticker was covering six item cycles in 32s, so it raced.
It now spends 28s per cycle like vocahq.com, uses the same dark-teal
band, and pauses when the tab is hidden.

* web: drive the hero panel clock from the visitor's local time

Replace the static 11:24 mock with a 24-hour HH:MM clock in the client
timezone, updated on the minute.

* web: add official platform marks on the family cards

Use the .github brand platform SVGs (Linux, Apple, Android, Windows)
plus the HQ server mark for VocaGateway, matching vocahq.com's product row.

* web: restore a Light/Dark flip on the screenshots page

The captured GTK shots exist in both appearances. A page-local switch
defaults to prefers-color-scheme and stores a tab override. The rest of
the site stays paper-only.

* web: make the homepage CTA say Install, not Get

The button already jumps to the one-line installer. Name that action,
note that there is no account, and keep the same copy in the closer
and the mobile menu.

* web: style install commands as GTK terminals

Command boxes use a headerbar with copy, a centered title, and
window controls on the right. The prompt is user@linux:~$, not
Mac traffic-light chrome.

* web: stop repeating the engine list in the hero

The proof line, the notes window, and the tray terminal all said
the same whisper.cpp / Whisper / VOSK / Remote API line. Engines
stay on the window chips. Proof is now desktop and license facts.
The terminal shows launching into the tray.

* web: reflow copy, color prompts, and add install paths

Drop pretty/balance wrapping so titles and ledes follow the
shell width. Color the user@linux prompt. Hero terminal now
says audio stays on this machine. Ribbon adds project words.
AppImage and source-build sit under the install command.

* web: put X, Discord, and GitHub by the footer mark

Official family social marks sit next to the Vocalinux logo.
Footer columns are Product, Install, Guides, and Project.
The bottom line is Part of VocaHQ, without X11 or whisper.cpp.

* web: add hello@vocahq.com to the footer social row

The official mail mark sits with X, Discord, and GitHub and opens
the family hello@vocahq.com address.

* web: use the circular mark in the header and footer

Header and footer now use the official teal circle lockup instead
of the square app icon.

* web: ship a circular favicon and a family OG card

Favicons use the teal circle lockup. The Open Graph image is redone
in the paper workbench language with the current homepage line.

* web: set the hero to Dictate locally / In any app

Shorter couplet in the VocaHQ display size, with a matching
lede and Open Graph card.

* web: gray the hero terminal note as a comment

The second line was reading as part of the vocalinux command.
It is now a dim # comment under the prompt.
2026-08-22 18:12:08 -07:00
Jatin K MalikandGitHub 4532b32f48 docs: require git worktrees and refresh AGENTS.md (#727)
Rewrite the root agent guide for current layout and toolchain, and make
linked worktrees mandatory so the primary checkout stays on main.
2026-08-21 23:23:51 -07:00
Jatin K MalikandGitHub 0d5e4a66af Welcome new issues and pull requests with a community comment (#723)
Calls the shared VocaHQ/.github workflow so the copy lives in one place.
2026-08-20 20:31:23 -07:00
Jatin K MalikandGitHub 8dc99a3b2c Point Discord and X community links at VocaHQ (#722)
Replace the old Discord invite, switch the X follow shield to @vocahq,
and add Discord to CONTRIBUTING and the site footer.
2026-08-20 19:38:37 -07:00
Jatin K MalikandGitHub 421132ddad fix(web): drop stale 100% offline copy and mark VocaWin alpha (#717)
* fix(web): drop stale 100% offline copy and mark VocaWin alpha

vocalinux.com still called VocaWin planned and said the Linux app was
100% offline. Local engines stay on the machine. Remote API is opt-in.
VocaWin is developer alpha: unsigned NSIS (~12 MB) and MSI (~34 MB),
latest prerelease v0.1.0-alpha.1.

Fixes #709

* fix(web): drop leftover never-leaves claims

Open source and privacy pages still said voice data never leaves the
machine. Local engines stay local. Remote API is opt-in.
2026-08-19 23:47:10 -07:00
Jatin K MalikandGitHub d946668319 fix(audio): replace synthesized catalog tones with preview WAVs (#708)
#707 wrote generated start/stop files for the family picker. Swap those
for the downloaded preview bytes (voca is pair 05) and skip the catalog
ids in generate_sounds.py so they stay put.
2026-08-18 22:02:18 -07:00
Jatin K MalikandGitHub d8ff7052cd fix(settings): stop Test Dictation calling a missing model "no speech" (#702)
* fix(settings): stop Test Dictation calling a missing model "no speech"

start_recognition now returns whether listening actually began. Test
Dictation checks that before starting its timer, so a missing or
auto-paused model gets a real error instead of "No speech detected".

* fix(settings): reconfigure Test Dictation against the live engine

UI matching config.json is not enough when speech_engine is still on
another engine or model size. Compare those too before the listen timer.

* fix(tests): keep settings_dialog lookups on one module

_load_settings_dialog() restored sys.modules but left
vocalinux.ui.settings_dialog pointing at the private reload.
Python 3.9/3.10 mock follows that attribute, so
test_whisper_list_and_delete patched a different copy than it
imported and the expanduser stub hid the real cache.

Point the package attribute back at the restored module, and
patch the imported module object in the deletion test.
2026-08-18 17:59:57 -07:00
Jatin K MalikandGitHub fd23ef0d9d feat(audio): add a family dictation tone picker (#707)
Settings → Audio can pick start/stop cues from the shared catalog
(lift, flick, ember, step, voca, soft, chirp, scale, drop, glass, off).
New installs and any missing or unknown sound_effects.tone value use
voca. A saved id, including off, is left alone.

The old start_recording/stop_recording files stay on disk. They are
no longer the implicit default. Error cues still follow the existing
enable switch.
2026-08-18 17:46:55 -07:00
Jatin K MalikandGitHub 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:-}.
2026-08-18 17:41:37 -07:00
Jatin K MalikandGitHub 609b192fbf chore(icons): switch to shared Voca family mark (#704)
Replace the old traced app icon and tray SVGs with the approved family
mic geometry from VocaHQ/.github brand assets. Regenerate vocalinux.com
favicons and logo PNGs/WebPs from the same source. No version bump.

Per brand book §9 and §10.
2026-08-18 01:11:58 -07:00
Jatin K MalikandGitHub 8e308a1735 fix(docs): rename VocaServer to VocaGateway; reuse GITHUB_REPO_URL (#697)
Point README/CONTRIBUTING sibling links at VocaHQ/vocagateway, and drive
homepage GitHub links and install/uninstall commands from GITHUB_REPO_URL.
2026-08-16 19:23:07 -07:00
Jatin K MalikandGitHub 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++.
2026-08-16 18:29:07 -07:00
Jatin K MalikandGitHub 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.
2026-08-16 15:22:22 -07:00
Jatin K MalikandGitHub edca65d22e docs: add Discord and VocaHQ README shields (#695) 2026-08-16 13:44:40 -07:00
Jatin K MalikandGitHub c3a9695a5b fix(audio): open stereo mics at native channel count (#673)
Prefer 2ch before 1ch when PortAudio reports two or more input
channels. Intel SOF digital mics (the Raptor Lake device in #666)
often only support stereo at the PCM; opening them as converted
mono can abort with heap corruption right after capture starts.

Mono-only devices are still never probed with 2 channels (#567).
2026-08-13 00:04:55 -07:00
Jatin K MalikandGitHub 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.
2026-08-12 23:42:36 -07:00
Jatin K MalikandGitHub cfe93f0397 feat(settings): searchable language combobox (#672)
* feat(settings): make language selector a searchable combobox

Turn the Speech Model language picker into a ComboBoxText with an
entry and GtkEntryCompletion so typing filters the 34+ language list
instead of scrolling the whole alphabet.

Fixes #652

* fix(settings): use ComboBoxText text/id column order for language search

Gtk.ComboBoxText stores display text in column 0 and the id in column 1.
Point EntryCompletion at the text column so typing "span" lists Spanish.
2026-08-12 23:32:08 -07:00
Jatin K MalikandGitHub 73ae0def31 fix(tray): reuse Settings and Logs windows instead of duplicating them (#669)
A second tray click on Settings, About, or View Logs now focuses the
existing window instead of opening another copy.

Fixes #653
2026-08-12 23:14:43 -07:00
Jatin K MalikandGitHub 50e9200a1e fix(settings): separate Close from dictation test controls (#670)
Add a horizontal separator between the sidebar Test Dictation
controls and the in-window Close button so Close reads as dialog
chrome rather than part of the testing section.

Fixes #651
2026-08-12 23:12:16 -07:00
Jatin K MalikandGitHub 36b2d923c6 fix(ibus): restore XKB layout after scoped injection on X11 (#665)
Scoped IBus injection (#457) temporarily switches to the Vocalinux
engine but only restored the previous IBus engine, not the XKB map.
On X11 that left non-US layouts (e.g. Brazilian ABNT2) stuck on us
after dictation. Capture XKB before the switch and re-apply it in
the inject_text finally block (Wayland remains a no-op via #474).

Fixes #664
2026-08-11 02:32:46 +05:30
Jatin K MalikandGitHub 068530535b chore(license): migrate from GPL-3.0 to AGPL-3.0 (#660)
Align Vocalinux with the other VocaHQ distribution projects
(VocaMac, VocaPhone, VocaServer) under AGPL-3.0.
2026-08-08 13:10:37 -07:00