The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.
On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.
Refs #18
* feat(parakeet): add optional Underdog Bark 3.0 int8 model
Adds bark-3.0 to the Parakeet model picker: Underdog Bark 3.0, an
English fine-tune of Parakeet TDT 0.6B v3, as the int8 sherpa-onnx
conversion published at velddev/Underdog-Bark-3.0-sherpa-onnx. It runs
on the existing sherpa-onnx recognizer, pinned to a revision with every
file checked against model_checksums.txt, like Orukeet (#840).
The conversion writes the unknown-word token where a quote mark was, so
the Parakeet transcript drops <unk>; the other models never emit it.
* docs(parakeet): say when to pick Bark 3.0; loosen its catalog test
The README now says Bark suits English dictation of conversational
speech and v3-european stays the choice for read or multilingual
speech. The test checks that bark-3.0 is offered rather than that it is
the last catalog entry, so a later model does not break it.
* test(parakeet): add docstrings to the Bark tests
* feat(parakeet): add optional 4-bit Underdog Bark 3.0 build
Adds bark-3.0-int4: the same Bark 3.0 weights quantized to 4-bit with
onnxruntime's HQQ quantizer, for machines short on memory. It needs
about half the RAM of the int8 build (0.64 GB against 1.28 GB peak) and
a 379 MiB download, at two to four times the CPU time (about 2x on
Intel, 3.7x on AMD by the publisher's measurement). The files are
published in velddev/Underdog-Bark-3.0-sherpa-onnx (Hugging Face PR #1),
and both Bark entries now pin its revision 3ae3af1, which holds both
builds; the int8 files are unchanged there.
Catalog entries gain an optional "precision" key, so the loader opens
encoder.int4.onnx and friends instead of assuming int8; every existing
model keeps the int8 default. The README section now covers both
builds in a few lines.
* test(parakeet): type the Bark test signatures
* docs(parakeet): name the Bark sherpa-onnx source and its pins
The README now says where both Bark builds download from (a pinned revision of
velddev/Underdog-Bark-3.0-sherpa-onnx), that it is an unofficial conversion, and
that each file is checked against its bundled SHA-256 pin, as the Orukeet
section does.
---------
Co-authored-by: Jatin <jatinkrmalik@gmail.com>
* chore(release): v0.18.5
- Update version from 0.18.4 to 0.18.5
- Add v0.18.5 sections to CHANGELOG, README, docs/UPDATE.md, metainfo.xml and the website changelog
- Bump website package metadata and softwareVersion schema
- Add the 0.18.5 row to the RELEASE_PROCESS version history
* docs(release): qualify the clipboard-history claim for wl-copy 2.2+
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
- Update version from 0.18.3 to 0.18.4
- Add v0.18.4 sections to CHANGELOG, README, docs/UPDATE.md, metainfo.xml and the website changelog
- Bump website package metadata and softwareVersion schema
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* chore(release): v0.18.3
- Update version from 0.18.2 to 0.18.3
- Add v0.18.3 sections to CHANGELOG, docs/UPDATE.md, metainfo.xml and website changelog
- Bump website package metadata and softwareVersion schema
- Clarify that the uinput-carrying sideload snap is the v0.18.2 asset
* docs(release): include the transcribe modal-grab fix (#965) in v0.18.3 notes
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Jatin K MalikDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(snap): drop uinput plug denied by the Snap Store
The Snap Store rejected revisions 8-10 over the super-privileged uinput
plug (allow-installation), and reviewers denied the grant request on
forum.snapcraft.io/t/53528, pointing at the RemoteDesktop portal path
the app already prefers. Remove the plug and the uinput-only ydotool so
revisions pass automatic review again; Wayland typing continues through
the RemoteDesktop portal (already the default backend ahead of ydotool),
wtype, IBus Wayland on KDE, and clipboard paste.
KDE Plasma loses its correct-case ydotool path on the store snap (KWin's
portal mangles keysyms on some layouts, KDE bug 489021); the v0.18.x
GitHub .snap still declares uinput for KDE users who sideload it.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(text-injection): stop telling snap users to connect uinput
The Store snap no longer declares the plug, so the warning pointed at a
command that cannot work. The Snap hints now explain that only the
v0.18.x GitHub sideload declares uinput, and that native Wayland typing
uses the RemoteDesktop portal or IBus Wayland on KDE.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Jatin K Malik <jk.malik@yahoo.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Represent every distribution format in the README header shields:
AppImage, .deb, .rpm, Flatpak, Snap (edge), AUR, PyPI, and Nix flake,
grouped on their own row above the social badges. Also documents the
flake install path in the Install section.
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* chore(release): v0.18.2
* chore(release): add Flatpak release notes and refresh changelog date
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
Prepares the v0.18.1 patch release: version bump, changelog and docs sync, native-package and web metadata, and release notes crediting every reporter and PR author.
* feat(packaging): build thin .deb and .rpm with nfpm and attach them to releases
Adds packaging/native: an nfpm config that produces a .deb and an .rpm
from the release wheel. The packages stay thin - Python, GTK, PyGObject
and the typelibs are distro Depends; only pywhispercpp and pynput are
vendored because the first is unpackaged on both distros and the second
is missing on Fedora. The Debian 12 build image and the nfpm download
are pinned in tool_checksums.txt.
just native-packages builds both in the pinned container, and
native-smoke installs the built package per distro: debian:12 and
ubuntu:24.04 exercise the .deb, fedora:42 the .rpm, then each runs
vocalinux --version plus an import smoke of the app and the vendored
extension. unified-pipeline.yml gates on the same three-distro matrix,
and release.yml attaches both arches to the release and covers them in
SHA256SUMS and the provenance manifest.
Refs #600
* fix(packaging): preserve launcher args and document Wayland input group
The launcher joined $* into an unquoted exec, re-splitting file paths
that contain spaces before argparse saw them. Exec "$@" directly and
re-quote each argument for the inner shell of 'sg input -c'.
On Wayland the packages could not grant the 'input' group membership
evdev hotkeys need; name the usermod step and its caveat in INSTALL.md
and the release notes.
* test(packaging): add return types to the native packaging tests
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* chore(release): prepare v0.18.0
Bump version surfaces for the v0.18.0 minor release: version.py,
web/package.json + lockfile, softwareVersion schema, SECURITY.md
supported-versions table, AUR pkgver/_tag (source and -bin),
AppStream metainfo release entry, and the release-history table in
RELEASE_PROCESS.md.
Docs and site copy for the delta since v0.17.0: README blurb, snap
sideload versions in README/INSTALL/UPDATE, CHANGELOG current-stable
entry, docs/UPDATE.md What's New with the 0.18 highlights table and
per-PR credits, website changelog + desktop-reliability entries, and
PRODUCT.md capability lines for Dictation Pad, per-language shortcuts,
tray history, overlay, ducking, PipeWire, portal injection, D-Bus
activation, custom dictionary, and file transcription.
* fix(release): address review findings on the v0.18.0 prep
- Keep the checked-in vocalinux-bin PKGBUILD on v0.17.0 so its real
AppImage digests stay consistent with its sources; release.yml bumps
pkgver/_tag and pins both-arch digests atomically at publish
- Qualify the Flatpak release note: PipeWire system-audio capture and
RemoteDesktop portal injection are native-install paths
- Refresh dateModified on the changelog and desktop-reliability pages
- Restore the release-history table order in RELEASE_PROCESS.md
- Revert the build-regenerated web/next-env.d.ts route-types reference
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* feat(post-processing): pipe transcriptions through user-defined script
Add a configurable post-processing step that passes each transcription
result to an external executable via stdin and uses its stdout as the
text to type. Enables custom transforms (formatting, LLM rewriting, etc.)
without modifying Vocalinux. Falls back to original text on script
failure or timeout.
* test(post-processor): add unit tests and extract apply_post_processing
Move config-reading logic from main.py closure into apply_post_processing()
in post_processor.py so it can be tested independently. Add 11 unit tests
covering PostProcessor.process() and apply_post_processing().
* docs(post-processing): document post-processing script feature
* fix formatting of one file
* fix(post-processing): run the user script with a stripped host environment
Rebase compat: the host_process AST guard requires every subprocess
spawned for a host binary to pass env=host_env() so an AppImage's
LD_LIBRARY_PATH/GI_TYPELIB_PATH/PYTHONHOME do not leak in. Also give
apply_post_processing() a typed ConfigManager parameter and keep
text_callback_wrapper's local variable typed as str for mypy.
* fix(post-processing): preserve paragraph breaks and tighten script handling
Take script stdout verbatim instead of rstripping every trailing
newline, so the "new paragraph" voice command's break survives a
passthrough script. A single trailing newline is still dropped, but
only when the transcription had none — it is almost always just the
line terminator echo/print-style tools append.
Also stop writing transformed output to the log (it can carry
sensitive dictation into retained logs), catch only the expected
process/OS/encoding failures instead of every Exception, and add the
missing signature annotations.
* fix(main): run the post-processing script off the recognition thread
A user script may take seconds per segment, and text callbacks run on
the recognition thread that drains the bounded audio-segment queue;
stalling it drops queued dictation once the queue fills. Submit
post-processing and injection to a dedicated single-worker executor so
the backlog waits instead of losing speech, while keeping injection
order. On timeout or failure the worker falls back to the original
text, and an unexpected error is logged with a traceback and still
injects the original rather than silently dropping the segment.
Also pin the callback tests' get_str mock to no-script so they
exercise the synchronous path deterministically, and add a callback
test proving transformed text (including preserved paragraph breaks)
reaches the injector.
* docs(post-processing): annotate new callbacks and clarify stdout contract
Add the missing type annotations on the new Settings page callbacks
per the repository guide, and document that script stdout is used
verbatim (a lone trailing newline added by line-oriented tools is
dropped only when the transcription had none) and that scripts run on
a dedicated worker.
* test(main): annotate new post-processing callback tests
* fix(main): bind queued dictation to its target app and drain worker on quit
Route every transcription segment through the single post-processing
worker (a pass-through when no script is configured) so clearing the
script path mid-queue can no longer let a later segment overtake an
earlier one still running.
A segment that can sit in the backlog — a script is configured or a job
is still running — is bound to the application focused when it was
dictated; the worker drops it if focus has verifiably moved to another
application, so delayed dictation cannot land in the wrong app.
Wire the worker into the tray quit path via an on_quit hook: it clears
the acceptance flag, cancels pending jobs, and waits — bounded by the
script's own timeout — so nothing injects after the injector stops.
* fix(main): keep voice actions ordered and probe focus off the recognition thread
- Queue voice-command actions on the post-processing worker so an action
can no longer overtake dictated text still waiting to be injected.
- Move the submit-time focus probe onto a dedicated executor; the
recognition thread only submits the probe, and the job joins the result
at inject time, so compositor tools can no longer stall consumption of
the bounded audio-segment queue.
- Re-check the quit flag after the inject-time focus re-probe, then shut
both workers down without waiting on a running script so quitting does
not freeze on the script timeout.
* fix(main): capture submit-time focus on dedicated threads and serialize quit
- Replace the shared focus-probe executor with a fresh daemon thread per
submission so a slow earlier probe cannot delay a later one past the
focus change it is meant to capture; jobs read the result off a
one-slot queue at inject time.
- Give queued voice actions the same submit-time focus binding as text
segments so a queued shortcut is dropped instead of fired into a newly
focused application.
- Hold injection_lock across the accept-check plus inject/action call so
quit waits only for an in-flight injection instead of racing it.
- Queue the IDLE reset of last_injected_text behind pending worker jobs
so a queued 'delete that' still sees the text it targets.
* fix(main): keep actions instant on a free worker and bound quit wait
- An action now starts a submit-time focus probe only when it actually
queues behind other jobs; with a free worker there is no delay window
to bind against, so the probe's compositor calls are skipped and the
action stays immediate even when a script is configured.
- Probe and executor submission now happen inside pending_jobs_lock so
queue order matches callback order — a job that saw an empty queue can
no longer slip behind one that arrived while it was submitting.
- Every inject-path subprocess call (xdotool window/key/type, wtype and
ydotool type) is bounded by a timeout, and the quit hook's
injection_lock wait is capped, so quitting can no longer freeze on a
stalled injector subprocess.
* fix(main): bind every action to submit-time focus without slowing it down
- Every voice action now starts a submit-time focus probe: submission
never guarantees immediate execution, so skipping the binding left a
focus-change window in which a shortcut could fire into another app.
The worker waits on the probe for at most one second — milliseconds
on a healthy desktop — so immediate actions stay immediate while a
genuinely stalled probe falls back to the injector's own targeting.
- Quit waits for an in-flight injection without a cap again: every
inject-path subprocess is individually bounded, so the wait always
ends with the injection and a long transcription is never cut off.
- The wtype/ydotool typing timeout is now derived from the configured
per-key delay (a generous multiple), so a legitimately slow type can
never be cut mid-text; a timed-out type no longer falls through to
the clipboard fallback, which would duplicate the partial fragment.
* fix(main): drop unverified actions and make quit-time injection abortable
- An action whose submit-time focus probe cannot answer inside its
one-second bound is now dropped instead of running unverified: a
stalled probe can no longer leave a shortcut pointing at whatever
application happens to be focused.
- Quit asks the injector to abort in-flight injections, and injection
paths honour it: the xdotool chunk loop stops between chunks, the
wtype/ydotool type call runs as a polling Popen that can be killed,
and paste/shortcut/backspace entry points exit early. The quit hook
still waits for an in-flight injection, but the wait is bounded by at
most one already-running subprocess call — never the injection's full
length — and no subprocess is ever stopped mid-call by a timeout cap
on the wait itself.
- Annotate the remaining untyped test helpers.
* fix(main): wait out slow focus probes; restore run() in wayland typing
- An action no longer races its submit-time focus probe on a one-second
bound: the job waits for the probe's own answer, whose compositor
calls already carry per-call timeouts and short-circuit absent tools.
A slow but living probe delays the command instead of discarding it,
so a valid "undo" can no longer be lost to timing alone, and focus
verification still decides every drop.
- Wayland typing goes back to subprocess.run so tests keep intercepting
the same entry point. Abortability is preserved by typing in
200-character chunks with the shutdown check between calls — a
whole-string call could not be interrupted, but no single chunk holds
the injector for more than its own scaled budget.
- The abort flag gains a class-level default so injectors built without
__init__ in tests still answer the checks; each real instance gets its
own event in __init__.
- Update call-shape assertions for the typed-call timeout and finish
annotating the test methods touched by the callback refactor.
* fix(inject): never retype text a failed chunked call already sent
- _PartiallyTyped carries the confirmed-delivered prefix length out of
the chunked type loops, so every downstream fallback continues from
the untyped remainder: the Wayland-to-xdotool switch types only the
tail, and the clipboard fallback copies only the tail, never the
whole string a second time.
- The xdotool retry loop gains the same accounting — a retry now
resumes at the first untyped chunk instead of restarting at the top
and duplicating everything the failed attempt already delivered.
- Annotate the remaining test signature the repository guide requires.
* fix(inject): report partial typing as failure even when clipboard lands
A chunked type call that dies mid-text leaves only a prefix on screen,
and the remainder-fallback keeps it that way: it copies the tail for a
manual paste but now returns False, so the caller no longer records the
full transcription as injected and a following "delete that" cannot
erase text that was never typed.
* fix(inject): keep deletion state aligned with delivered text
inject_text now records how many characters a call confirmed on screen
as last_typed_count: the whole length on success, the typed prefix
after a partial failure, -1 when the delivered amount is unknowable.
The transcription caller uses it to keep last_injected_text honest —
a following "delete that" erases the typed prefix or nothing, never
the previous segment or text that never arrived.
* fix(inject): never replay input a timed-out portal job may have delivered
- RemoteDesktopPortalError.delivered=None marks an unreported job's count
as unknowable; all three portal fallbacks demote and skip instead of
replaying, and text injection surfaces as TimeoutExpired so no
full-text clipboard copy is offered
- _PartiallyTyped counts raised after a portal demote are rebased onto
the original text (typed_prefix) so the caller clips the real prefix
- evdev: a consumed pure-modifier press is withheld and replayed ahead
of the first non-target press, so holding RightAlt for AltGr
composition still reaches the application
- history: extend_latest gains an expected_latest CAS guard so a late
segment from an older worker merges only into its own session's
snippet, never into a newer session's entry
- quit path aborts in-flight injections before waiting on the lock so
quitting cannot freeze behind a long transcription's typing budget
- tests: stop _ensure_ydotoold leaking a real Popen in Wayland tests
* fix(shortcut): replay withheld modifiers across devices and after SYN_DROPPED
- Replay withheld modifier presses from every device, not only the fd the
character arrived on: split keyboards deliver AltGr and the character on
separate devices, and a per-fd check forwarded a plain character.
- Let the post-SYN_DROPPED resync run when only withheld modifiers exist
for the fd, so a press whose release the dropped burst ate is pruned
instead of replaying a stale modifier on the next ordinary key.
* fix(history,pads): keep late segments pad-bound deliveries and pad deletions correct
- Track the worker behind the ended session's snippet so late segments
from an older worker cannot merge into a newer session's entry
- Skip the focus-change drop for dictation-pad segments and pad-bound
editing commands, which never target the focused application
- Refuse 'delete that' when a manual pad edit blurred the segment
boundaries instead of deleting the stale recorded length
* style: drop unneeded nonlocal for mutated worker set
* fix(shortcut): share one evdev device layer across shortcut listeners
Each KeyboardShortcutManager built its own EvdevKeyboardBackend, and on
evdev every backend opened and grabbed the same physical keyboards: the
first grabber received all events and the rest received none, so
per-language shortcuts could never fire on Wayland.
All backends now bind to a process-wide EvdevDeviceHub that opens each
keyboard once, owns the grabs and the paired uinput clones, and fans
every event out to each registered engine. An event is re-emitted to
applications only when no engine consumed it, so suppression semantics
stay the same while per-engine matching state (combo progress, withheld
modifiers, swallowed presses) remains independent.
* fix(history): own late segments per worker and decide pad routing once
- ended_worker_snippets maps each ended session's worker to its own
(snippet, commit epoch): late deliveries merge into their session's
entry however many sessions have committed since, so one session's
stragglers never split across Recent Snippets entries.
- The leftover-segment commit reads the live history epoch inside the
session lock: a mid-session clear() no longer makes the stale ended
epoch refuse valid post-clear dictation.
- The pad-bound routing decision is captured once per job and passed to
the injector/action handler, so a toggle flip between the focus check
and delivery cannot reroute text or shortcuts into the focused app.
- Dead recognition workers are pruned from the tracking set and map so
long runs don't accumulate finished threads.
- The custom-terms switch now suppresses GTK's own state-set on save
failure so it shows the value that actually persisted.
* fix(shortcut): serialize evdev device layer teardown against restart
The last listener's unregister released the engine lock before joining
the monitor thread and closing devices, so a new listener registering in
that gap opened keyboards the stale teardown then closed — it stayed
active but dead, and briefly grabbed keyboards stopped forwarding input.
A lifecycle lock now covers the whole close/open transition in register,
unregister, and the monitor's unexpected-exit teardown, so a cold start
can only open devices after the previous generation finished closing
(also making the new generation's grabs succeed). A generation stamp
tells a monitor thread that outlived its join() its incarnation is over
even when running has gone True again.
* fix(injection): log focus-probe failures and narrow clipboard fallback catch
A crashed focus probe turned into an unknown result with no logged
error, indistinguishable from unavailable focus information; the
partial-typing clipboard fallback caught every exception. Log the probe
failure with context and narrow the clipboard catch to the exceptions
the fallback path can actually raise.
* fix(shortcut): join the evdev monitor outside the lifecycle lock
If the monitor exited unexpectedly while the last listener unregistered,
its cleanup queued on the lifecycle lock that unregister() held for the
join — stalling shutdown for the join's full 2s timeout. The close now
finishes inside the lock and the join waits after it is released, so the
monitor's own cleanup can run and exit instead of queueing behind it.
Closing before the join also dies the monitor's read/select on the
closed fds right away. A _closed gate on _open_keyboard_device keeps a
rescan from opening a device between detach and close that nothing
would ever close.
* fix(injection): narrow focus-probe catch to the raisable exception set
get_focused_window() reports Optional and never raises; the catch now
covers only what can realistically escape the queue write and logs it
with context, keeping the waiting job released.
* fix(shortcut): tag evdev device fds with the generation that opened them
A monitor that outlived join() could still touch a newer generation's
devices: the loop re-checked the generation only at the top, so mid-iteration
it could dispatch from, forward to, or remove devices a restarted listener
had just opened.
Every fd is now tagged with the generation that opened it, and the tag is
verified at each point a monitor could mutate shared state — fd lookup for
dispatch, clone forwarding, forwarder release, device removal, and rescan
opens — while dispatch re-checks the generation between events and the
per-fd loop breaks on it. Cold starts bump the generation before opening
so fresh devices are tagged for the new generation. Devices an older
generation opened stay tagged stale, so its reader leaves them alone.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* chore(release): add snap-promote workflow for gated stable releases
Issue #783 option 2 (safer): v* tags keep auto-publishing the snap to
edge + candidate in release.yml, and latest/stable is promoted by a
manual snap-promote.yml dispatch after candidate QA.
The dispatch resolves the revision on latest/candidate and refuses to
promote unless its version matches the tag, so a typo or stale candidate
fails the run instead of shipping the wrong revision. Declaring the
snap-stable environment with required reviewers adds an approval gate
on top of the dispatch.
Fixes#783
* fix(ci): serialize snap promotions and re-verify candidate
awk used the gawk-only \S which mawk reads literally, so the latest-track candidate row never matched. The promote step also trusted a revision resolved minutes earlier, letting an overlapping promotion downgrade stable. Serializes dispatches and re-resolves the candidate right before release.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* feat(flatpak): publish a signed self-hosted OSTree remote on releases (#785)
release.yml gains publish-flatpak-remote: it imports the per-arch
.flatpak bundles into an OSTree repo, signs commits and the summary
with a provisioned GPG key, regenerates static deltas and the
vocahq.flatpakrepo/index page, and pushes to VocaHQ/vocalinux-flatpak
served by GitHub Pages — the Homebrew-tap analogue, so installs
auto-update via flatpak update instead of per-release downloads.
The job is secret-gated like publish-aur (FLATPAK_GPG_PRIVATE_KEY +
FLATPAK_REPO_TOKEN) and skips itself while unconfigured. Prior repo
objects are pulled back in from gh-pages so clients get incremental
deltas. The manifest now declares branch: stable and the flatpakrepo
declares DefaultBranch=stable + RuntimeRepo for the Flathub GNOME
runtime.
Docs: new docs/FLATPAK_REMOTE.md (one-time setup, rotation,
verification); README, docs/INSTALL.md, packaging/flatpak/README.md
and the release notes now lead with the remote and keep release
bundles as the no-auto-update fallback. tests/test_release_integrity.py
pins the job's shape.
Closes#785
* fix(flatpak-remote): publish a .flatpakref and gate the remote to stable tags
- needs build-and-release so the remote cannot lead the GitHub Release
- stable tags only: prereleases must not reach `flatpak update`
- probe gh-pages before the empty-repo fallback: a transient checkout
failure no longer discards remote history
- publish com.vocalinux.Vocalinux.flatpakref (RuntimeRepo lives there, not
on .flatpakrepo) plus vocahq.gpg, so clean installs resolve the GNOME
runtime and key rotation works via remote-modify --gpg-import
- gate the release-notes remote instructions on the publish actually
running; the bundle path stays primary otherwise
- document rotation via remote-modify --gpg-import / remote-delete
--force and the explicit Flathub setup step
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* 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(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>
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(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(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.
* 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(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>
* 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>
* 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(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.
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.
* chore(build): migrate justfile tooling to uv
All recipes now run through 'uv run' against .venv, with a shared
DEV_EXTRAS variable (dev + vad) because uv syncs the environment
exactly and would otherwise uninstall the dev tools between
recipes. 'deps' installs dev+vad; new 'deps-all' covers every
extra. 'build' switches to 'uv build'; 'version' uses --no-sync.
Bump [tool.uv] required-version to >=0.12,<0.13 for uv 0.12.x, and
raise the mypy python_version target to 3.12: mypy 1.19 in the lock
cannot target 3.9 and numpy 2.x stubs need a 3.12 target to parse
(mypy is not run in CI or pre-commit; this only affects 'just
typecheck').
* fix(tests): isolate test runs from real XDG state; fix Arch model-path crash
Local test runs failed en masse (185 failures) while CI stayed
green: tests picked up the developer's real ~/.local/share/vocalinux
models via the default XDG paths and switched IBus/keyboard code
paths through the desktop session env (XDG_CURRENT_DESKTOP/
XDG_SESSION_TYPE on GNOME/Wayland, issue #478 branches). conftest
now points XDG base dirs at a throwaway session-wide directory and
clears session variables at import time, mirroring a fresh CI
runner (paths like MODELS_DIR freeze at first import during
collection, so fixture-time isolation is too late).
Also guard the Arch branch of _get_system_model_paths(): it removed
/usr/local/share/vocalinux/models unconditionally and crashed with
ValueError on Arch hosts when a custom XDG_DATA_DIRS never added
that path. Add regression tests, and make the lock-file test assert
the actual $XDG_DATA_HOME/vocalinux contract instead of the
hardcoded ~/.local/share default.
* fix(tests): keep the IBus engine double a plain class
Python 3.14's unittest.mock re-instantiates a user subclass when it builds
child mocks (klass(name=...)). VocalinuxEngine inherits from whatever
IBus.Engine resolves to, and the test module bound that to the MagicMock
class itself, so every engine instantiation raised
TypeError: VocalinuxEngine.__init__() got an unexpected keyword argument 'name'
on 3.14 while passing on 3.13. Replace it with a plain stand-in class that
still hands out per-instance MagicMocks for the IBus API surface the engine
touches.
The same pattern remains in test_ibus_engine.py, test_ibus_engine_utils.py,
test_ibus_active_engine_guard.py and test_ibus_xkb_wayland.py; those never
instantiate the engine, so they are unaffected for now.
* fix(install): build the venv from the system Python
install.sh looked up a bare `python3` from PATH. Started from an activated
virtualenv — a shell left in uv's .venv after `just deps` — it built venv/
from that interpreter, where distro PyGObject is invisible even with
--system-site-packages, and the run aborted with
Distro PyGObject (python3-gi / python3-gobject) is not importable in the venv.
A mismatched venv/ then survived every retry, because the installer reuses
whatever venv it finds.
- drop an inherited VIRTUAL_ENV (and its bin from PATH) before any
interpreter lookup
- select the interpreter explicitly: $SYSTEM_PYTHON (default
/usr/bin/python3) or PATH python3, preferring one that can import gi,
and create the venv with it instead of `python3`
- recreate venv/ when it was built by a different interpreter
- name the interpreter in the PyGObject guard's error
Documents the two environments (uv's .venv for tooling, venv/ for the
installed app) in CONTRIBUTING, the justfile header and AGENTS.md, and adds
the mismatch symptom to the INSTALL troubleshooting section.
* chore(python): raise the supported floor to 3.11
requires-python promised 3.9 while the CI matrix ran 3.9/3.10/3.11/3.13,
the classifiers advertised up to 3.14, mypy targeted 3.12 and install.sh
enforced 3.9 — five different answers to "which Python does this support".
Settle on 3.11 everywhere: pyproject (requires-python, classifiers, black
targets), install.sh's MIN_VERSION, check-system-deps.sh, the distro package
map, the CI matrix (now 3.11-3.14, matching the classifiers), the release and
nightly workflows, and the docs.
mypy now targets the floor instead of 3.12, so type checking covers what we
promise; numpy 2.x ships stubs in 3.12 syntax that a 3.11 target cannot
parse, so its imports are skipped. (mypy has 71 pre-existing errors either
way; no CI job gates on it.)
uv.lock and the requirements/* exports are regenerated — dropping 3.9/3.10
removes most of the version-conditional resolution branches.
tests/test_python_version_policy.py pins the invariant: install.sh's floor,
requires-python, the classifiers and the CI matrix must agree.
* ci: run lint and tests from uv.lock
The lock file and the hash-pinned exports were not enforced anywhere: CI
still built a venv by hand and installed unpinned `PyGObject` plus
`-e ".[dev,vad]"`, so the tool versions it checked against had nothing to do
with the ones `just lint` uses locally.
Both Python jobs now use astral-sh/setup-uv and run through `uv sync
--locked` / `uv run --locked`, which also fails the build when uv.lock drifts
from pyproject.toml.
Partially covers phase 5 of #701 (tests install from the lock, lock freshness
gate); trusted publishing, artifact checksums and action SHA pinning are
still open there.
* ci: lint from a dedicated dependency group
The lint job failed on this branch: `uv run --extra dev` syncs the whole
project, so it tried to build pyaudio from source on a runner that installs
no system dependencies.
src/pyaudio/device_api.c:9:10: fatal error: portaudio.h: No such file or directory
Move black/isort/flake8 out of the `dev` extra into a `lint` dependency
group. CI runs `uv run --locked --only-group lint`, which installs the three
pure-Python tools and skips the project entirely — no portaudio or
girepository headers needed. `just deps` and DEV_EXTRAS request the group
alongside the dev extras, so the local environment still carries test and
lint tooling at once and `just lint` matches CI version for version.
pre-commit is unaffected: its hooks pin their own tool versions upstream.
* fix(install): never configure an engine the venv cannot import
vosk became an optional extra in #705, but the installer still writes
`"engine": "vosk"` into config.json on the whisper.cpp fallback path even
when installing the extra only produced a warning. config.json also survives
reinstalls — every writer guards on "file does not exist" — so an engine
picked by an earlier attempt outlives the venv that supported it. Either way
Vocalinux dies at startup:
ModuleNotFoundError: No module named 'vosk'
- the fallback now writes the vosk config only when `import vosk` actually
succeeds, and otherwise leaves the engine configuration alone
- a new verify_configured_engine() runs near the end of the install: if
config.json selects vosk and the venv cannot import it, it says so and
prints both fixes instead of leaving a broken install behind
* feat(models): verify every model download against a pinned digest
Pins a sha256/md5 per downloadable speech model in
src/vocalinux/utils/model_checksums.txt and verifies against it in both
install.sh and the runtime downloaders, before the file is moved into place.
Verification fails closed. Covers phase 2.5 of #701; see the PR for details.
* docs(agents): rebase AGENTS.md onto the #727 rewrite
* fix(install): hash models that are already on disk instead of trusting them
* refactor(models): one download path per engine, and fix Whisper's
* docs(install): drop distros that cannot meet the 3.11 floor
* docs: retire the distros and Python versions the 3.11 floor rules out
* fix(install): abort instead of warning when Python is below the floor
* fix(install): fall back to the system Python, not the one on PATH
* fix(install): match an existing venv by installation, not by X.Y
* fix(install): repair a config naming an engine the venv cannot import
* fix(models): pin VOSK by the sha256 of bytes we fetched, not upstream's md5
* fix(justfile): request the dev set in every recipe that syncs
* test(version): scan the docs and website trees for a stale Python floor
* docs: correct the stale model-checksum and distro claims
* fix(models): stamp the VOSK tree the app extracts, as the installer does
* fix(install): replace an unverified VOSK model only once its successor is ready
* docs(install): align the Fedora floor with the rest of the tree
* test(install): drop an f-string with no placeholders
* test(models): stub the downloader probe the VOSK installer test relies on
* docs: drop the long dashes from two model-checksum comments
* fix(install): gate on the interpreter, not on the distro release label
* fix(justfile): export the lint group into requirements/dev.txt
* fix(models): unpack the VOSK tree into staging and stamp it before the swap
* fix(whisper): adopt a checkpoint an earlier release stored under the catalog name
* docs(settings): state what a stray Whisper copy really means for deletion
* fix(models): hash a whisper.cpp model found on disk before it reaches ctypes
* docs(contributing): stop telling readers to uninstall their linters
* fix(install): stop warning off distros the project documents as supported
* docs(distros): file the supported derivatives under supported
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.
Phases 0-1 of #701. Drops dead deps, moves vosk to an optional extra, adds uv.lock and hash-pinned requirements exports, pins pywhispercpp to 1.5.0.
Closes#705. Refs #701.
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.
* 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(shortcuts): default to hold Right Alt push-to-talk on first install
Double-tap Ctrl conflicts with many desktop and app shortcuts, so new
installs now use hold Right Alt (Option) in push-to-talk mode. Existing
config.json values are preserved unchanged.
Updates README, user guide, AGENTS.md, first-run dialog, website copy,
and tests to match the new defaults.
* refactor(shortcuts): trim redundant default-change test and comments
Drop the fresh-install assertion that duplicated DEFAULT_CONFIG coverage,
remove the duplicated Right-Alt rationale from config_manager, and drop a
vacuous source-string check from the settings shortcuts test.
* fix(shortcuts): align install.sh seeds and preserve legacy toggle mode
install.sh still wrote ctrl+ctrl without mode, so merges with the new
push_to_talk default became hold-Ctrl. Seed right_alt + push_to_talk for
new installs, and migrate existing shortcut-only configs to mode=toggle
so prior behavior is unchanged.
* style(config): fix black formatting after shortcuts migration
* fix(shortcuts): preserve legacy defaults when shortcuts section is missing
Existing config files without a shortcuts section previously inherited
ctrl+ctrl toggle from DEFAULT_CONFIG. Pin those historical defaults on
upgrade so the new first-install Right Alt push-to-talk defaults do not
silently change behavior.
* docs(agents): unwrap hard-broken dictation control paragraph
Keep the AGENTS.md paragraph as a single line like the surrounding prose.
* docs(web): refresh screenshots for v0.15 sidebar settings UI
Recapture product and settings gallery shots against the searchable
sidebar settings dialog, keeping the previous asset filenames and
exact pixel dimensions with wallpaper padding around each window.
* fix(web): correct screenshot semantics after adversarial review
Keep the existing transcription and tray images because the replacements did not
show their claimed flows. Populate the Logs dialog and unlock Advanced settings
so those refreshed images demonstrate the features described by the gallery.
* fix(web): retake screenshots as square crops with uniform padding
Resize settings dialogs to a near 1:1 window, capture with ~28px wallpaper
padding on all sides, open About from the tray menu, and skip redoing the
awkward XFCE system-tray asset.
* docs(web): retake v0.15 screenshots on GNOME (light/dark)
Window-only captures from Ubuntu/GNOME at 960x720 for settings and
logs, with matching dark variants under screenshots/dark/. Replace the
old Shortcuts shot with Performance to match the sidebar.
* feat(web): theme-aware light/dark screenshot switching
Serve paired GNOME captures and swap them with the site theme via
dark:hidden / hidden dark:block. Replace Shortcuts gallery entry with
Performance to match the v0.15 sidebar.
* docs(web): refresh screenshots after About moves into Settings
Rebase onto main brought the in-app update checker: About is now a
sidebar settings page. Recapture light/dark GNOME panels at 960x720 and
update gallery copy/metadata accordingly.
* docs: use single screenshot source for README and website
Point README images at web/public/screenshots so product shots live in
one place, drop the unused resources/screenshots tree (including the old
demo video and about-updates set), and replace the missing shortcuts
shot with performance.
* docs(web): refresh light/dark screenshots and product grid
Replace the screenshot set with fresh captures for both themes, and
lay out the three Product shots in a single equal-height row.
* fix(web): align screenshot metadata, sizes, and theme loading
Restore dictation copy for the 00-transcription asset, set width/height
from the recaptured PNGs, preload only the light next/image twin when
priority is set, and eager-load both theme variants by default so dark
mode does not flash empty on first toggle.
* fix(web): show full settings screenshot without crop
Use object-contain so the homepage settings dialog fits the frame
instead of object-cover clipping the sidebar.
* feat(settings): add in-app update checker with stable/nightly channels
Move About into the Settings sidebar and check GitHub Releases for updates.
Stable uses /releases/latest; Nightly follows nightly-YYYY-MM-DD tags.
Remove the standalone About dialog and wire tray About to Settings.
Fixes#183
* docs(readme): update About screenshot caption for Settings page
* fix(settings): address update-checker review and About polish
Discard stale in-flight results when the update channel changes, page
through GitHub releases for nightly tags, restore the About logo next
to the title, and replace the em dash in the app description.
* docs(screenshots): add About update-checker state captures
Host PR review screenshots on the branch so GitHub can render them
from raw.githubusercontent.com (Cursor artifact URLs 403 in PR bodies).
* fix(updates): recognize .devYYYYMMDD nightlies and clear Open style
Parse CI nightly versions like 0.14.2.dev20260802+sha when checking for
updates, and drop the suggested-action style from Open after a failed
GitHub lookup.