Commit Graph
529 Commits
Author SHA1 Message Date
244b62c8ad refactor(installer): split implementation into sourced modules (#862)
* refactor(installer): split implementation into sourced modules

* test(installer): harden module path coverage

* fix(installer): qualify interactive privacy and engine claims

The guided installer overstated local-only privacy and ranked
whisper.cpp without evidence. Match PRODUCT.md: on-device local
engines, optional Remote API, and the ~33 language catalog plus
Auto-detect.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-29 01:07:16 -07:00
38e5be70ba feat(parakeet): add optional verified Orukeet model (#840)
* feat(parakeet): add optional verified Orukeet model

* fix(parakeet): recover from invalid release manifests

* chore: mark Orukeet tip verified

Empty signed tip so the ready-bar Verified gate clears on contributor unsigned history.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-29 01:06:54 -07:00
7bdbedce0b feat(language): follow the active keyboard layout while dictating (#837)
* feat(language): follow the active keyboard layout while dictating

Adds a "Follow keyboard layout" switch to Settings -> Speech Model ->
Advanced. With it on, the active keyboard layout is re-read at the start
of every dictation and the matching language is used, so someone who
switches layouts to work in two languages gets the right one without
opening Settings.

#796 already seeded the language once on first run, but from the
*configured* primary layout, which never changes as the user switches
between them: a "fr,us" setup reports fr forever. detect_active_keyboard_
layout() reads GNOME's input sources instead. It takes mru-sources rather
than the current index, because that index goes stale -- observed still
reading 0 after a switch to the second layout -- which is why the IBus
injection path already trusts the MRU list (#497, #738).

An active source that is an IBus engine rather than an xkb layout is a
real answer that cannot be mapped, so the language is left to the engine's
own auto-detect. Borrowing the configured xkb layout there would pin
English for someone typing Japanese through mozc-jp.

The mode is a switch over the language picker rather than an entry inside
it, since it is a mode and not a language. While on, the picker is
disabled and shows what the layout resolves to; turning it off pins that.

A language change no longer re-initialises the engine when the loaded
model takes the language per utterance, which is the whole whisper
family, so following a layout costs a settings read rather than a model
reload. VOSK loads one model per language, Parakeet ignores the picker,
and an English-only model has no non-English weights, so Settings refuses
the mode for all three rather than offering a switch that does nothing.

Fixes #821

* fix(language): keep follow mode armed and map IBus engines

Swap English-only whisper.cpp weights to the multilingual sibling while
passing the layout sentinel into reconfigure, so follow mode stays armed.
Map common GNOME IBus engines onto catalogue languages instead of
leaving them unmapped.

* fix(language): stop IBus hyphen false-maps and gate the sibling swap

The last-token hyphen heuristic mapped mozc-us and foo-us to English.
Restrict it to *-jp/*-kr and name Mozc's extra engines as Japanese.
Only whisper.cpp may consult the whisper.cpp download catalog.

* fix(language): keep multilingual weights while following layout

The picker shows the active layout while follow is on, often en-us.
Deriving whisper.cpp .en from that display made Settings refuse the
mode as English-only, then the next auto-apply persisted a concrete
language and disarmed follow. Use LANGUAGE_FOLLOWS_LAYOUT for .en vs
multilingual derivation instead, including on the load path before
the switch is synced.

* fix(language): type follow helpers and log sibling-swap failures

* fix(test): stub follow-layout model rec so mocked psutil does not blow up

follow-mode English-only guard calls get_recommended_whispercpp_model, which
reads RAM through psutil. Full CI leaves a MagicMock in sys.modules["psutil"],
so ram_gb >= 16 raises TypeError. Patch the settings_dialog alias to return a
multilingual tiny rec so the assertion still holds without real RAM.

* fix(language): catch specific errors on layout lookup and sibling swap

* fix(language): swallow more sibling-swap reconfigure failures

ImportError, TypeError, AttributeError, and ChecksumError from the
multilingual swap must not abort dictation or disarm follow mode.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-29 01:06:32 -07:00
Jatin K MalikandGitHub 9646011e77 fix(settings): speech model follow-ups from QA (#836)
* fix(settings): speech model follow-ups from #834

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

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

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

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

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

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

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

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

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

test_settings_dialog reimports the dialog module and leaves the replacement in sys.modules. Recorder methods still close over the original dict, so the harness never recorded timeouts or io watches.
2026-09-29 01:05:46 -07:00
a38cff59ea fix(vosk): refuse to load a model that would exceed available memory (#850)
* fix(vosk): refuse to load a model that would exceed available memory

vosk.Model() allocates the whole model in one spike during loading,
including the RNNLM rescoring component some models ship, with no way
to check the requirement up front or load incrementally. Walk the
model directory, compare its on-disk size against psutil's available
memory with a 1.5x margin, and raise before calling into the native
loader instead of letting the kernel OOM killer take the process down
mid-load.

Fixes #676

* fix(vosk): match memory safety margin to the documented peak

greptile flagged the guard's own comment: loading can spike to ~2x the
model's on-disk size, but the margin only required 1.5x available memory,
so a load between those two multiples could still trigger the OOM this
guard exists to prevent.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-29 01:05:24 -07:00
Jatin K MalikandGitHub 74d6f505d4 fix(update): fall back when GitHub API rate-limits (#846)
Shared NAT hits the unauthenticated api.github.com quota and the
update check logged a WARNING that looked like a failure. On 403
or 429, look up the latest stable tag from the releases page
JSON, and stay quiet if that also fails. Nightly still refuses
to treat the stable latest tag as a nightly.

Fixes #845
2026-09-29 01:05:02 -07:00
f1327b7d1d chore(deps): bump the github-actions group across 1 directory with 2 updates (#870)
* chore(deps): bump the github-actions group across 1 directory with 2 updates

Bumps the github-actions group with 2 updates in the / directory: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [codecov/codecov-action](https://github.com/codecov/codecov-action).


Updates `astral-sh/setup-uv` from 10.1.0 to 10.2.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/bec219d24cd3e171d82865faccec33120bb574f4...c18668ad3cf93ea998bef934396af7bb5c839dc7)

Updates `codecov/codecov-action` from 7.0.0 to 7.1.1
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/fb8b3582c8e4def4969c97caa2f19720cb33a72f...303a32d7a59b442fa8d48b6a1cc6825c09c847a5)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: codecov/codecov-action
  dependency-version: 7.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: retrigger greptile review

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-28 09:07:47 -07:00
Kanishk PachauriandGitHub f4e86f6325 fix(deps): clear open dependency advisories (#869) 2026-09-24 04:42:08 +05:30
Kanishk PachauriandGitHub 2e72fd4ded ci: limit workflow tokens to repository reads (#866) 2026-09-23 22:03:07 +05:30
Kanishk PachauriandGitHub 51edc41546 fix(web): update vulnerable site dependencies (#865) 2026-09-23 21:29:22 +05:30
Jatin K MalikandGitHub 5b4e2c3572 fix(snap): add hardware-observe so hotkeys can read input devices (#858)
Snap confinement denies /proc/bus/input/devices unless hardware-observe
is connected. Declare the plug, tell users to connect it next to
raw-input, and update the in-app hints.

Fixes #857
2026-09-23 00:09:54 -07:00
acd90fdd05 fix(installer): install hash-pinned dependencies and build tools (#856)
* fix(installer): install hash-pinned dependencies and build tools

* fix(installer): add cairo-devel for the pinned pycairo build

* fix(installer): reject missing or empty bootstrap exports

* fix(installer): preserve remote behavior for older tags

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-23 00:09:48 -07:00
Jatin K MalikandGitHub 4df612dd7a fix(settings): drop Test Dictation scrolled-window stripe (#853)
* fix(settings): drop Test Dictation scrolled-window stripe

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

Fixes #847

* Add missing test return annotation
2026-09-23 00:09:43 -07:00
Kanishk PachauriandGitHub 33492d7cb1 docs: mark VocaMac as available now (v1.0.0) (#859) 2026-09-23 06:04:31 +05:30
dd3bd69add Pin the ydotoold probe in the wtype fallback tests (#841)
* test(injection): pin the ydotoold probe so local runs match CI

* chore(pytest): filter the three known warnings out of the summary

* fix(pytest): stop hiding save_settings deprecation globally

Keep the ignore on the one test that still exercises the deprecated alias so the rest of the suite can surface new save_settings() calls.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-20 13:40:46 -07:00
Sergey SilaevandGitHub 923bc42708 fix(ci): verify a release whose run failed after publishing (#842)
* fix(ci): verify a release whose run failed after publishing

* fix(ci): only forgive a confirmed 404 when verifying a published release
2026-09-20 13:03:23 -07:00
Amir FathiandGitHub c7b8cd4802 fix(ibus): stop the xkb:us::eng guard flipping GNOME/X11 to a US layout (#827)
The gsettings fallback for the bogus xkb:us::eng default only fired on
GNOME/Wayland; on GNOME/X11 the default is accepted and written back
into the X keymap, permanently switching the user off their real
layout (Hungarian, Brazilian ABNT2, etc).

_get_gnome_current_source() also had no fallback when mru-sources is
empty (a fresh session with a single input source never populates the
MRU list), so even with the guard fixed the recovery path still
returned None. It now falls back to the static sources list.

Fixes #699
2026-09-20 11:18:08 -07:00
Jatin K MalikandGitHub 1f121546fb fix(release): attach the snap to GitHub Releases (#839)
* fix(release): attach the snap to GitHub Releases

v0.17.0 packed the snap, then Store review of uinput failed the job, so
the file never reached the tag. Build and attach first. A Store hold is
a warning, not a failed release. SHA256SUMS covers the .snap.

snap-backfill.yml rebuilds an existing tag, appends the snap to the
manifest, and attests only that file.

* fix(release): keep snap tags out of the shell and fail real Store errors

Pass the tag through the environment like verify-release.yml. Treat only
an allow-installation review as a successful Store publish. snap-backfill
runs verify_release.py after it rewrites SHA256SUMS.

* style(test): format snap-backfill shell-injection guard with black
2026-09-16 19:40:13 -07:00
Jatin K MalikandGitHub 47dd983def docs(readme): drop ecosystem status and the VocaWin SmartScreen note (#835)
The family table was going stale on every sibling release. Keep project
links only. Also say honestly that the 0.17.0 snap is still waiting on
Store review of the uinput plug.
2026-09-16 01:03:46 -07:00
Jatin K MalikandGitHub 26f90b6998 chore(release): prepare v0.17.0
Minor on the stable line. Faster Whisper and Parakeet extras, Speech Model simple setup, Snap uinput, Flatpak bundles on the tag, and QA fixes from the release branch.

Fixes tracked in the PR. Post-release follow-ups: #834.
2026-09-16 00:18:09 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
60213454eb chore(deps): bump astral-sh/setup-uv in the github-actions group (#831)
Bumps the github-actions group with 1 update: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv).


Updates `astral-sh/setup-uv` from 10.0.1 to 10.1.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/20cfd1bf945f4377ade1205e4dbc17946fc9a30d...bec219d24cd3e171d82865faccec33120bb574f4)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-15 22:42:44 -07:00
b90bdb075d fix(deps): export and cap every engine extra (#828)
* fix(deps): export and cap every engine extra

* fix(deps): docstring main and tighten export pairing tests

Give check_exports.main the required public docstring, annotate the
dependency export test helpers, and require --extra to sit with its -o
on the same just lock line (whisper pip-compile stays special-cased).

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-15 22:41:26 -07:00
Jatin K MalikandGitHub 092dde105e fix(audio): open 3-8ch HDA mics at native layout (#813) (#829)
* fix(audio): open 3-8ch HDA mics at native layout (#813)

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #816

* docs: portal no-preference still checks gsettings

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

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

PRs no longer watch src/**. python-test already covers those, and the
expensive compile does not. push to main still does, so a sandbox
install break fails the default branch.
2026-09-12 13:42:42 -07:00
d5656b8060 fix(text-injection): route XWayland xdotool fallback through clipboard paste (#680)
* fix(text-injection): route XWayland xdotool fallback through clipboard paste

xdotool type simulates keypresses against the active X keyboard layout, so
a character the layout doesn't map comes out as the wrong glyph or garbled
(#657). The native ydotool path already avoids this by pasting via the
clipboard instead of typing (_inject_with_wayland_tool); the XWayland
xdotool fallback (_inject_with_xdotool) never got the same treatment.

Prefer clipboard paste on that fallback too when ydotool is installed,
falling back to raw xdotool type only when no paste tool is available or
the paste itself fails.

Fixes #657

* fix(text-injection): X11 clipboard paste for the XWayland fallback

jatinkrmalik's review on #680 pointed out the AppImage build does not
bundle ydotool, so the prior fix's ydotool clipboard paste never ran on
the actual reported setup; it fell through to the same layout-dependent
xdotool type as before. It also flagged a real paste-target mismatch:
an XWayland window reads the X11 CLIPBOARD selection, not the Wayland
clipboard wl-copy/ydotool write to.

The XWayland fallback now copies via xclip/xsel and pastes with
xdotool key --clearmodifiers ctrl+v when either is installed, waiting
for held modifiers to release first (the same wait already used by the
native ydotool path). ydotool clipboard paste stays as a second
fallback when xclip/xsel are absent; xdotool type is the last resort.

New regression test drives inject_text() end-to-end with no ydotool on
PATH, reproducing the reported AppImage/GNOME log rather than calling
the fallback method directly, so it also covers the recovery check
that could otherwise skip this path.

* fix(text-injection): thread x11_only through clipboard read/clear so restore matches the paste backend

jatinkrmalik's rebase pulled in the layout-aware paste work from #788,
and greptile's review on the merged branch caught a real gap in this
PR's own XWayland path: _copy_to_clipboard already takes x11_only to
write the X11 CLIPBOARD via xclip/xsel (since that is what an xdotool
paste actually reads), but _read_clipboard and _clear_clipboard never
gained the same parameter. On a Wayland host, _read_clipboard prefers
wl-paste, so the "previous clipboard" saved before an XWayland paste
could be read from an unrelated Wayland clipboard, and restoring it
back through _copy_to_clipboard with no x11_only wrote to that same
wrong backend, leaving the X11 CLIPBOARD stuck on the dictated text.

Threads x11_only through both helpers and every restore call site
(the immediate-failure path and the delayed _restore closure), so an
XWayland paste captures, clears, and restores the same X11 CLIPBOARD
selection it wrote to.

* fix(text-injection): annotate clipboard helpers and XWayland paste tests

Greptile P2 on #680: give `_get_clipboard_tools` a return annotation and
mark the new XWayland clipboard-paste regression tests `-> None`.

* fix(text-injection): derive XWayland paste from environment

Drop the ydotool/wl-copy second fallback on WAYLAND_XDOTOOL (wrong
clipboard, and rc=0 is not a paste) and the x11_only flag stack. Paste
picks xclip/xsel + xdotool from environment; user-facing copy stays
on the session clipboard.

* fix(text-injection): always pass host_env() to the paste chord

The AST guard only recognizes a direct host_env() call. The ternary
around it looked like we were handing the AppImage library path to
xdotool.

* test(text-injection): give XWayland injector stubs clipboard restore state

_inject_with_xdotool now pastes first, and that path reads
_clipboard_restore_target. The __new__ helper in test_text_injector_ext
never set it.

* fix(text-injection): skip clipboard restore on a stale paste failure

If a newer paste has already copied its text, the older paste's failure
handler must not write its saved clipboard back. Success restores already
honored the generation counter; the failure path did not.

* fix(text-injection): keep original clipboard across a stale paste failure

Publish the restore target as soon as copy succeeds (first writer wins)
so an overlapping paste inherits it. If the older paste then fails, the
newer restore still writes the pre-first clipboard, not intermediate
dictated text.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-12 13:16:10 -07:00
Jatin K MalikandGitHub 4b6e81d441 fix(snap): type into native Wayland apps with ydotool (#823)
* fix(snap): inject into native Wayland apps via ydotool

Edge rev 7 only ships wtype and xdotool. GNOME rejects the virtual
keyboard protocol, so the snap falls back to xdotool and types only
into XWayland windows. Native GTK/Qt apps get nothing.

Stage ydotool, request the uinput plug, and prefer ydotool when wtype
is rejected. Distro ydotool 0.1.x still needs a writable /dev/uinput;
log `snap connect vocalinux:uinput` when the Snap cannot open it.

* docs(snap): v0.16.2 edge has no uinput plug

The store snap (0.16.2, latest/edge rev 7) does not declare uinput.
`sudo snap connect vocalinux:uinput` fails with "has no plug named
uinput" and dictation only reaches XWayland apps. Spell that out in
README, INSTALL, UPDATE, the website changelog, and the store copy so
people do not run the 0.17 connect command against rev 7.

* docs(snap): drop the unconfirmed 0.17 version from uinput notes

Greptile P2: the tree is still 0.16.2 and the release process does not
name the next tag. Tell people to refresh until `snap info` lists the
uinput plug, instead of waiting on a version we have not cut.
2026-09-12 12:58:35 -07:00
Jatin K MalikandGitHub 70d5ce7688 fix(snap): put gnome-platform first so SVG icons load (#822)
core24 desktop-launch rebuilds gdk-pixbuf-loaders.cache from the first
LD_LIBRARY_PATH directory that ends in the arch triplet. Stage-packages
drop a gdk-pixbuf tree without libpixbufloader_svg.so at
$SNAP/usr/lib/<triplet>, ahead of gnome-platform, so About-page SVGs
fail with "Image type svg is not supported".

Fixes the same core24 GTK3 gap documented at:
https://forum.snapcraft.io/t/missing-svg-support-for-gtk3-in-core24/48851
2026-09-12 12:43:39 -07:00
1fac26ff07 fix(packaging): derive the Flatpak and AUR dependency sets from the lock (#819)
* fix(packaging): derive the Flatpak and AUR dependency sets from the lock

* test(packaging): add return annotations on new packaging tests

Greptile asked for -> None on the new tests, matching AGENTS.md.

* test(packaging): type parametrized Flatpak source arguments

Greptile asked for type hints on the source parameter, matching AGENTS.md.

---------

Co-authored-by: jatinkrmalik <jatinkrmalik@gmail.com>
2026-09-12 12:41:06 -07:00
4d8c58f536 feat(shortcuts): allow bare function keys for push-to-talk (#815)
* feat(shortcuts): allow bare function keys for push-to-talk

Accept F1–F24 shortcuts without a modifier so dictation can use a
dedicated key (e.g. F10) instead of Shift/Ctrl/Alt combos that fire
while typing. Settings shortcut capture records bare function keys too.

* fix(shortcuts): evdev availability for bare function keys

Check EV_KEY capabilities for bare F1-F24 shortcuts instead of an empty
modifier name. Add availability regression tests and type hints on new tests.

* refactor(shortcuts): fold bare F-keys into ShortcutSpec

Teach primary_modifier to return the bare key so parse_shortcut and the
backend stop special-casing empty modifiers. Capture now asks the parser
instead of re-encoding the F-key allow-list, and evdev shares one
capability probe for modifiers and main keys.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-12 12:39:25 -07:00
Jatin K MalikandGitHub 0ef1f09cec fix(web): bump homepage VocaWin card to v0.1.1-beta (#812)
Homepage family strip still pinned the old Win tag. Match PRODUCT.md and
the published VocaWin release. Keep the unsigned / SmartScreen line.

Fixes #811.
2026-09-12 12:04:24 -07:00
SergeyandGitHub b45a26e1cd feat(ci): run install.sh in the distro matrix instead of grepping it (#810) 2026-09-09 23:43:40 -07:00
Jatin K MalikandGitHub 94c2eaac08 fix(tray): make PTT tray icon turn red on every hold (#809)
* fix(tray): refresh PTT icon when AppIndicator reuses a path

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

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

* test(tray): assert alternating IconThemePath nudge args

Greptile P2: the PTT redraw regression only checked set_icon_theme_path
call count, so identical paths would still pass. Assert the alternating
ICON_DIR / ICON_DIR+os.sep arguments that drive the host reload.
2026-09-08 20:56:18 -07:00
ab1400b7c1 fix(injection): read WM_CLASS with xprop so xdotool stops dumping core (#807)
* fix(injection): read WM_CLASS with xprop so xdotool stops dumping core

`xdotool getwindowclassname` hands an uninitialized XClassHint to XFree
when the window carries no WM_CLASS, so it aborts with SIGABRT
("free(): invalid size") and leaves a coredump behind. On KDE Plasma
Wayland that is the normal case, not an edge case: while a native
Wayland client holds focus, KWin points XWayland's _NET_ACTIVE_WINDOW at
a property-less window, and `xdotool getactivewindow` reports it happily.

Every paste-shortcut probe therefore killed a child process and filled
the journal with coredumps. Injection still worked, because the abort
only made the probe return an empty class, so the symptom was silent
except in `coredumpctl`.

read_wm_class() now reads the property with xprop, which handles a
missing WM_CLASS as an ordinary empty result, and falls back to xdotool
only when xprop is absent and the window looks like a real client (it has
a pid or a title). The debug window-info logging in text_injector.py goes
through the same helper.

Reproduced on Fedora 44, Plasma 6 Wayland, xdotool 3.20211022.1:
`xdotool getwindowclassname 0x200000` exits 134 on the window that
`_NET_ACTIVE_WINDOW` points at.

* fix(injection): keep xdotool classname for class-only X11 clients

When xprop is missing, always allow the xdotool classname fallback so a
real terminal that exposes WM_CLASS but no title or _NET_WM_PID still
classifies as a terminal (Ctrl+Shift+V). xprop remains the primary path,
so Plasma Wayland with xprop never calls getwindowclassname.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-08 20:52:34 -07:00
Kanishk PachauriandGitHub d1ce96cf2a docs: prohibit agent co-authors (#808) 2026-09-09 02:41:55 +05:30
Jatin K MalikandGitHub f48bdda525 docs: improve project documentation for a clearer public surface (#634)
* docs: polish project documentation for a clearer public surface

Rewrite the README and supporting docs for a more professional first
impression: less emoji and badge noise, install-first structure, and a
real documentation index. Add a Contributor Covenant, tighten SECURITY
and GitHub templates, and make distro/update guides read as product docs
instead of internal status notes.

* docs: split install guide and add changelog plus support pages

Break INSTALL.md into a short primary guide, manual/PyPI package lists,
and troubleshooting. Add root CHANGELOG.md and SUPPORT.md, wire them
into the docs index and README, and drop the outdated screenshot lag note
now that v0.15 assets are on main.

* docs: humanize PR copy and remove em dashes

Strip em/en dashes from the documentation rewritten in this PR, fix
mid-sentence colon fallout from that pass, and smooth a few stiff
phrases so the public docs read more natural.

* docs: list Faster Whisper after rebase onto main

Installer and CLI already accept faster_whisper. Document it next to
the other engines, drop the outdated screenshot lag note, and keep the
on-device wording honest for local vs remote.
2026-09-07 22:51:14 -07:00
Jatin K MalikandGitHub cdfe541c25 feat(speech_recognition): add faster-whisper engine backend (#543)
* feat(speech_recognition): add EngineType enum and Engine Protocol

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

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

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

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

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

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

- Import missing get_compute_type helper

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

- Clear _has_torch_cuda lru_cache around exception test to avoid cached True

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

- Restore registry module state after test

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

Cover previously missing lines in:

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

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

- engine registry import-error path

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

- Add faster_whisper to --engine help text and examples

- Add faster-whisper as interactive option 5 with description

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

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

- Add smoke test for faster-whisper import

- Update welcome message engine display name

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

* refactor(faster_whisper): apply ponytail review cleanups

- Inline FASTER_WHISPER_MODEL_INFO dict literal; drop unused AVAILABLE_MODELS/MODEL_SIZES

- Reuse _has_torch_cuda() in FasterWhisperEngine.device property

- Remove redundant model None guard after is_ready() verification

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

- Update device tests to manage _has_torch_cuda lru_cache

- Remove test for deleted dead-code guard

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

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

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

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

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

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

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

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

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

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

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

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

* chore(speech_recognition): sign tip after rollback follow-up
2026-09-07 20:47:56 -07:00
7664266823 feat(commands): localize punctuation voice commands (#640) (#642)
* feat(commands): localize punctuation voice commands by language

Voice command phrases were English-only even when recognition language
was set to Italian, French, etc. Merge locale punctuation/line-break
aliases from the recognition language into CommandProcessor, keep
English phrases available, and document the behavior.

Fixes #640

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

* fix(commands): keep line breaks and French apostrophe aliases

Attach-left punctuation now eats only horizontal whitespace, so a
localized "new line" followed by a period or comma still inserts a
newline. French aliases include ASCII, U+2019, and backtick apostrophes,
and longest-match still wins so "point" cannot fire inside
"point d'interrogation".

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-09-07 20:47:12 -07:00
Jatin K MalikandGitHub f90e4d5765 fix(injection): release ydotool Ctrl after a timed-out paste (#675)
subprocess.run(..., timeout=3) SIGKILLs the ydotool client. ydotoold
keeps any keys it already applied, so virtual Left Ctrl can stay held
until logout. Send a best-effort V-up/Ctrl-up (or a legacy ctrl tap)
after paste failure.

Fixes #658
2026-09-07 20:47:07 -07:00
Jatin K MalikandGitHub 2c6acbc15e fix(inject): layout-aware clipboard paste for Neo and friends (#788)
* fix(inject): resolve paste chord for non-QWERTY layouts

ydotool was sending physical KEY_V (47) for Ctrl+V. On German Neo that
key types p, so paste opened Print instead. Prefer wtype keysyms when
usable; otherwise resolve the evdev key for Latin v via xkbcommon, and
read KDE kxkbrc so Plasma Wayland Neo users are covered.

Fixes #787

* fix(inject): read Plasma active layout from real session sources

kxkbrc has no CurrentLayout/LayoutIndex, so multi-layout Neo always
fell back to us at index 0 and paste still hit KEY_V. Prefer D-Bus
getLayout or layout_memory.xml for the active layout, keep kxkbrc
for the list only, and prefer KDE over leftover GNOME on Plasma.
2026-09-07 18:10:43 -07:00
Jatin K MalikandGitHub d07c70dbc9 fix(audio): stop sink-wake from clipping start/stop cues (#804)
* fix(audio): prepend silent preroll so sink-wake does not clip cues

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

Fixes #800

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

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

Fixes #800

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

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

Fixes #800

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

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

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

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

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

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

* fix(asr): narrow Parakeet transcription exception handling

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

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

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

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

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

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

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

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

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

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

Refs #779

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

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

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

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

Refs #779

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

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

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

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

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

Refs #779

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

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

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

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

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

Refs #779

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

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

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

Refs #779

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

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

Four things reported from the installed build, all real.

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

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

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

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

Refs #779

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

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

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

Refs #779

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

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

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

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

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

Refs #779

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

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

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

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

Refs #779

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

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

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

Refs #779

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

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

Refs #779

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

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

Refs #779

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

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

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

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

Refs #779

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix(asr): reject a truncated Parakeet download

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

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

* style(asr): annotate the Parakeet model catalog

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

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

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

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

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

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

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

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

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

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

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

* fix(ui): restore language after leaving Parakeet

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

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

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

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

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

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

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

---------

Co-authored-by: Alex Thunder <AlexThunder@gmail.com>
2026-09-07 14:32:13 -07:00
946e19ee41 feat(settings): make the language picker searchable while its list is open (#798)
* 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

* fix(settings): empty Enter must not auto-apply first language

Opening the searchable language picker clears the filter, so every row
is visible. Enter then took the first store row via _first_visible_row,
emitted changed, and auto-applied a language the user never chose.

Skip first-match activation when the needle is empty (or whitespace);
Enter only selects after a non-empty filter. Add regression coverage
and type the SearchablePicker surface (Greptile P2).

* fix(settings): finish SearchablePicker annotations

Greptile P2 stayed open after the empty-Enter fix: handler params,
get_child and _first_visible_row returns, and the ComboBoxText-compat
methods still lacked docs. Annotate the remaining signatures, add
one-line public docs, and type the test helpers. Behavior unchanged.

---------

Co-authored-by: Kacper Paczos <23406555+kacperpaczos@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-07 14:12:04 -07:00
9a0a677271 feat(models): make the recommendation applicable and stop ignoring the disk (#797)
* feat(models): make the recommendation applicable and stop ignoring the disk

The panel worked out which model fits the hardware and the language, printed it
as a plain label, and left the pickers on the wrong one. Applying it meant
hitting two dropdowns by hand after decoding that "Small EN" means size Small
plus specialization English-only.

Three changes:

- the recommendation gets a button that moves both pickers at once
- before offering a download, look at what is already on disk. A candidate has
  to be no smaller than the recommendation, and an English-only recommendation
  is not answered with multilingual weights, which weigh the same and recognise
  English worse (#776). So nothing is ever quietly downgraded.
- the size list shows weight and the downloaded marker, the way the
  specialization list already did. A size is a download decision too.

Closes #778

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

* fix(settings): keep recommendation fresh across language and apply

Changing EN to PL left Use it pointed at the old English-only target, and
clicking it briefly auto-applied the default size (full large) before the
intended turbo specialization landed. Refresh the recommendation card after
language changes, and move both pickers under _populating_models before the
single apply.

* fix(settings): type the recommendation language and apply callbacks

* style(settings): wrap whispercpp model info import for isort

Lint Python was failing the isort check on the single-line
WHISPERCPP_MODEL_INFO import. Parenthesize it so black/isort agree.

---------

Co-authored-by: Kacper Paczos <23406555+kacperpaczos@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-07 14:11:59 -07:00
Jatin K MalikandGitHub 40bc31bebf fix(settings): stop freezing when applying an already-downloaded model (#790)
* fix(settings): apply already-downloaded models off the GTK main loop

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

Fixes #789.

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

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

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

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

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

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

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

Call the real size, specialization, and language handlers while the
apply guard is held, and run finish through the real resync path so
whisper.cpp size and language cannot silently stick on the second pick.
2026-09-07 14:11:54 -07:00
43bf363f6b fix(models): let the selected language decide the whisper.cpp variant (#795)
* fix(models): let the selected language decide the whisper.cpp variant

whisper_cpp_model_size holds the id that loads the model, and for every size
below "large" the multilingual variant id is the bare size name. A stored
"medium" therefore could not say whether the user picked the multilingual
variant or never picked anything, so the language-aware default never ran and
selecting English left the picker on "Standard multilingual" while the panel
recommended English-only right underneath.

Record the specialization the user picks in its own key. Without that pin the
variant is derived from the language; a value that is not a bare size name was
written by an older build as a deliberate choice and is adopted as the pin, so
existing configurations keep the variant they had.

Closes #776

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

* fix(models): keep language-derived whisper.cpp variants unpinned

Auto-apply was writing the derived specialization into both the pin and
whisper_cpp_model_size, so a later language change fought a stale pin and a
leftover .en id. Only deliberate specializations are pinned now; unpinned
saves store the bare size and resolve against the current language.

* fix(models): keep multilingual whisper.cpp pins across languages

Language auto-apply cleared any pin that matched the new language default,
so an explicit multilingual choice made under English was lost when switching
away and back. Preserve bare-size pins that still match the selection; keep
language-derived and stale .en pins unpinned. Wrap the isort import for CI.

---------

Co-authored-by: Kacper Paczos <23406555+kacperpaczos@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-07 14:11:49 -07:00
Jatin K MalikandGitHub f02a6a7c66 fix(settings): flip whisper.cpp specialization when language is English (#780)
Bare size names like tiny/small/medium are also the multilingual variant
ids, so a fresh config looks like a deliberate multilingual pick and the
.en preference never runs. Retarget those ambiguous ids (and matching
quantized pairs) when language implies English-only vs multilingual.

Fixes #776.
2026-09-07 14:11:44 -07:00