* 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.
* fix(appimage): ship transitive GI typelibs for non-Debian hosts
The 0.15.0 AppImage pruned girepository down to a partial allowlist and
dropped xlib/Dbusmenu/GModule. On Ubuntu the missing typelibs fell back
to /usr/lib/x86_64-linux-gnu/girepository-1.0; on openSUSE/Fedora that
path does not exist, so Gtk failed to import and startup reported
missing GTK3/AppIndicator (#585).
Keep linuxdeploy's typelib extras, seed the full transitive set, bundle
AppIndicator/Notify shared libs, and smoke-test GI imports with host
typelibs hidden.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* style(test): format appimage packaging regression test
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(ci): avoid libappindicator/ayatana apt conflict
Ubuntu packages libappindicator3-1 and libayatana-appindicator3-1
conflict. Rely on gir1.2-* to pull a single implementation and keep
libdbusmenu/libnotify explicit for bundling.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(ci): install gir1.2-dbusmenu-glib for AppImage typelib seed
Dbusmenu-0.4 is a required transitive typelib for AppIndicator; without
the GIR package the build fails the required-typelib check.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(appimage): require only one AppIndicator typelib variant
Runtime accepts AppIndicator3 or AyatanaAppIndicator3. Treat them as
alternates in the AppImage typelib check so single-stack build hosts
do not fail require_all.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(appimage): prefer Ayatana after rebase onto main
Align AppImage typelib seeding and GI smoke order with main's
Ayatana-first tray preference from #621/#638.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(appimage): accept lowercase AyatanaAppindicator3 typelib
Match tray_indicator.py's three-step fallback so AppImage builds and
GI smoke succeed on hosts that only ship AyatanaAppindicator3-0.1.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(main): accept lowercase AyatanaAppindicator3 in dep check
Align check_dependencies with tray_indicator.py and the AppImage GI
smoke path so a lowercase-only typelib host does not fail at startup.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
git push --tags fails when a tag already exists on Codeberg (e.g.
v0.15.0 from a prior sync or retag). GitHub is the source of truth for
this read-only mirror, so force-push tags to keep them aligned.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
Fix ESLint errors for hash install links, anonymous default-export
warnings, and the ineffective next.config headers() that warned under
static export. Correct softwareVersion sync to match unquoted JS keys,
and bump Actions to Node-24 runtimes so deprecation notices stop.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(ui): AppImage smoke fixes for tray icons, labels, and identity
Replace placeholder mic tray SVGs with opaque branded icons, show
whisper.cpp instead of Whisper_cpp, set GLib/AppIndicator identity to
Vocalinux (not main.py), and give About dialog buttons enough padding
so labels are not clipped under Adwaita.
* fix(packaging): harden AppImage build for Wayland and tray deps
Pin the build interpreter, install onnxruntime into the bundle, set
desktop Exec=AppRun + StartupWMClass, prune typelibs to an allowlist
after linuxdeploy, and stop forcing GDK_BACKEND=x11 on Wayland
(VOCALINUX_FORCE_X11=1 remains as an escape hatch). CI AppImage jobs
also pull Ayatana AppIndicator and librsvg SVG loader packages.
* fix(packaging): bundle full stdlib from base_prefix in AppImage
When build.sh runs under a venv, sys.prefix only has site-packages, so
the AppImage was missing encodings and crashed on start. Copy the
interpreter and stdlib from sys.base_prefix, force pip
--ignore-installed into AppDir, set Gdk program class to Vocalinux, and
tone down the muted tray slash so small StatusNotifier icons stay slate.
* fix(tray): use absolute icon paths so stale hicolor icons lose
XFCE StatusNotifier resolves IconName through the system theme and can
ignore IconThemePath. A leftover ~/.local/share/icons placeholder
(vocalinux-microphone-off.svg filled #e74c3c) was winning over the
AppImage's bundled icons. Outside Flatpak, pass absolute SVG paths to
set_icon_full / new_with_path instead of bare theme names.
* docs(mirror): add Codeberg mirror setup and optional sync workflow
Document one-time Codeberg migration (pull mirrors are disabled on
Codeberg.org) plus an optional GitHub Actions push for ongoing sync.
Links the expected mirror URL from README and CONTRIBUTING.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* docs(mirror): drop maintainer setup doc from PR
Keep only the README mirror table and optional sync workflow.
Setup notes are one-off and do not belong in the repo.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* chore(ci): remove obsolete mirror setup comment
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* feat(packaging): add self-contained AppImage build
Bundles the wheel plus PyGObject/GTK3/AppIndicator/IBus typelibs into a
relocatable x86_64 AppImage via linuxdeploy + linuxdeploy-plugin-gtk, so
users get a zero-install, no-sandbox-negotiation alternative alongside
AUR/PyPI. Wired into nightly and tagged releases, plus a smoke-test job
in the PR pipeline since AppImage/ELF bundling can't be verified on a
macOS dev machine.
* feat(packaging): add aarch64 AppImage builds
Parameterizes build.sh on uname -m instead of hardcoding x86_64 (both
linuxdeploy and appimagetool publish aarch64 continuous builds, and
pywhispercpp ships manylinux_aarch64 wheels, so no from-source compile
needed). Adds an arch matrix to the PR smoke-test job, and a second
job in nightly.yml/release.yml that builds on GitHub's free
ubuntu-24.04-arm hosted runner and attaches the aarch64 AppImage to
the same release the x86_64 job already created.
GitHub rejects job-level if expressions that reference matrix
(Unrecognized named-value: matrix). Resolve the include list in a
small setup job instead so PRs stay x86_64-only.
Using github.sha in the cache key forced a full rebuild on every commit.
Hash packaging/flatpak instead so .flatpak-builder can restore across
runs, and only rebuild modules that actually changed.
PRs build x86_64 only; main and workflow_dispatch still cover both arches.
Drop the extra upload-artifact step in favor of the action's own upload.
* feat(flatpak): add Flatpak packaging for universal distribution
Adds a Flathub-ready Flatpak for the whisper.cpp engine, plus the minimal
runtime adaptations Flatpak actually needs — deliberately scoped down from the
earlier attempts (#416, #428), which bundled unrelated whisper.cpp GPU, audio,
and clipboard changes.
Packaging (packaging/flatpak/):
- app manifest, AppStream metainfo, and desktop entry (com.vocalinux.Vocalinux)
- generated offline Python dependency modules (flatpak-pip-generator)
- bundled shared modules: PortAudio, libayatana-appindicator, xdotool, xsel
Runtime changes (Flatpak-only, behaviour unchanged outside a sandbox):
- utils/paths.py: honour XDG_CONFIG_HOME/XDG_DATA_HOME so config, models and
runtime data live under ~/.var/app/<app-id> in the sandbox
- autostart uses `flatpak run <app-id>` (the host launcher is always on PATH)
- text injection and keyboard backend detect a Wayland host reached via XWayland
(no Wayland socket granted) and route through xdotool / pynput-on-X11
- tray indicator references the exported `<app-id>-*` host icon theme names
Also: a CI workflow building x86_64 + aarch64 bundles, and README / packaging
README / distro-compatibility doc updates.
Verified with a full flatpak-builder build + bundle install on aarch64:
pywhispercpp loads its native libraries without LD_LIBRARY_PATH, XDG paths
redirect correctly, bundled tools are present, and the entrypoint runs.
Manifest and builddir lint are clean and AppStream metadata validates.
Closes#167
* chore(flatpak): sync metainfo version to 0.14.0-beta
Also ignore flatpak-build-dir/ so local builder output stays untracked.
* refactor(flatpak): trim docs and comments after ponytail review
Drop redundant packaging README command blocks, shrink path/tray/autostart
comments, and stop ignoring a non-canonical build-dir name.
* fix(flatpak): close readiness gaps for v0.14.0-beta
Route log files through XDG data dirs so Flatpak keeps them under
~/.var/app, refresh AppStream screenshots to vocalinux.com, and use
the SPDX GPL-3.0-only license id.
* fix(flatpak): enable global hotkeys via evdev and --device=input
pynput over XWayland only receives keys for the focused X client. Prefer
evdev inside Flatpak and grant /dev/input so shortcuts work app-wide.
* fix(flatpak): inject text with ydotool/uinput on Wayland
xdotool only reaches XWayland clients, so typing appeared to succeed but
never hit native apps. Ship ydotool, grant --device=all for uinput, start
ydotoold on demand, and type via uinput inside the Flatpak.
* fix(flatpak): paste via wl-copy + Ctrl+V instead of typing
ydotool type injects character-by-character. Ship wl-clipboard, grant
--socket=wayland, and prefer clipboard paste for instant insertion.
* test(text-injection): expect ydotool default key-delay of 2
Matches the shipped VOCALINUX_YDOTOOL_KEY_DELAY default used when
clipboard paste falls back to character typing.
* feat(packaging): add AUR release package and CI publish
Ship packaging/aur/vocalinux for yay installs of tagged releases, wire
AUR publish into the release workflow (skipped until secrets are set),
and document one-time maintainer setup.
Fixes#116
* refactor(packaging): slim AUR PR to PKGBUILD + CI
Drop systemd unit, install script, website edits, and long docs.
Keep release PKGBUILD, minimal publish job, and short AUR setup notes.
Configure actions/labeler@v5 to automatically label PRs based on file paths.
Maps changes to: app, web, documentation, ci, tests, installer, resources, dependencies.
Creates 6 new labels to complement existing issue labels for better PR organization.