* 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).
* 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
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.
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.
* 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.
* 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.
* 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.
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
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
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
* 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.
* 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#983Fixes#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
* 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.
* 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#987Fixes#990
* test(whispercpp): move new ggml probe tests into their test class
* 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
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
* 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.
* 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
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
* 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
* 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.
* 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.
* 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>
* 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
* 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
* 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
* 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
* 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.
* 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.
* 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.
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
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
* 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
* 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
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.
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.
* 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
* 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.
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.
* 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.
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
Homepage family strip still pinned the old Win tag. Match PRODUCT.md and
the published VocaWin release. Keep the unsigned / SmartScreen line.
Fixes#811.
* 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.
* 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.
* 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
* 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>
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
* 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.
* 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
* 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.
* 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>
* 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>
* 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.
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.
* 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.
* 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
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.
* 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.
* 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.
* 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.
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.
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.
* 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.
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.
* 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.
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.
* 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.
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.
* 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.
* 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>
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.
* 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>
* 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.
* 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.
#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.
* 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.
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.
* 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:-}.
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.
* 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++.
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.
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).
* 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.
* 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.
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
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