Add build-xbps tag job to release.yml: verifies the tag against
src/vocalinux/version.py, fails closed when XBPS_SIGNING_KEY is unset,
builds and signs via podman-only repo.sh, and attaches packages,
signatures, repodata, and client key files to the release. Fold the
xbps-repo artifact into publish-checksums (manifest + SHA256SUMS).
Document the Void install in the root README (glibc x86_64 scope,
fingerprint check, third-party repo caveat) and the post-tag template
version/checksum bump as a pull request in docs/RELEASE_PROCESS.md.
Proof: local podman run with a throwaway RSA4096 key built all four
templates, signed packages + repodata, and verified voculinux installs
from the signed repo in a clean rootdir (fingerprint
eb:29:5d:9e:5c:94:c1:63:ae:35:cd:35:db:7a:28:da).
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
* fix(snap): build and verify payload from locked python inputs
* docs(snap): document locked inputs and artifact verification
* test(clipboard): isolate restore workers between tests
* test(packaging): tighten isolation and clarify snap audit guarantees
* fix(ci): run snap recipe tests on snap-only changes
A change to snap/snapcraft.yaml or snap/local/smoke-test.sh only matched
the snap filter, so the Python tests that guard the recipe (including the
portable whisper.cpp flags from #957) and the smoke script were skipped.
Match all of snap/ in the python filter, as scripts/ already is.
* docs(snap): use a version placeholder in the smoke-test example
---------
Co-authored-by: Jatin <jatinkrmalik@gmail.com>
* fix(web): SEO fixes from Search Console and Lighthouse audit
Technical:
- Emit BreadcrumbList JSON-LD from the same trail as the visual breadcrumbs
on every SeoSubpageShell page.
- absoluteUrl() no longer appends a trailing slash to file paths. The
Organization and publisher logo URLs pointed at /vocalinux.png/, a 404.
- Fetch the GitHub star count once at build time in next.config.js and
drop the client-side api.github.com fetch and preconnect (Lighthouse
"third-party code" on every page). Falls back to the "GitHub" label.
- Fix h1 -> h3 heading skips on 11 pages by promoting the leading card
headings to h2; new .subpage-card-title keeps the old h3 sizing.
- Header and footer logo get alt="" next to the visible "Vocalinux" text.
- Footer links every guide, distro and feature page, so the pages added in
#1038 and /vs-nerd-dictation/ (Discovered, not indexed) and /writers/
(no inbound links) are linked sitewide.
Titles and descriptions:
- Rewrite titles/descriptions for pages with high impressions and low CTR
in Search Console (home, FAQ, compare, languages, open-source, GPU,
alternatives, offline, install) to lead with the queries they rank for.
- Distro pages use "<Distro> Speech to Text: Offline Voice Typing Setup"
as the search title; the on-page h1 is unchanged.
- Remove brand repetition and shorten 75+ character titles.
- Bump sitemap lastmod for the rewritten pages.
* fix(web): keep home install card titles styled
The home page's install cards still use h3; only /install/ moved to h2.
Style both so the home cards keep their size and spacing.
* fix(aur): gate source publication on attested sdists
Prepare and verify the canonical release source, build the exact PKGBUILD on Arch, and publish the gated artifact without further rewrites. Part of #701.
* fix(aur): isolate local gate and binary publication
Keep local gate distributions and diagnostics in a private workspace, preserving other build outputs and failed diagnostics. Publish vocalinux-bin after its own AppImage inputs without depending on the source gate or unrelated release jobs. Add regression coverage for both review findings.
* chore(deps): allow uv versions below 0.14
* fix(ci): align uv version and generated exports
* fix(dev): check Fedora build headers and AppImage GI bundle
Fail early with distro-specific native dependency hints before dev sync. Verify the AppImage's PyGObject installation and Python path after linuxdeploy, and report isolated GI import failures with actionable path diagnostics. Guard the CI uv pin and cover both preflights with tests.
* feat(build): add locked reproducible wheel gate
Build independently in clean source trees and virtualenvs, compare wheel bytes, and stage only verified canonical distributions. Safely clean recognized prior outputs on repeat runs.
Part of #701
* ci(release): verify wheel reproducibility before publishing
Run the shared gate in PR and tag workflows, preserve failure diagnostics, and keep verification artifacts out of release checksums. Make Snap Store publication wait for the gate and reuse the canonical distributions for PyPI.
Part of #701
* docs(release): document reproducible wheel validation
Describe the locked toolchain, canonical publication boundary, same-platform scope, diagnostic outputs, and safe automatic cleanup.
* chore(scripts): update executable permissions
* fix(build): isolate release tooling and preserve cleanup retries
Exclude host site-packages from build-only virtualenvs so unlocked setuptools plugins cannot affect verified distributions. Delete staged artifacts before canonical ownership evidence to recover safely from interrupted cleanup.
Add regression tests and document the build-only environment exception.
* fix(release): make reproducible builds retryable and refresh tools
Regenerate release build exports from uv.lock. Hold a checkout lock through cleanup and staging, and atomically rename complete archives into dist. Cover interrupted staging, competing processes, and lock release with regression tests.
---------
Co-authored-by: Kanishk Pachauri <itskanishkp.py@gmail.com>
Co-authored-by: sesav <1636488+sesav@users.noreply.github.com>
* fix(perf): reduce startup and inference overhead
* fix(perf): report effective whisper.cpp fallback settings
* fix(injection): add opt-in X11 paste for long dictations
* perf(vad): lower default silence timeout to 1.2s
* fix(types): resolve mypy errors across application source
* ci: run mypy in the lint job
* ci: include mypy in lint dependency group
* fix(types): narrow evdev timestamp comparison
* refactor(recognition): add SpeechEngine protocol and engine dispatch table
* refactor(settings): move About section and engine tables out of settings_dialog
* docs: add ARCHITECTURE.md and refresh docs index and AGENTS layout
* fix(settings): preserve GTK dialog compatibility after extraction
* style: format touched Python modules
* fix(types): keep local typecheck from syncing dependencies
* test(vad): align main startup expectation with new default
* style: sort engine registry and protocol test imports
* test(settings): follow extracted About builder attribute alias
* test(settings): isolate GTK dialog base in About test
* test(settings): isolate About color scheme build test
* test(settings): restore GTK base after About scheme test
* test(settings): isolate About scheme check from GTK mocks
* test(settings): support extracted dialog mixin methods
* docs: add before/after performance benchmark charts
* fix(injection): type instead of pasting when the clipboard holds non-text data
* fix(release): recreate refs/remotes dropped by git on the Flatpak remote publish
The first real remote publish (v0.18.3) failed at
'flatpak build-update-repo --generate-static-deltas' with
'opendir(refs/remotes): No such file or directory'. ostree init creates
refs/remotes empty, git does not track empty dirs, and the site/repo
checked back out of gh-pages therefore lacks it — while delta generation
opens that dir unconditionally. mkdir -p it (and refs/mirrors) after the
copy. Also name the Snap Store review-queue blockage in its publish
error, so the fix (reject the stale upload on the dashboard, re-run)
surfaces on the job page.
* test(release): pin the refs/remotes mkdir before build-update-repo
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
The credential gate silently skipped the remote publish when
FLATPAK_GPG_PRIVATE_KEY / FLATPAK_REPO_TOKEN were unset, so the
documented auto-update remote could stay unpublished for releases at a
time (#951). Emit an Actions warning annotation and a run-summary entry
instead of a plain log line, and note in docs/FLATPAK_REMOTE.md that any
token with Contents write on the remote repo works.
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-16 09:20:44 -07:00
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>
* fix(ci): attach snap with --repo, keep checksums on attach failure
The Attach Snap job had no checkout and no --repo flag, so
gh release upload failed with 'not a git repository' on v0.18.0.
Every other release upload passes --repo; do the same here.
Publish Snap to Store matched 'will need manual review', but current
snapcraft prints 'human review required', so the expected
allow-installation review hold reported as a hard failure.
A failed attach-snap also skipped Checksums and Provenance even though
it hashes build artifacts, not release state. Run it unless cancelled
and guard that all expected artifacts exist before manifesting.
* fix(ci): keep literal review phrase in snap publish grep
test_release_attaches_snap_then_tries_the_store asserts the literal
'will need manual review' stays in the publish-snap block. Keep it as
the first alternation so the existing test documents the matcher.
* fix(ci): require both native package sets in checksum guard
Merged main added build-native and build-native-arm64 (.deb/.rpm per
arch) to publish-checksums' needs and the SHA256SUMS glob. The artifact
guard now requires two .deb and two .rpm files so a failed native build
cannot publish an incomplete manifest.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* ci(install): gate the remote install path end to end
A curl|bash user takes a different path than the local install gate
covers: bootstrap outside a checkout, tag selection, clone/fetch of the
selected tag, then handoff to the tagged installer with the original
arguments. Add scripts/install-remote-test.sh, which publishes HEAD as a
tag in a local mirror, runs install.sh against it twice (fresh clone,
then fetch+reset of the existing clone), and fails closed when the
clone's HEAD is not the selected tag commit. install.sh gains a
VOCALINUX_REPO_URL override so the mirror can stand in for the public
remote. Wire a single-distro job into the distro matrix and a just
remote-install-gate recipe.
Refs #701
* ci(install): publish the gate tag as an orphan from HEAD's tree
CI checks out with fetch-depth 1, and git refuses to push or fetch a
shallow boundary commit into a ref. Build the tag on an orphan commit
created from HEAD's tree instead, and make tree equality the fail-closed
check that installer, modules and exports are this commit's files.
* test(install): force the re-run arm to prove fetch and reset
Review feedback on the remote-install gate: the second run reused a clone
already on the tag, so a broken fetch or reset would still pass. Move the
clone onto a descendant commit and delete its local tag ref before the
re-run, so only a real fetch + reset restores them. Also list the new
recipe in AGENTS.md's command guide.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* 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>
* fix(appimage): reuse the Vulkan pywhispercpp build across CI runs
The x86_64 Build AppImage step spends most of its time recompiling
pywhispercpp with Vulkan. That compile depends on the base image,
tool_checksums.txt, and the pywhispercpp pin, not on the commit.
Cache the native tree under one key shared by CI, Nightly, and Release.
* fix(appimage): reject a partial Vulkan cache and key the build tools
A failed copy could leave a tree that still contained libggml-vulkan.so,
and the next run would reuse it. Check each copy, and keep the previous
cache until the new one is in place.
The cache id now also covers the pinned cmake and ninja export and the
Vulkan compile flags. The hit path is what the build calls, and a test
runs it with a stub compile command.
* chore(ci): rerun AppImage against the saved Vulkan cache
The review fix changed the native cache id, so run 37586979294
compiled pywhispercpp again and saved
appimage-native-X64-ebf478b87986e2f6b9d80c97c2f5869fe3120c2bcd48f5d8f5a65fe06ec03015.
This commit leaves the base image, tool checksums, pywhispercpp pin,
installer tool pins, and Vulkan compile flags alone.
* feat(dictionary): combined custom dictionary with corrections
Rebased onto latest main (conflict-resolution squash for PR #818).
* fix(dictionary): stabilize the terms and corrections file contracts
- corrections_path() stays at the fixed config_dir() location promised by the file contract instead of following a custom or --dictionary-file terms path, which could strand or duplicate corrections; a leftover file beside the terms file is still read once and copied over.
- _atomic_write() preserves the replaced file's permission mode instead of resetting it to the process umask.
- build_initial_prompt() streams terms and stops at the configured cap instead of reading and processing the whole file for every segment.
* fix(settings): restore the selected dictionary pane after a search
Searching can auto-switch the Custom Dictionary management pane to reveal a match, but clearing the query only restored widget visibility. Record the pane selection in the search baseline snapshot and restore it alongside the previous page so users return to the pane they chose.
* fix(dictionary): close the remaining custom dictionary file-contract gaps
- A --dictionary-file session override no longer imports a corrections
file found beside the override into the standard config directory;
the override scopes to terms only.
- Terms reads validate the whole file before yielding: corrupt UTF-8
mid-file now yields no terms at all, matching the invalid status
Settings reports instead of leaking a partial dictionary to
recognition.
- The terms read is bounded by MAX_TERMS_FILE_BYTES (1 MiB); a larger
file is invalid everywhere with a dedicated status instead of being
read on every segment.
* fix(dictionary): keep usable terms prefix on oversized files and legacy corrections under overrides
* fix(dictionary): cap yielded terms and keep added terms visible on oversized files
* fix(dictionary): keep added terms visible without dropping scanner-owned lines
* fix(dictionary): keep oversized terms manageable and honor mid-search pane switches
- terms list gains a Show all / Show first 500 toggle; adding a term
beyond the cap auto-expands so the new row renders with a Remove
button instead of being invisible until the file is hand-edited
- a manual dictionary pane switch during an active search drops the
saved pre-search pane so clearing the query no longer rolls back the
user's newer selection
- annotate the new settings tests and type model_info so the parakeet
branch stops adding fresh mypy errors
* fix(settings): do not auto-expand terms on add past display cap
Rebuilding up to thousands of term rows on the UI thread stalls the
dialog; the overflow button already shows the true count, so the added
term stays one click away.
* test(dictionary): stub segment_callbacks on bare manager for history dispatch
* fix(dictionary): stream oversized terms files when building the prompt
build_initial_prompt decoded the whole 1 MiB read window before every
transcription segment even though it only needs the first max_words
terms. _iter_terms_for_prompt now reads oversized files line by line and
stops at the prompt limit, keeping the same contract: complete leading
lines only, invalid UTF-8 inside the usable window yields nothing.
* fix(history): only the ended session's worker may extend its snippet
A late segment could extend the newest history entry regardless of which
session produced it, so a worker finishing after the next dictation
session leaked its text into that session's entry. Track the worker that
owned the ended session and require late segments to arrive on the same
thread before merging; anything else forms its own snippet.
* fix(speech): transcribe queued segments the reload worker never drained
The recognition worker can exit during stop_recognition's bounded
audio-thread join, so the released key's final buffer lands in the queue
after the worker is gone and was silently dropped when the reload
teardown replaced the queue. Drain whatever remains before swapping it
— except on capture failure or an explicit cancel, which still discard
the session's leftovers.
* fix(settings): build term rows in idle slices and pin hidden additions
Two problems shared one cause. Rebuilding up to 4096 rows synchronously
stalled the dialog, and a term appended past the 500-row cap reported
success while staying invisible. Rows now build in bounded GLib idle
slices guarded by a refresh token, and a saved term outside the visible
window is pinned to the top of the capped list until it disappears or
becomes visible on its own. Adding under "show all" scrolls to the new
term instead.
* test(engine): annotate new test method return types
* fix(ci): match a candidate row that leads the latest track block
snapcraft status normally prints candidate as a continuation line under
the latest track (channel in column 1). If candidate ever leads the
track block it rides the track line instead (channel in column 3), and
the resolver missed it and refused to promote. Match both layouts in
the resolve and the pre-release re-verify.
* fix(dictionary): bound prompt reads and scroll to landed terms
- _iter_terms_for_prompt now reads each line via readline() capped by the
remaining byte window, so an overlong line can never pull more than
MAX_TERMS_FILE_BYTES into memory
- the prompt path keeps decoding every complete line inside the usable
window after the term limit is collected, so invalid UTF-8 anywhere in
the window still yields no terms and matches the reader's invalid status
- under "show all" Settings scrolls to the index where an added term
landed (prepended or appended), verified against the row's label,
instead of always scrolling to the end
* fix(history,reload): keep late-segment ownership and drain stamped tuples
- Track session_workers_seen so a worker from an earlier session can
never merge into a newer snippet while a session is open
- Unpack (segment, language) queue tuples in _transcribe_queued_segments;
passing the tuple as audio raised TypeError and lost final speech
- Refuse 'delete that' when a manual pad edit blurred segment boundaries
- Translate term-row coordinates into the scrolled card's space so the
scroll actually reaches rows below the fold
- Type _on_dictionary_pane_changed's varargs
* style: drop unneeded nonlocal for mutated worker set
---------
Co-authored-by: Graham <graham@thegeeky.ninja>
* feat(aur): add vocalinux-bin PKGBUILD shipping the AppImage
Closes#817
* fix(aur): hard-dep injection tools, pin real digests, gate the -bin package
* fix(aur): pin both-arch -bin digests and build the binpkg as builder
updpkgsums in the publish action only regenerates the x86_64 sum array, leaving aarch64 stale after every tag; release.yml now pins both AppImage digests itself and publishes with updpkgsums off. The AUR gate also created $WORKDIR/binpkg as root after handing WORKDIR to builder, so makepkg could not write there. AUR.md now matches the published -bin package.
* fix(ci): wait for aarch64 AppImage before publishing vocalinux-bin
publish-aur only needed build-and-release, so on a tag the digest-pinning
step could curl the aarch64 AppImage before build-appimage-arm64 attached
it — a 404 that failed the job between the source and -bin publishes.
Split the -bin bump/pin/publish steps into publish-aur-bin, which needs
[publish-aur, build-appimage-arm64]: the source package still ships as
early as before while the binary package only runs once both AppImages
exist on the release.
* fix(ci): ship -bin local sources and wait for the aarch64 asset on AUR publish
Split the -bin bump/pin/publish into publish-aur-bin needing
[publish-aur, build-appimage-arm64] so the digest-pinning step can no
longer curl a 404 for the not-yet-attached aarch64 AppImage mid-release,
and publish with asset_dir so the PKGBUILD's local sources
(vocalinux.desktop, vocalinux.svg, LICENSE) reach the AUR repo instead
of a lone PKGBUILD users cannot build.
* style(tests): apply black formatting to aur packaging test
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* 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>
* 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>
* 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
* 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>
The builder action restore-keys is `flatpak-builder-${arch}`. Our key
was `flatpak-builder-${hash}`, which the action stores as
`flatpak-builder-${hash}-x86_64`. That does not start with
`flatpak-builder-x86_64`, so a packaging hash change (a lock refresh,
or merging one) discarded the whole .flatpak-builder cache and
recompiled numpy and pywhispercpp from source, ~18 min instead of ~4.
Put ${{ matrix.arch }} in the key first so the fallback matches. Then
flatpak-builder skips unchanged modules and only rebuilds what the
hash change actually touched.
PRs no longer watch src/**. python-test already covers those, and the
expensive compile does not. push to main still does, so a sandbox
install break fails the default branch.
* fix(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>
* feat(ci): verify the published release, not just the workflow
* fix(ci): filter attestations by predicate, name a missing distribution
Two findings from review on #791.
The attestations query accepted any predicate, so an SBOM would have read as
build provenance while `gh attestation verify`, the command the notes hand
users, rejected the artifact. It now sends the same
`predicate_type=https://slsa.dev/provenance/v1`. Checked on a real digest: the
SLSA filter returns the bundle, a foreign predicate returns 404.
check_pypi compared whatever distributions were left, so a release that lost its
sdist passed on the wheel alone. Each expected suffix is now named when absent.
The lazy urllib import moved past the early return, where it belonged.
The other two findings do not hold. `workflow_run.head_branch` carries the tag
for a tag-triggered run: the last four release runs report v0.16.2, v0.16.1,
v0.16.0 and v0.15.0. And `host_env()` is for the app spawning host binaries out
of an AppImage; `scripts/` is in no `packages` entry and the AppImage build
never references it, so this cannot run from a bundle.
* fix(ci): read release digests from the REST API
gh release view --json assets omits digest on gh 2.46, which is what
many distros ship and what just verify-release often runs, so every
asset failed as undigested. Pull the release through gh api instead,
treat a null body as notes FAIL, and prefer removeprefix for the tag.
* fix(ci): fail verify when PyPI has an extra distribution
Matching release digests alone left a differently named wheel or
sdist on PyPI unchecked. Reject those extras too, same suffixes.
* fix(test): clear tempfile mock before urllib import
Other suites leave a MagicMock in sys.modules['tempfile'], which
breaks urllib.request import with a metaclass conflict in the extra
PyPI distribution check. Drop the stub (and stale urllib modules)
first, and assert the full problem list.
* fix(ci): restore real tempfile before urllib in verify test
The MagicMock tempfile left by recognition_manager still broke
urllib.error on CI. Put the real module back and drop cached urllib
packages before the import.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* 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
* feat(ci): gate AUR PKGBUILD builds on every PR
Nothing ever ran makepkg on the PKGBUILD before a tag pushed it to AUR
users, and the channel broke twice that way (#736, #757) — a PKGBUILD
change did not even start a CI job, because no paths filter matched
packaging/aur/**.
The gate (packaging/aur/build-test.sh, run in archlinux:latest — the
AUR is a rolling channel, so the image stays deliberately unpinned):
- swaps the tag-archive source= for a git archive of the checkout under
the same filename and prefix, so build()/package() run unchanged and
the gate answers "does this commit build on Arch"
- installs depends/makedepends from the repos; python-pywhispercpp
(virtual, provided by the AUR backends, #579) and python-pynput (AUR)
are satisfied by a stub so makepkg still verifies the full depends
set, and pip-installed from the hash-pinned requirements/runtime.txt
for the import smoke
- runs namcap on the PKGBUILD and the built package, failing on errors
only (its warnings are noise around lazy imports and virtual depends)
- installs the package and imports the whole UI/recognition stack, plus
the vocalinux --version entry point
Part of #701 (phase 6).
* fix(ci): stop partial-upgrading in the AUR gate and correct the #736 attribution
* chore(release): build once, and publish sha256sums and provenance (epic #701, phase 5)
* chore(ci): pin every third-party action by commit sha (epic #701, phase 5)
* test(ci): anchor the dependabot guard on the ecosystem it must cover
* fix(nightly): stamp the version before building instead of renaming the wheel
* test(nightly): require the known build jobs rather than an exact count
* build(appimage): pin the interpreter, packages and tooling
* ci(appimage): build inside a pinned ubuntu 22.04 image
* test(appimage): guard the pins and the base image
* docs: state the AppImage glibc floor
* fix(appimage): bundle libibus with its typelib
* ci(appimage): let the container run the isolated GI smoke test
* fix(appimage): keep the bundle's libraries out of host binaries
* fix(appimage): bundle libgpg-error with the libgcrypt that needs it
* docs(agents): require host_env() for spawned binaries
* ci: run the AppImage jobs when its other inputs change
* fix(appimage): check every bundled typelib, not just the seeded ones
* fix(ibus): keep the bundle when respawning our own interpreter
* fix(injection): pass host_env() to setxkbmap and notify-send
* test(ibus): wait for the IBus warmup thread before asserting
* fix: leftover engine repair, just --no-sync, and ggml verify from #713#713 only rewrote a broken config.json toward whisper_cpp, so a reinstall
that kept the old engine still died when vosk was the extra that installed.
verify_configured_engine now prefers SELECTED_ENGINE, then any working extra,
and the vosk fallback rewrites an existing file.
just recipes use uv run --no-sync so they do not prune extras from
just deps-all. CONTRIBUTING lists libgirepository-2.0-dev for Ubuntu 24.04.
An unverified ggml file is not loaded if unlink fails, and an unpinned name
is refused rather than deleted.
* fix(appimage): retry tooling downloads on curl reset
* fix(build): bootstrap .venv for --no-sync just recipes and cover the contributor path in CI
---------
Co-authored-by: Sergey Silaev <hello@8am.dev>
* 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.
Stacked bottom of #701. Replaces Makefile with justfile and hardens install.sh (strict mode, logging, HTTP probes, curl -f on model downloads, no v0.10.1-beta fallback).
Closes#700.
* fix(whisper): use bundled GPU libs and skip software Vulkan devices
AppImage 0.15.0 shipped a CPU-only pywhispercpp wheel, so the Settings
GPU picker and host vulkaninfo never moved inference off the CPU. Rebuild
pywhispercpp with Vulkan in the AppImage, log the libraries that actually
load, skip llvmpipe in auto-select, and document how to verify.
Fixes#604
* fix(appimage): pin gcc/g++ for Vulkan pywhispercpp rebuild
Hosts that default cc/c++ to clang fail CMAKE's C++ test because clang
does not search gcc's libstdc++ path. Default the rebuild to gcc/g++.
Repo links, install one-liners, and release checks now use
github.com/VocaHQ/vocalinux. The site mentions VocaHQ without a redesign.
The Codeberg mirror, personal email, and X handle are unchanged.