Commit Graph
67 Commits
Author SHA1 Message Date
xavierk 4a0a245578 test(void): tie the stock pre-install to the constrained depends
CI / security (pull_request) Successful in 1m10s
CI / lint (pull_request) Successful in 25s
CI / ai-review (pull_request) Successful in 8s
The version-constrained carried packages are named in three places: the
voculinux depends, the stock pre-install line and the pkgver loop of the
install smoke. Nothing required them to agree, so a third gap package
could be added to one place and missed in the others. The new test
requires all three to name the same set.

Also corrects the README "Version floors" wording: the offline
stock-version test only records the versions verified on 2026-11-06,
and the install smoke's pkgver check is the live signal.

Refs #16
2026-11-06 21:45:04 +05:30
xavierk 06b7b79664 fix(void): carry python3-pysocks and constrain the audio and SOCKS floors
CI / security (pull_request) Successful in 1m13s
CI / lint (pull_request) Successful in 23s
CI / ai-review (pull_request) Successful in 8s
Void's stock python3-PyAudio (0.2.12_5) and python3-pysocks (1.7.0_8) sit
below the project floors (pyaudio>=0.2.13, PySocks>=1.7.1). voculinux
depended on both by bare name, which an installed stock package already
satisfies, so a user who had the stock packages was never upgraded.

- add packaging/void/srcpkgs/python3-pysocks: PySocks 1.7.1 from the PyPI
  sdist that uv.lock pins (sha256 3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0)
- voculinux depends on python3-PyAudio>=0.2.14_1 and python3-pysocks>=1.7.1_1
- the install smoke installs the stock packages first, then voculinux, and
  fails unless each reads back as its carried pkgver
- tests: each constraint must name its carried template's current version
  and revision; the stock version must sit below the floor; the depends
  parser refuses malformed tokens instead of stripping them
- docs: "Version floors" in packaging/void/README.md; a pointer in
  docs/RELEASE_PROCESS.md

Refs #16
2026-11-06 21:32:33 +05:30
xavierk b47a65c91e fix(void): build the xbps release from the tag, not an attached sdist (#37)
CI / security (push) Successful in 1m15s
CI / lint (push) Successful in 23s
CI / ai-review (push) Skipped
Every release is built from its tag alone by packaging/void/build-release.sh, shared by the tag workflow and local dry runs. Signs in a network-less container with the key mounted read-only and publishes to the tag release and the rolling latest release.
2026-11-06 15:31:47 +00:00
xavierk 180cd4d512 fix(spec16): close the review findings for the Void port (#34)
CI / security (push) Successful in 1m14s
CI / lint (push) Successful in 24s
CI / ai-review (push) Skipped
Model recommendation keyed to the installed build and free memory, fork-sourced Void templates, signed xbps release workflow on Gitea with the pinned repository key, niri injection guard and wtype-first order, tightened naming sweep, web copy fixes, and CI security and duplication fixes.
2026-11-06 14:13:49 +00:00
soubarnak e6c9069e0b fix(void): use concrete release-download URL for xbps client registration
Replace the self-referential <repo-url-from-release-notes> placeholder
in the root README, the release-notes template, and the Void packaging
README with the concrete per-tag download prefix the build-xbps job
attaches the signed repo to. Static docs default TAG to the current
version; release.yml renders github.ref_name at publish time.

Fixes #29
2026-11-06 09:42:12 +05:30
soubarnak 9bc04936b6 feat(void): add signed xbps repository pipeline
Build every template in smoke-gate order, index the full repository and
sign packages plus metadata with the RSA4096 key, in the pinned Void
container via podman. Fail-closed without a key: no key file, no repo.

Ticket #29 (needs #27, #28).
2026-11-06 06:29:22 +05:30
soubarnak bd7b624f50 feat(void): add package smoke gate across all templates
Ticket xavierk/voculinux#28, needs #27. One documented command
(packaging/void/smoke.sh, just void-smoke) lints, builds, installs
and smoke-tests every template under packaging/void/srcpkgs in
dependency order in the pinned Void container via podman, failing
fast naming the package with the harness echoing each stage.

The gate caught three latent breaks in the per-package harness and
the app template, fixed here:
- container-main.sh scoped the libggml scan to python3-pywhispercpp
  and gave the leaf packages their own import smokes (pynput by
  presence: no display in the container), failing loudly for
  packages without a smoke recipe;
- voculinux template drops the deprecated wrksrc override (xbps-src
  renames the single-top-dir archive itself; xlint rejects wrksrc)
  and builds the PEP 517 wheel (the sdist ships no setup.py);
- post_install promotes the pre-rename sdist names (desktop entry
  content, icons, /usr/bin launchers) to the renamed identity, and
  the payload smoke rejects pre-rename leftovers in OS-visible paths.

Offline policy tests (tests/test_void_smoke_gate.py) pin the gate to
the template set, the app-last order, podman-only pinned inputs and
per-package failure identity; the #27 policy tests and the rename
sweep cover the harness and template contracts.
2026-11-06 05:57:37 +05:30
soubarnak d7a4b8c21e feat(void): add xbps template for the voculinux app
Ticket xavierk/voculinux#27, built on #18 (rename) plus the #25/#26
dependency templates. The template builds the app from the canonical
checksum-verified release sdist, depends on Void's interpreter, GTK,
desktop bindings and packaged Python libraries, and installs the
renamed desktop entry, icons and a packaged-install marker. No speech
models, no vendored wheels, no pip at build or install time.

Packaged installs keep background update checks as a signal, but every
update affordance presents the package-manager path
(src/vocalinux/utils/packaging.py). Also corrects the Void distro
inventory (Ayatana provider name; ydotool marked absent) and regens
the installer map.
2026-11-06 03:03:33 +05:30
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
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
2026-11-06 01:29:39 +05:30
soubarnak d4be3caf7a Merge feature/26 into integration/spec16 (ticket #26: void pywhispercpp template)
Conflict in packaging/void/tool_checksums.txt (add/add with #25):
kept #26 rows (base-image digest + void-packages pin the kept
build.sh reads) with the #25 header reference folded in. #25's
base-image digest differed; flagged for re-pin on next bump.
2026-11-06 00:06:02 +05:30
soubarnak 1d8b07727e feat(void): add xbps template for python3-pywhispercpp
Spec #16 ticket #26: Void template for the whisper.cpp Python binding,
built from the pinned 1.5.0 sdist with the Vulkan backend (default-on
option), shared libraries, and portable CPU flags. Includes the podman
build/smoke harness, pinned base-image pins, and offline policy tests.

Refs: #26
2026-11-06 00:04:47 +05:30
soubarnak c462ffb4b2 feat(void): add xbps templates for pynput and updated PyAudio
Ticket: #25

Two in-repo void-packages-layout templates under packaging/void/srcpkgs:
python3-pynput 1.8.2 (new; Void does not ship it) and python3-PyAudio
0.2.14 (updated local template; Void ships 0.2.12, below the project's
pyaudio>=0.2.13 floor). Versions and checksums track uv.lock and
requirements/runtime.txt; pynput post_patch strips setup_requires so the
build never fetches helpers over the network (setuptools-lint and twine
are not in void-packages). No noverifypydeps/nopyprovides anywhere.

Adds tests/test_void_small_deps.py (offline policy test): template
layout and metadata shape, lock/export/floor pins, checksum pins,
no verification-disabling options, linux dep coverage, verified-only
dependency names, and the digest-pinned base image in
packaging/void/tool_checksums.txt.

Verified: xlint clean on both templates; both packages build with
xbps-src in the pinned ghcr.io/void-linux/void-glibc-full container
(podman) and install in a fresh container with pyaudio 0.2.14
importing against PortAudio V19.6.0.
2026-11-05 23:09:16 +05:30
Jatin K MalikandGitHub 0a4d637f5f fix(overlay): detect Wayland by GType so the fallback overlay stays compact (#1030) (#1035)
* fix(overlay): detect Wayland by GType so the fallback overlay stays compact (#1030)

_is_wayland() checked isinstance(display, Gdk.WaylandDisplay), but GTK3's
Gdk typelib has no WaylandDisplay class, so it always returned False. On
Wayland without gtk-layer-shell, _layout_bottom_strip() therefore sized the
prewarmed fallback window to the full monitor width (the X11 strip) instead
of the compact centered window meant for Wayland, which on KDE showed up as
a wide empty "Vocalinux Dictation" window listed in the taskbar.

Drop _is_wayland() and use _display_is_wayland(), which checks the GType
name and already gates prewarm, so both decisions agree.

Also list gtk-layer-shell as an optdepend of the source AUR package: with
it KWin and wlroots compositors get a layer surface that is never listed
in the taskbar and never needs the prewarmed toplevel. The AppImage (and
so vocalinux-bin) already bundles it.

* docs(overlay): describe the compact Wayland fallback in the overlay docstrings

The class docstring still said the overlay is always a full-width strip, which no longer holds on Wayland without layer shell (#1030).

* fix(overlay): keep the full-width strip on GNOME Wayland

Mutter ignores CENTER_ALWAYS for a plain Wayland toplevel, so the compact
fallback window landed in the top-left corner on GNOME (tested on GNOME
Shell 46 with the Ubuntu Dock). The full-width strip at least keeps the
orb horizontally centered there. Use the compact window only on KDE,
where KWin listed the strip in the taskbar as a wide empty window (#1030).
2026-11-02 19:17:27 -08:00
dbf8039196 chore(release): v0.18.5 (#1027)
* chore(release): v0.18.5

- Update version from 0.18.4 to 0.18.5
- Add v0.18.5 sections to CHANGELOG, README, docs/UPDATE.md, metainfo.xml and the website changelog
- Bump website package metadata and softwareVersion schema
- Add the 0.18.5 row to the RELEASE_PROCESS version history

* docs(release): qualify the clipboard-history claim for wl-copy 2.2+

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-26 21:40:44 -07:00
Sergey SilaevandGitHub 5687363a4a fix(aur): gate source publication on attested sdists (#1020)
* 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.
2026-10-26 20:26:04 -07:00
Jatin K MalikandGitHub 750d64dc9b fix(tray): prefer libayatana-appindicator-glib when available (#1019)
Use the maintained AyatanaAppIndicatorGlib 2.0 indicator when it is installed, mirroring the existing Gtk.Menu into a Gio.Menu whose actions keep stable, never-reused names and fire the original items. Fall back to the GTK 3 indicators otherwise; the AppImage stays on its bundled GTK 3 indicator, and the .deb recommends the glib typelib.

Fixes #1013
2026-10-25 14:49:34 -07:00
Sergey SilaevandGitHub b59797f1af chore(dev): widen uv range and harden Fedora/AppImage builds (#952)
* 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.
2026-10-23 22:16:16 -07:00
Kanishk PachauriandGitHub b148bca5fa fix(deps): bump vulnerable dependencies flagged by Dependabot (#1008) 2026-10-22 12:24:29 +05:30
62b0d157c4 chore(release): v0.18.4 (#1007)
- Update version from 0.18.3 to 0.18.4
- Add v0.18.4 sections to CHANGELOG, README, docs/UPDATE.md, metainfo.xml and the website changelog
- Bump website package metadata and softwareVersion schema

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-21 01:49:03 -07:00
1909f45fdf fix(flatpak): bundle onnxruntime for Silero VAD (#1002)
* fix(flatpak): bundle onnxruntime for Silero VAD

Bundle the [vad] extra in the Flatpak and select the matching CPython wheel for each architecture.

Fixes #969

* docs(flatpak): keep the runtime.txt history in the sync script docstring

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-21 00:27:05 -07:00
Jatin K MalikandGitHub d59395b5c9 fix(hotkeys): use the X11 listener for Flatpak on native X11 sessions (#997)
* fix(hotkeys): use the X11 listener for Flatpak on native X11 sessions

Use evdev for Flatpak Wayland/XWayland while preserving the GNOME X11 fallback.

Fixes #971

* docs(flatpak): note that GNOME X11 still needs /dev/input for hotkeys
2026-10-20 23:41:07 -07:00
2da20f791a chore(release): v0.18.3 (#964)
* chore(release): v0.18.3

- Update version from 0.18.2 to 0.18.3
- Add v0.18.3 sections to CHANGELOG, docs/UPDATE.md, metainfo.xml and website changelog
- Bump website package metadata and softwareVersion schema
- Clarify that the uinput-carrying sideload snap is the v0.18.2 asset

* docs(release): include the transcribe modal-grab fix (#965) in v0.18.3 notes

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-16 20:24:22 -07:00
ed0ed8ba49 fix(packaging): build pywhispercpp at the portable x86-64 baseline (#959)
* fix(packaging): build pywhispercpp at the portable x86-64 baseline

Every packaged channel shipped a libggml-cpu compiled for AVX2+FMA:
the manylinux PyPI wheel itself carries -mavx2 -mfma -mbmi2 code, so
any package that bundles it dies with SIGILL when whisper.cpp loads a
model on a pre-Haswell CPU (the reporter's i5-3337U). GGML_NATIVE=OFF
alone is not the portable baseline either; while the native default
is on, ggml turns every GGML_* SIMD option on, so each is pinned off.

- Snap: force the sdist (PIP_NO_BINARY) with the flag floor via
  CMAKE_ARGS in the part's build-environment.
- Flatpak: extend the module's CMAKE_ARGS with the same floor.
- Native .deb/.rpm: the build still vendors the wheels, then rebuilds
  libggml-cpu alone from the same hash-pinned sdist and swaps it into
  the vendored copies, asserting no AVX/FMA survives; the container
  image gains cmake, g++, make and binutils.
- AppImage: extract the pinned-sdist reinstall into a shared helper
  and route every CPU-only fallback of the Vulkan path through a new
  baseline rebuild at the same floor; the Vulkan compile keeps its
  own flags via the same helper. Keeping the wheel on those paths was
  the bug's exact binary.

Fixes #957

* style(tests): apply black/isort to the new packaging guards

* fix(native): keep the wheel's libggml providers, alias its base/gomp sonames

Swapping the whole libggml set for the baseline build broke the import
smoke: the wheel's auditwheel-mangled core lib doubles as the
extension's __cxa_call_terminate provider, and its NEEDED entries are
the mangled names. Swap only libggml-cpu, and satisfy the baseline
library's own unmangled NEEDEDs (libggml-base.so.0, libgomp.so.1) with
aliases of the vendored copies, which carry no generated SIMD and are
the same upstream version. Fail closed when the vendored set has
restructured instead of silently shipping the AVX2 wheel.

* fix(packaging): fail builds that lose the speech engine, unblock the ISA assert

Greptile findings on the baseline work:

- A failed baseline rebuild in the AppImage returned success after
  remove_appdir_pywhispercpp, so the bundle would ship with no speech
  engine at all while logging that the wheel was kept. Propagate the
  failure through every CPU-only fallback so the build stops instead.
- The native libggml-cpu assertion used grep -q under pipefail, where
  grep's early exit SIGPIPEs objdump and the pipeline reports a miss,
  letting an AVX2 library through. Read the full disassembly instead.

* fix(installer): build pywhispercpp from sdist on the CPU path

install_cpu_pywhispercpp let pip pick the manylinux wheel, whose
libggml-cpu is compiled for AVX2+FMA and SIGILLs on pre-Haswell CPUs
(#957). Pass --no-binary pywhispercpp like the Vulkan/CUDA paths so the
sdist compiles against this machine's own instruction set.

* fix(installer): install C++ toolchain on distros that ship without one

Forcing the pywhispercpp sdist on the CPU path needs a compiler, and the
fedora, mageia, alpine, void and solus package maps did not request one.
Add gcc-c++/make (rpm), build-base (alpine), base-devel (void) and
gcc/g++/make (solus) so source installs compile on minimal systems.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-16 09:24:15 -07:00
0faac6b3b8 chore(release): v0.18.2 (#943)
* chore(release): v0.18.2

* chore(release): add Flatpak release notes and refresh changelog date

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-13 00:00:20 -07:00
DepfrostandGitHub f10eb64fa0 fix(nix): enable Vulkan GPU inference for pywhispercpp (#942)
Nice work, Vulkan acceleration on Nix has been a long time coming.
2026-10-11 23:07:31 -07:00
devin-ai-integration[bot]andGitHub 4beb58c585 fix(packaging): build bundled pywhispercpp without -march=native (#939)
Good catch, this is what was crashing the AppImage and Flatpak on older CPUs.
2026-10-11 23:07:16 -07:00
c3c390f19d feat(nix): add flake package, NixOS and Home Manager modules (#936)
* feat(nix): add flake package, NixOS and Home Manager modules

Add a flake.nix plus packaging/nix derivations so Nix and NixOS users can
run and install Vocalinux declaratively, including via Home Manager.

- package.nix: buildPythonApplication with GTK3, libayatana-appindicator,
  IBus and libnotify typelibs; wraps the entry point with every host tool
  the injectors probe for (xdotool, wtype, ydotool, wl-clipboard, xclip,
  xsel, xprop, setxkbmap, ibus, gdbus, notify-send, paplay, aplay)
- pywhispercpp.nix: builds pywhispercpp from its vendored PyPI sdist
  (absent from nixpkgs, same gap as Debian/Fedora), static whisper.cpp,
  GGML_NATIVE=OFF for a portable baseline
- module.nix: NixOS module enabling hardware.uinput and installing the
  package; input/uinput group membership stays a per-user choice
- home-manager.nix: programs.vocalinux.enable installs into the profile
- docs/INSTALL.md gains a Nix / NixOS section; packaging/nix/README.md
  carries the usage snippets

Refs #931

* fix(nix): address review findings on imports check and docs

- pywhispercpp: check pywhispercpp.model and _pywhispercpp too, so the
  compiled binding is proven loadable, not just the package
- README Home Manager example: declare the nixpkgs/home-manager inputs
  and pass pkgs, which homeManagerConfiguration requires
- package.nix: use top-level xprop/setxkbmap attrs (xorg.* is deprecated)

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-10 20:40:25 -07:00
devin-ai-integration[bot]andGitHub f418a11482 chore(release): prepare v0.18.1 (#926)
Prepares the v0.18.1 patch release: version bump, changelog and docs sync, native-package and web metadata, and release notes crediting every reporter and PR author.
2026-10-08 00:17:58 -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>
9eba80b53d fix(packaging): declare python3-pysocks in deb and rpm deps (#920)
* fix(packaging): declare python3-pysocks in deb and rpm deps

#909 added PySocks as a runtime dependency for SOCKS5 proxy support but
only covered the AUR PKGBUILD — the nfpm deb and rpm packages never
declared python3-pysocks, so
test_every_project_dependency_is_covered_in_both_formats fails on main
and on every PR's merge check. Add it to both depends lists and the
test's DEB/RPM_PROVIDES maps.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(packaging): deb name is python3-socks

Debian/Ubuntu package PySocks as python3-socks (1.7.1+dfsg); the rpm
name stays python3-pysocks — Fedora's python3-socks is a different
library (python-socks). Verified in debian:12, ubuntu:24.04 and
fedora:42 containers.

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>
2026-10-07 17:45:06 -07:00
775863fd30 feat(settings): add proxy support for model downloads and update checks (#909)
* feat(settings): add proxy support for model downloads and update checks

Add a Proxy page to Settings (above About) with Off / System / Manual
modes. Manual mode supports SOCKS5 and HTTPS (HTTP CONNECT) proxies
with optional username/password auth; the password is stored in
config.json in plaintext like the other settings. System mode honors
the *_proxy environment variables and falls back to the GNOME
org.gnome.system.proxy manual configuration.

The resolved proxy is applied to every in-app outbound path that
fetches models or checks updates: the shared requests download path in
recognition_manager (_stream_model_download, used by the whisper.cpp,
whisper, Vosk, Parakeet and faster-whisper downloaders) and the
release checks in update_checker. A small Test Connection button on
the Proxy page verifies reachability through the configured proxy.

SOCKS5 needs PySocks, added as a runtime dependency; uv.lock, the
requirements/*.txt exports and the Flatpak dependency manifest are
regenerated by just lock (with a fix in sync_flatpak_deps.py for
packages like PySocks that publish more than one universal wheel).

Fixes #655

* fix(proxy): bracket IPv6 hosts, escape test errors, declare pysocks for AUR

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-07 17:16:48 -07:00
4b7d57c077 feat(packaging): build thin .deb and .rpm and attach them to releases (#913)
* 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>
2026-10-07 17:10:00 -07:00
Jatin K MalikandGitHub d53300841d fix(appimage): reuse the Vulkan pywhispercpp build across CI runs (#903)
* 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.
2026-10-07 01:04:18 -07:00
1d4dbe33cc chore(release): prepare v0.18.0 (#895)
* chore(release): prepare v0.18.0

Bump version surfaces for the v0.18.0 minor release: version.py,
web/package.json + lockfile, softwareVersion schema, SECURITY.md
supported-versions table, AUR pkgver/_tag (source and -bin),
AppStream metainfo release entry, and the release-history table in
RELEASE_PROCESS.md.

Docs and site copy for the delta since v0.17.0: README blurb, snap
sideload versions in README/INSTALL/UPDATE, CHANGELOG current-stable
entry, docs/UPDATE.md What's New with the 0.18 highlights table and
per-PR credits, website changelog + desktop-reliability entries, and
PRODUCT.md capability lines for Dictation Pad, per-language shortcuts,
tray history, overlay, ducking, PipeWire, portal injection, D-Bus
activation, custom dictionary, and file transcription.

* fix(release): address review findings on the v0.18.0 prep

- Keep the checked-in vocalinux-bin PKGBUILD on v0.17.0 so its real
  AppImage digests stay consistent with its sources; release.yml bumps
  pkgver/_tag and pins both-arch digests atomically at publish
- Qualify the Flatpak release note: PipeWire system-audio capture and
  RemoteDesktop portal injection are native-install paths
- Refresh dateModified on the changelog and desktop-reliability pages
- Restore the release-history table order in RELEASE_PROCESS.md
- Revert the build-regenerated web/next-env.d.ts route-types reference

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-06 14:03:21 -07:00
b4babcaeca feat(packaging): AUR -bin package publishing with per-arch digests (#879)
* 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>
2026-09-30 00:27:28 -07:00
f36c3f6ae5 feat(text_injection): add RemoteDesktop portal injection backend (#885)
* feat(text_injection): add RemoteDesktop portal injection backend

Wayland offers exactly one sanctioned key-injection path:
org.freedesktop.portal.RemoteDesktop's NotifyKeyboardKeysym. It reaches
native clients on compositors with no input-method-v2 bridge, works from
inside the Flatpak sandbox without /dev/uinput or a helper daemon, and
delivers keysyms so non-US layouts survive -- unlike ydotool's evdev
keycodes.

RemoteDesktopPortal is a synchronous client over a dedicated worker
thread holding a private bus connection and GLib context, which keeps
Request::Response delivery deterministic. Sessions are created lazily on
first use so startup probing only reads the interface version; v2 portal
sessions persist a restore token (mode 0600 under data_dir) so the
permission prompt appears once.

Backend selection now prefers the portal on Wayland: it is probed at
runtime ahead of ydotool/wtype/xdotool in autodetection, is pinnable via
VOCALINUX_FORCE_BACKEND=portal or text_injection.backend, and demotes to
the previous order on runtime failure. Flatpak still needs --device=all
for evdev hotkeys, but uinput is only a fallback path now.

Closes #750

* fix(text_injection): resolve Greptile findings on RemoteDesktop portal injection

- Move persist_mode/restore_token to SelectDevices options (portal v2
  sends them there, not CreateSession); a bad token is ignored rather
  than sinking the session, so drop the stale-token retry paths that
  could issue a second Start after a user cancel
- Track delivered units on RemoteDesktopPortalError so Wayland
  fallbacks retry only the undelivered tail instead of retyping the
  prefix (text chars, backspace taps, shortcut steps)
- Give queued portal jobs a single deadline budget across
  CreateSession/SelectDevices/Start waits; on submit timeout the job
  is cancelled via a Gio.Cancellable plus abort checkpoints so the
  worker cannot keep emitting keys after the caller gave up
- Close sessions whose Start never succeeded
- Release held shortcut modifiers on mid-step failure
- Skip the portal probe when an installed wtype/ydotool backend is
  explicitly pinned
- Type annotations on the new signatures and touched tests

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-29 23:55:27 -07:00
c9ea6c7de7 feat(flatpak): publish a signed self-hosted OSTree remote on releases (#875)
* 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>
2026-09-29 22:45:49 -07:00
Jatin K MalikandGitHub fb9166ed9e feat(settings): run local VocaGateway from Advanced (podman-first) (#774)
* feat(settings): run local VocaGateway from Speech Engine (podman-first)

Add an optional Local VocaGateway panel next to Remote Server so Linux users
can start the pinned v0.1.0 compose project with podman (docker fallback),
watch Live/Pairable/Ready status, pair a phone without loopback QR URLs, and
preset remote_api to the gateway transcriptions endpoint. Stop preserves
volumes; Flatpak fails closed; tray offers Stop only when this session manages
the gateway. Default on-device engine stays whisper.cpp.

* fix(gateway-embed): drop nonexistent compose cpu profile

VocaGateway v0.1.0 exposes default service gateway plus native/cuda/vulkan
profiles, not cpu. Start and stop the default service without --profile, keep
the latest image pin rejection, and prefer save_config for the remote_api preset.

* fix(gateway-embed): keep compose --profile cpu per embed contract

v0.1.0 has no cpu profile entry, but Compose still starts the unprofiled
gateway service when --profile cpu is set, and keeps cuda/vulkan/native off.
That matches the frozen desktop embed contract and stays forward-compatible.

* fix(gateway-embed): harden embed path for settings and compose

Keep podman/docker probing off the GTK thread, reject link-local and
loopback public URLs for phone QR, sanitize compose .env image overrides,
and cap bootstrap token reads so a hostile file cannot blow memory.

* fix(gateway-embed): keep LAN toggle and phone QR in sync

When Allow LAN flips while compose is already managed, rewrite publish
host and PUBLIC_URL and recreate the gateway before Pairable comes back.
Also reject default docker/podman bridge addresses for phone QR.

* test(gateway-embed): pin bridge and LAN-gate pairing cases

Cover 172.17.0.2 / link-local not becoming Pairable, and the mid-run
LAN toggle path that must wait for compose republish before advertising
a phone URL.

* style(gateway-embed): black format embed modules

* fix(gateway-embed): reject short-form loopback and localhost.* for QR

Align Pairable/QR host checks with vocagateway desktop-embed policy so
127.1 / localhost.* never become phone-reachable display URLs.

* fix(gateway-embed): detach Settings status listener on destroy

Gateway poll callbacks were left registered after Settings closed, so
idle_add could touch destroyed GTK widgets while compose stayed up.

* fix(gateway-embed): pass host_env to bridge ip probe

_bridge_iface_ips shells out to host ip; without host_env the AST
gate fails and AppImage bundles can hand the wrong GLib to ip.

* fix(packaging): ship gateway_embed in the AppImage

The explicit setuptools packages list omitted the new package, so the
bundle booted from source trees but died with ModuleNotFoundError on
every distro boot test.

* style(packaging): black the gateway_embed package guard

* fix(settings): pack local VocaGateway into the Advanced island

The rebase onto main auto-merged textually onto the simple Speech Model
page. Keep the gateway card on advanced_box, next to Remote Server.

* fix(gateway-embed): treat missing Compose as unavailable

When podman or docker is present but Compose is not, stop advertising Run.
Return empty compose_args, show an install-Compose hint, and cover it with a unit test.

Also add module loggers on the new gateway_embed modules and annotate the
gateway Settings/tray callbacks Greptile flagged. just lock found no lockfile drift.

* fix(gateway-embed): keep Allow LAN bind matched to the switch

Snapshot the LAN flag for start/republish, record what compose actually
got, then republish again if the switch moved mid-flight so 0.0.0.0 never
lingers while Allow LAN reads off.

* fix(gateway-embed): detect leftover compose after Quit

Quit does not stop compose, so a LAN-published gateway can keep running.
Settings and the tray treated that as Stopped because managed_by_us is
only process memory. Probe compose after runtime detection, show Stop
when it is actually up, and let Stop work even if this session did not
start it.

* fix(gateway-embed): restore LAN switch from leftover compose

Quit leaves compose running, so the next launch now reads
VOCAGATEWAY_PUBLISH_HOST from the embed .env, adopts the project as
managed, and keeps Allow LAN honest so republish can close 0.0.0.0.

* fix(gateway-embed): restore .env if LAN republish fails

compose recreate can fail after write_env_file already pointed
VOCAGATEWAY_PUBLISH_HOST at loopback. Restore the previous .env so
orphan adopt still sees a live 0.0.0.0 bind, and snap Allow LAN back
to the running publish mode.

* style(gateway-embed): type helper args and black MagicMock

* refactor(types): move Readable protocol to common_types

AGENTS.md keeps Protocol interfaces in common_types. Pairing now imports
the shared Readable surface instead of a local _Readable helper.

* fix(gateway-embed): satisfy mypy on rebased embed code

* fix(audio): keep duck restore point when a set reports failure
2026-09-29 15:33:27 -07:00
Jatin K MalikandGitHub c2654609ac feat(ui): floating glowing dictation overlay (#516)
* feat(ui): add floating glowing dictation overlay

Show a bottom-center non-focus overlay while recognition is LISTENING
or PROCESSING so users get a clear visual cue beyond the tray icon.
Optional GtkLayerShell on Wayland; plain always-on-top GTK fallback
elsewhere. Configurable via ui.show_overlay (default on) and Settings.

* fix(test): isolate dictation overlay tests from GTK pollution

Overlay unit tests no longer construct real GTK windows or import
tray_indicator, which was loading gi.repository early and breaking
later tests that mock GI (single-instance and suspend GLib checks).

* docs: add dictation overlay screenshots

Capture the floating glowing orb in listening (green) and processing
(amber) states for the PR and README screenshots section.

* test(ui): raise dictation overlay coverage for Codecov patch gate

Exercise GTK init, draw/animation, positioning, layer-shell fallbacks,
tray overlay wiring, settings toggle, and config edge cases so patch
coverage on the floating overlay clears the 80% Codecov target.

* test(tray): call overlay helpers via unbound methods

Avoid instance-level MagicMock shadowing of set_overlay_enabled under
some import/mock orderings (seen on CI Python 3.10).

* test(tray): inject explicit config mock for overlay helpers

Assign a MagicMock config_manager on the instance so assertions do not
depend on the constructor patch (which can leave a real ConfigManager
under some Python versions / import orders).

* refactor(ui): ponytail pass on dictation overlay

Drop the hand-drawn mic glyph, simplify the Cairo clear/pulse path,
reuse ConfigManager get_bool/set, tighten tray overlay helpers, and
delete string-grep structural tests that proved nothing.

* fix(ui): pin overlay to bottom strip and stop stealing focus

Use a full-width bottom strip (layer-shell left/right/bottom anchors, or
resize+move fallback) so the orb is bottom-center on Wayland. Map the
window once and toggle opacity instead of hide/show so compositors do not
re-activate it and yank focus out of the editor (which broke IBus inject).

* fix(appimage): seed GtkLayerShell typelib for dictation overlay

* fix(ui): address greptile findings on dictation overlay

* fix(appimage): keep layer-shell typelib optional for direct builds
2026-09-29 14:24:31 -07:00
Jatin K MalikandGitHub 26f90b6998 chore(release): prepare v0.17.0
Minor on the stable line. Faster Whisper and Parakeet extras, Speech Model simple setup, Snap uinput, Flatpak bundles on the tag, and QA fixes from the release branch.

Fixes tracked in the PR. Post-release follow-ups: #834.
2026-09-16 00:18:09 -07:00
1fac26ff07 fix(packaging): derive the Flatpak and AUR dependency sets from the lock (#819)
* fix(packaging): derive the Flatpak and AUR dependency sets from the lock

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

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

* test(packaging): type parametrized Flatpak source arguments

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

---------

Co-authored-by: jatinkrmalik <jatinkrmalik@gmail.com>
2026-09-12 12:41:06 -07:00
Jatin K MalikandGitHub 16175d9ea3 feat(release): attach Flatpak bundles to v* GitHub Releases (#786)
* 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.
2026-09-05 20:10:13 -07:00
Jatin K MalikandGitHub bb2b098193 chore(release): prepare v0.16.2 (#782)
Bump version metadata and series-aware notes for the 0.16.2 patch:
KDE leftover IBus, Wayland/IBus shortcuts, BackSpace delete that,
installer glslc, nightly stamp, release integrity, and CI/docs gates.
2026-09-05 15:04:48 -07:00
SergeyandGitHub a46000e249 chore(ci): test the distros the docs promise, and fix the docs that drifted (#773) 2026-09-04 23:28:58 -07:00
SergeyandGitHub e61133e246 feat(ci): gate AUR PKGBUILD builds on every PR (#772)
* 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
2026-09-04 23:25:09 -07:00
Jatin K MalikandGitHub 21b8ee8d05 fix(aur): build on Arch setuptools 84 and skip 1.4 context_params (#757)
AUR --no-isolation uses extra python-setuptools 84, so the <82 cap blocked the 0.16.0 rebuild. AUR pywhispercpp backends are still 1.4 and crash if we pass context_params; skip that argument below 1.5.
2026-08-30 01:12:48 -07:00
Jatin K MalikandGitHub 68b00bd7d8 chore(release): prepare v0.16.1 (#755)
Bump the stable line from 0.16.0 to 0.16.1. README and UPDATE keep the 0.16 series highlights and add the patch-only fixes. Website changelog, schema, AppStream, and AUR PKGBUILD follow.
2026-08-30 00:49:09 -07:00
SergeyandGitHub e9167b6e05 test(appimage): boot the built AppImage on six distros (#744)
* test(appimage): boot the built AppImage on six distros

* docs(agents): document the AppImage boot matrix
2026-08-29 15:14:03 -07:00
SergeyandGitHub 65381b4582 fix(appimage): build against a glibc floor users actually have (#743)
* 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
2026-08-29 14:46:50 -07:00
f43f9dcd72 fix: leftover engine repair, just --no-sync, and ggml verify from #713 (#732)
* 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>
2026-08-24 10:42:52 -07:00
Sergey SilaevandGitHub 582d494633 chore(build): uv tooling, a Python 3.11 floor, and verified model downloads (epic #701, phases 2.5 & 5) (#713)
* 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
2026-08-24 00:25:06 -07:00