Commit Graph
94 Commits
Author SHA1 Message Date
Jatin K MalikandGitHub cdfe541c25 feat(speech_recognition): add faster-whisper engine backend (#543)
* feat(speech_recognition): add EngineType enum and Engine Protocol

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

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

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

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

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

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

- Import missing get_compute_type helper

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

- Clear _has_torch_cuda lru_cache around exception test to avoid cached True

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

- Restore registry module state after test

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

Cover previously missing lines in:

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

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

- engine registry import-error path

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

- Add faster_whisper to --engine help text and examples

- Add faster-whisper as interactive option 5 with description

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

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

- Add smoke test for faster-whisper import

- Update welcome message engine display name

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

* refactor(faster_whisper): apply ponytail review cleanups

- Inline FASTER_WHISPER_MODEL_INFO dict literal; drop unused AVAILABLE_MODELS/MODEL_SIZES

- Reuse _has_torch_cuda() in FasterWhisperEngine.device property

- Remove redundant model None guard after is_ready() verification

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

- Update device tests to manage _has_torch_cuda lru_cache

- Remove test for deleted dead-code guard

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

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

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

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

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

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

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

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

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

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

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

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

* chore(speech_recognition): sign tip after rollback follow-up
2026-09-07 20:47:56 -07:00
8f4aeca2f1 feat(asr): add Parakeet TDT 0.6B engine via sherpa-onnx (#802)
* feat(asr): add Parakeet engine via sherpa-onnx

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix(asr): reject a truncated Parakeet download

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

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

* style(asr): annotate the Parakeet model catalog

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

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

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

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

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

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

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

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

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

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

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

* fix(ui): restore language after leaving Parakeet

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

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

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

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

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

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

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

---------

Co-authored-by: Alex Thunder <AlexThunder@gmail.com>
2026-09-07 14:32:13 -07:00
Jatin K MalikandGitHub af6b372177 fix(install): Fedora and Arch need glslc, not glslang (#763)
* fix(install): Fedora and Arch need glslc, not glslang

ggml's Vulkan CMake looks for glslc and will not take glslangValidator.
install.sh was still installing glslang on Fedora, Arch, Alpine, Void,
Solus, and Gentoo, so the pywhispercpp source build fell back to a CPU
wheel. Ubuntu and Debian already pull glslc plus patchelf.

See #604.

* fix(install): Gentoo patchelf is under dev-util

sys-devel/patchelf does not exist. emerge fails and install.sh exits,
so a fresh Gentoo install never reaches the whisper.cpp build.

See #604.
2026-08-31 10:43:06 -07:00
SergeyandGitHub f4e9fcb0ee fix(installer): survive a release without a checksum manifest, and stop inventing GPUs (#736)
* installer: leave model downloads to first run when the release pins no checksums

* installer: refuse an unpinned VOSK model before spending the download

* installer: require a real device before reporting a Vulkan GPU

* build: replace the deprecated license table and complete the packages list

* installer: treat software Vulkan renderers as no GPU, not as one

* installer: keep diagnostics off stdout so captured detection output stays clean
2026-08-26 22:53:29 -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
Jatin K MalikandGitHub 994dda10a6 fix(install): leftover review notes from #700 and #705 (#706)
* fix(install): leftover review notes from #700 and #705

The scratch dir now holds pip logs and model download temps, so a failed
run has something to inspect. Dep, venv, leftover-process, and pip
failures use exit codes 2/3/4 instead of a blanket 1. --help says the
transcript is written during installation, and the tag hint is
--tag=<release>.

AGENTS.md no longer claims the hash-pinned exports are consumed (they
wait for phases 2/3/5). Docs add a just install line and
vocalinux[vosk] next to the PyPI commands. Settings points at
--engine=whisper. The whisper.cpp fallback installs ".[vosk]".

Fixes leftover review notes from #700 and #705.
Refs #701.

* fix(install): do not let pip build PyGObject from sdist

Fresh --auto --skip-system-deps died on pygobject-3.56.3 even with
distro python3-gi present. Install the other runtime deps from
pyproject.toml, then pip install --no-deps. Extra installs use the
extra's packages only, not pip install ".[extra]".

Fixes leftover review notes from #700 and #705.
Refs #701.

* fix(install): tolerate unset XDG_SESSION_TYPE under set -u

--auto with DISPLAY but no login session died on
XDG_SESSION_TYPE: unbound variable. The session probe now uses
${XDG_SESSION_TYPE:-}, ${WAYLAND_DISPLAY:-}, and ${DISPLAY:-}.
2026-08-18 17:41:37 -07:00
Sergey SilaevandGitHub 95d135ebbf chore(deps): lock dependencies with uv and pin every build input
Phases 0-1 of #701. Drops dead deps, moves vosk to an optional extra, adds uv.lock and hash-pinned requirements exports, pins pywhispercpp to 1.5.0.

Closes #705. Refs #701.
2026-08-18 16:26:13 -07:00
Sergey SilaevandGitHub 71c84fe86b chore(build): harden installer script and replace Makefile with Justfile
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.
2026-08-18 16:24:11 -07:00
Jatin K MalikandGitHub 32a17a9659 chore: point GitHub URLs at VocaHQ (#696)
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.
2026-08-16 15:22:22 -07:00
00680c35c5 fix(installer): gate util-linux-extra to Ubuntu 24.04+ (#635)
* fix(installer): gate util-linux-extra to Ubuntu 24.04+

util-linux-extra does not exist on Ubuntu 22.04 (package split landed in
24.04). Listing it unconditionally in APT_PACKAGES_UBUNTU breaks
install.sh on 22.04-based distros such as Linux Mint.

Fixes #526

* fix(installer): probe util-linux-extra via apt-cache

Replace Ubuntu VERSION_ID gate with apt-cache show probe (per review
feedback). Handles Ubuntu derivatives (Mint, Zorin) whose VERSION_ID
does not match the noble base where util-linux-extra exists.

Fixes #526

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-08-12 23:49:12 -07:00
Jatin K MalikandGitHub c337d8f56d feat(shortcuts): default to hold Right Alt (push-to-talk) for new installs (#648)
* feat(shortcuts): default to hold Right Alt push-to-talk on first install

Double-tap Ctrl conflicts with many desktop and app shortcuts, so new
installs now use hold Right Alt (Option) in push-to-talk mode. Existing
config.json values are preserved unchanged.

Updates README, user guide, AGENTS.md, first-run dialog, website copy,
and tests to match the new defaults.

* refactor(shortcuts): trim redundant default-change test and comments

Drop the fresh-install assertion that duplicated DEFAULT_CONFIG coverage,
remove the duplicated Right-Alt rationale from config_manager, and drop a
vacuous source-string check from the settings shortcuts test.

* fix(shortcuts): align install.sh seeds and preserve legacy toggle mode

install.sh still wrote ctrl+ctrl without mode, so merges with the new
push_to_talk default became hold-Ctrl. Seed right_alt + push_to_talk for
new installs, and migrate existing shortcut-only configs to mode=toggle
so prior behavior is unchanged.

* style(config): fix black formatting after shortcuts migration

* fix(shortcuts): preserve legacy defaults when shortcuts section is missing

Existing config files without a shortcuts section previously inherited
ctrl+ctrl toggle from DEFAULT_CONFIG. Pin those historical defaults on
upgrade so the new first-install Right Alt push-to-talk defaults do not
silently change behavior.

* docs(agents): unwrap hard-broken dictation control paragraph

Keep the AGENTS.md paragraph as a single line like the surrounding prose.
2026-08-08 00:07:54 -07:00
afc4b37c80 Fix missing KDE tray icon by preferring Ayatana AppIndicator (#621)
* Fix missing KDE tray icon by preferring Ayatana AppIndicator

Fedora and Arch installs pull libappindicator-gtk3, Canonical's original
AppIndicator library that hasn't had a real release since ~2013. It loads
fine and Indicator.new_with_path()/set_status() run without raising, but
it never registers a StatusNotifierItem with KDE's StatusNotifierWatcher,
so the tray icon silently never shows up on Plasma. Confirmed on Fedora 44
/ Plasma 6.7 by checking RegisteredStatusNotifierItems over D-Bus directly
before and after the fix.

The actively maintained Ayatana fork registers correctly and is already
what the Debian, Ubuntu, openSUSE and Gentoo branches of install.sh
prefer. Reorder the gi import in tray_indicator.py to try
AyatanaAppIndicator3 first and fall back to the legacy AppIndicator3
typelib only if neither Ayatana variant is available.

Update install.sh so Fedora and Arch installs pull the Ayatana package
too, via a shared install_preferred_appindicator() helper that falls back
to the legacy package if Ayatana isn't in the repos (e.g. RHEL/CentOS
without EPEL).

* Add tests for the AppIndicator/Ayatana import fallback chain

The reordered try/except in tray_indicator.py wasn't exercised by the
existing tests: they replace gi.repository with a MagicMock, which
auto-creates any attribute on access and so can never raise the
ImportError the fallback chain is built to catch.

Add a small test class that swaps in a plain object exposing only a
chosen subset of AppIndicator symbols, so importing anything else raises
a real ImportError, and verify each of the three branches (Ayatana,
lowercase Ayatana variant, legacy AppIndicator3) resolves correctly.

* Retry Ayatana install even when legacy AppIndicator is already present

install_preferred_appindicator() previously skipped the Ayatana install
whenever either package was already present, so re-running install.sh
on an existing Fedora/Arch setup with libappindicator-gtk3 from a prior
install would never pick up Ayatana, leaving the KDE tray broken for
exactly the users this fix targets. Now it only short-circuits when
Ayatana itself is installed, tries Ayatana first regardless of legacy
presence, and only falls back to a warning about the already-installed
legacy package if the Ayatana install fails.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-08-04 03:33:40 +05:30
Jatin K MalikandGitHub 4f2a91055e feat(tray): allow disabling missing tray warning dialog (#628)
* feat(tray): allow disabling missing tray warning dialog

Fixes #620

* test(tray): stabilize missing-watcher opt-out coverage

Default the D-Bus ListNames mock to include StatusNotifierWatcher so
TrayIndicator construction no longer opens the warning dialog during
setUp, and bind the don't-show-again checkbox test to the module Gtk mock.

* test(tray): make missing-watcher opt-out tests deterministic
2026-08-02 19:41:22 -07:00
40546e7f07 fix(uninstall): remove IBus data dir & stop app by PID file not pgrep -f (#597)
* fix(uninstall): remove IBus runtime data directory on uninstall

Discussion #559 reported ~/.local/share/vocalinux-ibus surviving uninstall.
PR #569 fixed launcher wrappers but not this IBus IPC directory (socket,
PID file, stderr log). Stop the engine via engine.pid when present, remove
the directory regardless of --keep-data, and verify it in post-checks.

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

* fix(scripts): stop Vocalinux by PID file, not pgrep -f

pgrep -f vocalinux matches any process whose argv mentions the repo
path (editors, shells, test runners) and can kill unrelated local work.
AGENTS.md already warns against this.

- Prefer PIDs from instance.lock and vocalinux-ibus/engine.pid
- Verify each candidate via is_vocalinux_process (python -m vocalinux.main,
  ibus_engine.py under vocalinux, or argv0 basename vocalinux/vocalinux-gui)
- Narrow pgrep fallback only for those entrypoint patterns
- Drop the redundant stop_ibus_engine helper (engine covered via PID file)
- Still remove ~/.local/share/vocalinux-ibus on uninstall (#559)
- Align install.sh and correct the pkill tip in docs/UPDATE.md

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

* fix(scripts): detect setuptools console-script Vocalinux processes

Real installs run as `python …/bin/vocalinux`, so argv0 is python and the
previous argv0-basename check missed the live app (instance.lock PID was
skipped). Scan all cmdline args for …/bin/vocalinux or …/bin/vocalinux-gui
instead, which still avoids matching a bare repo directory path.

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>
2026-07-26 21:14:19 -07:00
Jatin K MalikandGitHub ac59a681ac fix(install): prefer both libgirepository 1.0 and 2.0 when present (#583)
On Ubuntu 24.04+ apt still lists libgirepository1.0-dev, so the installer
kept only 1.0 and skipped 2.0. Pip builds of PyGObject need the modern GI
headers from 2.0; install both packages when both exist.

Fixes #571
2026-07-23 23:43:17 -07:00
Jatin K MalikandGitHub 853c1504b2 chore(release): prepare v0.14.1-beta (#525)
Patch release for Flatpak/AUR packaging and post-0.14.0 fixes:
installer wrappers, ydotool dual API and Flatpak socket probe, model
download timeouts, layout-aware hotkeys, and related docs/web bumps.
2026-07-16 17:51:45 -07:00
48a4dd6374 fix: sg not found on Ubuntu 26.04/Debian 13 (#524)
* fix: sg not found on Ubuntu 26.04/Debian 13

The 'sg' command moved from the 'login' package to 'util-linux-extra'
in Debian 13 and Ubuntu 26.04, which is not installed by default.
This causes vocalinux and vocalinux-gui wrappers to fail with
'exec: sg: not found'.

Two changes:
1. Add util-linux-extra to APT dependency lists (Ubuntu + Debian 13+)
2. Add command -v sg safety check in both wrapper scripts so they
   fall back to direct execution if sg isn't available

Fixes #522

* fix: attempt to install util-linux-extra if sg is missing

When the wrapper scripts detect that sg is not available, they now
attempt to install util-linux-extra via apt-get before falling back
to direct execution. This provides a better user experience on
Ubuntu 26.04/Debian 13 where sg moved to a separate package.

The installation runs silently and only triggers if:
1. User is in input group but session doesn't have it
2. sg command is not found
3. apt-get is available (Debian/Ubuntu systems)

If installation fails or sg still isn't available, the scripts
fall back to direct execution (app starts but keyboard shortcuts
won't work until next login).

* refactor: extract shared sg check, drop runtime install

- Extract sg fallback logic to shared SG_CHECK variable (eliminates duplication)
- Remove runtime apt-get install of util-linux-extra (dependency list handles it)
- Flatten to simple command -v sg check with direct exec fallback

net: -24 lines

---------

Co-authored-by: Alfred Pennyworth <alfred@openclaw.ai>
2026-07-15 21:00:34 -07:00
Jatin K MalikandGitHub 8f32f01f0e fix(installer): include xsel for Wayland clipboard fallback (#496) 2026-07-02 10:33:28 -07:00
Jatin K MalikandGitHub 9c5065f452 fix(input): clarify KDE Wayland IBus setup (#466) 2026-06-10 16:08:11 -07:00
Jatin K MalikandGitHub 0c3c32f584 fix(installer): harden CUDA diagnostics with auto-remediation and behavioral tests (#451)
* fix(installer): surface pywhispercpp CUDA fallback diagnostics

* fix(installer): add patchelf auto-remediation and behavioral tests

- Add patchelf auto-remediation for bundled libcuda linkage issue
- Add ARM64 CUDA toolkit path support (aarch64-linux, sbsa-linux)
- Fix nvcc symlink resolution with readlink -f
- Add --verbose flag to CPU pywhispercpp pip install
- Add behavioral tests for CUDA diagnostic functions
- Add string-presence test for patchelf remediation

Fixes #450

* fix(installer): replace ★ with * to fix box border alignment

The ★ character (U+2605) renders as 2 columns wide in most terminals,
but the box padding assumes 1 column. This pushes the right border │
one column past the intended box edge, making it appear cut off.
Replacing with ASCII * (always 1 column) fixes the alignment.
2026-06-06 19:18:57 -07:00
Jatin K MalikandGitHub ddf073711b fix(ui): respect advanced toggle for Remote Server section (#454)
* fix(ui): respect advanced toggle for Remote Server section

The Remote Server section was packed directly into advanced_tab,
bypassing the advanced_revealer controlled by the 'Unlock Advanced
Settings' toggle. This made it always visible regardless of toggle state.

Changes:
- Move remote_api_group and remote_status_label packing from
  advanced_tab to controls_box (inside the revealer)
- Add visual separator between whisper.cpp controls and Remote Server
- Increase scrolled min_content_height from 350 to 500 for taller content
- Update power_user_switch tooltip/subtitle to mention remote server
- Update info label to clarify whisper.cpp vs remote server scope
- Align connection test to use requests.Session (consistent with
  transcription path)
- Add empty-URL warning when toggling remote server ON
- Add source-assertion tests for UI structure

Fixes: Remote Server now hidden when 'Unlock Advanced Settings' is OFF

* refactor(ui): move remote server to Speech Engine tab as peer engine

Remote server is now a first-class engine option in the Speech Engine
tab dropdown, alongside Vosk, Whisper, and whisper.cpp. The 'Use remote
server' override toggle has been removed from the Advanced tab.

Changes:
- Add 'Remote API' to engine combo dropdown (removed skip logic)
- Move remote server UI fields (URL, API key, endpoint, test button)
  from Advanced tab to Speech Engine tab
- Show/hide remote server fields based on engine selection
- Remove 'Use remote server' toggle and override logic
- Simplify get_selected_settings (engine comes from combo directly)
- Simplify _update_engine_specific_ui (check combo value)
- Remove _on_use_remote_toggled method
- Update _load_and_apply_settings (no remote switch logic)
- Update power_user tooltip/subtitle (no longer mentions remote server)
- Update tests for new behavior

Backend requires zero changes - recognition_manager.py already treats
remote_api as a first-class engine.

User flow:
1. Select 'Remote API' from engine dropdown
2. Model selection hides automatically
3. Remote server config fields appear (URL, API key, endpoint)
4. Enter server details and test connection
5. Done - clear and intuitive
2026-06-06 17:53:54 -07:00
+1
sevenjayGitHubSbenazarjayJatin K MalikFakhareddineSisyphusdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>parko636juanfradbPep Locanto
84d2e1a929 feat: add remote API speech recognition engine with installation and configuration support. (#335)
* feat: add remote API speech recognition engine with installation and configuration support.

* fix: reformatted by black

* fix: unit test in test_main_initializes_components

* fix typo

* Apply suggestion from @jatinkrmalik

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>

* feat: Validate remote API URL format, enhance connection testing, update default remote API endpoint.

* fix typo

* feat: add remote API recognition manager test suite

* fix: add Python 3.14 support and lxml>=6.1.0 (fixes #404)

- Add Python 3.14 to classifiers
- Add lxml>=6.1.0 to dependencies to fix install on Python 3.14 (CachyOS, Arch)
- Update black target-version to include py314

Fixes #404
Closes #405

* fix(install): validate pyproject.toml/setup.py content before entering local repo mode (fixes #396)

Fix false local repo detection when users run `uv init` before installation. Changed detection from naive file existence check to content validation:
- For pyproject.toml: check for `name = "vocalinux"`
- For setup.py: check for "vocalinux" string
- Falls through to remote clone mode if neither matches

Fixes #396

* fix(recognition): preserve final speech on stop (fixes #401)

Replace hard-coded stop tail trim with configurable stop_sound_guard_ms
(default 200ms) so quick key release no longer clips the final spoken
word in push-to-talk mode.

Co-authored-by: Fakhareddine <fe.sabbari@claranet.com>

* fix(ibus): handle engine instance destruction on layout switch (fixes #388, closes #389)

Override do_destroy() to clear _active_instance when IBus destroys the
engine on keyboard layout switch (Super+Space). Add retry logic in
inject_text() — on NO_ENGINE, re-activates via switch_engine() and
retries once to recover cleanly.

Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com>

* chore: ignore opentree worktree dir and config (#412)

.opentree/ contains git worktrees created by the opentree CLI tool
(https://github.com/axelgar/opentree). These are local developer
artifacts and should not be tracked.

opentree.toml is the per-repo opentree config - also local/personal,
not something contributors need to share.

* test(ibus): fix do_destroy CI failure (issubclass TypeError without IBus) (#410)

* test(ibus): fix do_destroy super() test failing in CI without IBus installed

Patch builtins.super instead of relying on mock_ibus.Engine.do_destroy,
which fails with issubclass() TypeError when VocalinuxEngine is built
with object as its base (IBus unavailable in CI).

* test(ibus): fix MagicMock.__new__ TypeError on Python < 3.13

VocalinuxEngine inherits from MagicMock at import time (IBus is mocked
via sys.modules at test-file level). Calling VocalinuxEngine.__new__(VocalinuxEngine)
delegates to MagicMock.__new__ which runs issubclass(cls, AsyncMockMixin).
On Python < 3.13 that raises TypeError when the module-cached class is a
MagicMock spec object rather than a real type.

Fix: introduce _make_engine() helper that allocates via a thin concrete
subclass whose __new__ calls object.__new__ directly, bypassing the
MagicMock allocation path entirely. Verified across all three
TestVocalinuxEngineDestroy test methods.

* test(ibus): use unbound do_destroy call to avoid MagicMock interference

The previous fix (thin _ConcreteEngine subclass) still failed because
VocalinuxEngine inherits MagicMock.__getattribute__, which intercepts
.do_destroy on instances and returns a mock attribute rather than the
real method - regardless of Python version.

Call VocalinuxEngine.do_destroy(engine) directly with a plain object()
as the self argument instead. do_destroy only uses self for identity
comparison (is VocalinuxEngine._active_instance), so any object works.
This bypasses MagicMock's MRO entirely.

* test(ibus): reload module and instantiate real engine class for do_destroy tests

* refactor(ibus): extract destroy handler for stable cross-version tests

* test(ibus): cover destroy helper branches and do_destroy delegation

* test(ibus): stabilize do_destroy CI behavior across environments

Avoid brittle super() mocking in no-IBus environments and gate the IBus-only parent destroy path test behind runtime capability checks to prevent false CI failures on Python matrix runners.

* test(ibus): remove flaky do_destroy class-path assertions

Drop brittle class-method destroy tests that fail under mocked GI/IBus on older Python runners and keep deterministic helper-level coverage for destroy behavior.

* fix(text-injection): recover from IBus runtime failures (#411)

* fix(ibus): retry and recover from transient socket failures

Harden IBus injection against connection-refused, timeout, and engine process races so dictation is more resilient in real-world desktop sessions.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* fix(text-injection): fallback to non-IBus backend on runtime IBus failures

Allow dictation to continue when IBus runtime injection fails by automatically switching to xdotool/wtype/ydotool fallbacks instead of returning an immediate failure.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* fix(ibus): preserve live socket injection during recovery

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* test(coverage): add runtime fallback and ibus retry coverage

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* fix(ibus): probe engine readiness at startup and harden NO_ENGINE retries (#391)

* fix(ibus): wait for engine readiness before first injection

Add a startup readiness probe that pings the IBus engine after activation so first-use dictation no longer races do_enable(). Also switch NO_ENGINE retry behavior to exponential backoff and cover the new flow in tests.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test(ibus): cover readiness probe edge cases

Add focused tests for the new startup readiness path, including direct PING success, retry exhaustion, unexpected responses, and engine re-activation before injection. This raises coverage on vocalinux.text_injection.ibus_engine from 68% to 73% in the targeted suite.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(text-injection): avoid blocking IBus and clipboard fallbacks

Warm IBus in the background so startup no longer stalls while readiness probing, and make clipboard helpers fail fast with per-session health caching so non-ASCII ydotool fallback does not lock dictation.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test(ibus): update retry tests for merged NO_ENGINE recovery logic

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* chore(deps-dev): bump postcss from 8.4.39 to 8.5.10 in /web

Bump postcss dev dependency (CSS processing). No runtime impact.

* fix(logging): clean up runtime log noise and cache hardware detection

Clean up duplicated code artifacts from recent merges and cache hardware detection to eliminate ~1.3s UI lag when opening settings dialog.

* test: cover recognition internals and suppress desktop notifications (#414)

* test(recognition): cover _perform_recognition and _enqueue_audio_segment

The previous commit demoted logger.info('DEBUG: ...') to logger.debug(...)
and removed a dead loop body in _perform_recognition. Codecov flagged 12
uncovered lines — all inside _perform_recognition and _enqueue_audio_segment,
which existing tests always mocked out.

Add direct (non-mocked) tests for both methods:

- _enqueue_audio_segment:
  * empty buffer → early return
  * normal buffer → enqueues correctly
  * full queue → drops oldest and retries

- _perform_recognition:
  * processes a segment then exits on None signal
  * drains remaining items on None signal
  * exits cleanly when idle with empty queue
  * continues looping on queue timeout while recording

These tests exercise the actual threading/queue logic and cover the
logger.debug / logger.warning calls as a side effect.

* test(conftest): suppress real desktop notifications during test runs

_show_notification shells out to notify-send, which pops up real system
notifications when tests exercise the model-not-ready path. Add an autouse
fixture that patches _show_notification for all tests EXCEPT those whose
name contains 'show_notification' (so direct tests of the helper still
verify subprocess.Popen calls).

* chore(gitignore): ignore .sisyphus directory

* fix(install): improve openSUSE dependency handling with fallbacks (#418)

The suse case in install_system_dependencies() previously installed all
packages blindly, causing the entire install to fail if a single package
was unavailable (e.g., glslang on some openSUSE repos).

Changes:
- Check each package individually with rpm -q before installing
- Add graceful fallback for glslang -> glslang-devel
- Add graceful fallback for libappindicator-gtk3 -> typelib-1_0-AppIndicator3-0_1 -> libayatana-appindicator3-1
- Continue installation with warning instead of hard failure

Fixes #407

* fix(remote_api): re-init engine on resume from suspend

Add a remote_api branch to reinitialize_after_resume mirroring the
dispatch chain in __init__, so the engine is recreated after a
suspend/resume cycle. Null self._http_session alongside the existing
self.model and self.recognizer resets so a stale pooled-connection
session is not reused after resume.

* perf(remote_api): pool HTTP connections via requests.Session

Introduces a manager-owned requests.Session (_http_session) so all
remote_api engine HTTP traffic reuses a single connection pool instead
of opening a new connection for every request.

Lifecycle:
- __init__: self._http_session = None (attribute always present)
- _init_remote_api: close prior session if it exists, then create a
  new requests.Session(); use session.get for the connection test.
  Prevents connection leaks on back-to-back inits (re-init guard).
- _try_openai_api / _try_whispercpp_server_api: replaced requests.post
  with self._http_session.post; local 'import requests' retained for
  the requests.exceptions.ConnectionError catch.
- reconfigure: captures old engine before self.engine is updated;
  closes and nulls _http_session when switching away from remote_api.
- reinitialize_after_resume: nulls _http_session before _init_remote_api
  recreates it.

Tests: added TestHTTPSession class (4 cases) in
tests/test_recognition_manager_remote_api.py; all 40 tests pass.
Updated _setup_requests_get_*/post_* helpers to configure the session
instance (mock_requests.Session.return_value) instead of the
module-level mock, keeping all 36 pre-existing tests green.

* docs: add remote HTTP transcription setup guide

* fix(install): avoid asking for remote server URL twice

run_interactive_install collects the URL upfront in Step 3, but
install_python_package re-declared REMOTE_API_URL as local and prompted
again, ignoring the wizard value. Inherit the wizard's value instead.

* ui(settings): hide remote API behind an Advanced tab

Addresses upstream PR #335 review feedback from @jatinkrmalik that
remote API is not plug-and-play and should not be a peer of the local
engines for casual users.

- Remove 'Remote API' from the Speech Engine tab's engine combo; the
  combo now shows local engines only (vosk / whisper / whisper_cpp).
- Add a new 'Advanced' notebook tab with a 'Remote Server' group
  containing a 'Use remote server' switch, the existing URL/Key/
  Endpoint fields, and the Test Connection button + status label.
- The switch overrides the local engine selection. get_selected_settings
  returns engine='remote_api' iff the switch is on, otherwise the combo
  selection. URL/Key/Endpoint persist independently of the toggle state.
- _load_and_apply_settings: if config has engine='remote_api', the
  switch is turned on and the combo is set to a sensible local fallback
  (whisper_cpp, or first available local engine) so toggling off has
  somewhere to land.
- _update_engine_specific_ui no longer special-cases the combo for
  remote_api (the combo never has it); it hides the model row, info
  card and legend whenever the remote switch is on.
- Update HTTP_REMOTE.md to reflect the new tab location.

* ui(remote-api): mark remote API as advanced and default toggle off

- install.sh: relabel option 4 as 'REMOTE API (ADVANCED)' to signal
  it as a non-default, advanced engine choice.
- settings_dialog.py: explicitly initialise the 'Use remote server'
  switch to inactive at widget creation, making the default state
  unambiguous (saved-config load logic still toggles it on for
  existing remote_api installs).

* fix(remote-api): preserve trailing newline from transcription responses

The remote-API transcription path stripped all whitespace from the
upstream response before injection, silently discarding a trailing
newline that some pipelines (post-processing proxies, fixup layers)
emit to signal "submit" / Enter. Whisper itself doesn't emit trailing
newlines, so for the common case this is a no-op; for users with a
post-processing layer it stops their submit signal from being eaten.

_filter_non_speech now uses a whitespace-stripped probe for its
non-speech pattern checks but returns probe + "\n" when the original
input ended with "\n" or "\r" (CRLF normalises to LF). The redundant
.strip() at the call site is removed so the function receives the raw
API text. Existing filter behaviour (BLANK_AUDIO, music notes, low
speech-content ratio, etc.) is unchanged. Four new unit tests cover
the trailing-newline cases.

* test(remote-api): cover transcribe exception handler and unknown-engine reinit branch

Adds two unit tests buttoning up previously-uncovered defensive branches:

- _transcribe_with_remote_api swallows unexpected exceptions and returns
  "" — verified by raising RuntimeError from _try_whispercpp_server_api.
- reinitialize_after_resume short-circuits without dispatching when the
  engine attribute is unknown — verified by mutating engine to a bogus
  value and asserting no _init_* method is called.

* feat(settings): add Advanced tab with whisper.cpp anti-hallucination parameters (#415)

* feat(settings): add Advanced tab with whisper.cpp anti-hallucination parameters

Add a new Advanced settings tab that exposes whisper.cpp decoding
parameters to help users reduce hallucinations and tune recognition
behavior.

Parameters exposed:
- No Timestamps (default: on)
- Suppress Non-Speech Tokens (default: on)
- No Context (default: on)
- Initial Prompt (default: empty)
- Temperature (default: 0.0)
- Temperature Increment (default: -1.0, disables fallback)
- Entropy Threshold (default: 2.4)
- Logprob Threshold (default: -1.0)
- No-Speech Threshold (default: 0.6)

All parameters are saved under the advanced config section and are
only active when the whisper.cpp engine is selected. The UI grays out
these settings and shows an explanatory note when another engine is
active.

Changes:
- config_manager.py: add whisper.cpp keys to DEFAULT_CONFIG[advanced]
- recognition_manager.py: read params from kwargs/config, pass to Model()
- main.py: pass advanced settings from config to SpeechRecognitionManager
- settings_dialog.py: new Advanced tab with switches, spin buttons, entry
- tests: update test_main.py assertions, add whispercpp_advanced_defaults
  and whispercpp_advanced_persistence tests

Fixes #363

* fix(whispercpp): gracefully filter unsupported params for pywhispercpp 1.4.1

pywhispercpp 1.4.1 does not expose no_timestamps or suppress_non_speech_tokens
to Python even though they exist in the underlying whisper.cpp C struct.
This causes AttributeError on startup.

Add _load_model_with_compatible_params() which catches AttributeError,
identifies the unsupported param from the error message, removes it, and
retries until the model loads successfully or a non-AttributeError occurs.

Also refactor _build_whispercpp_model_kwargs() from _load_whispercpp_model()
for testability.

Fixes local startup crash reported during PR testing.

* fix(whispercpp): persist advanced settings correctly

* chore(config): add advanced power-user default

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* feat(settings): gate advanced controls behind power-user opt-in

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* fix(whispercpp): prefilter unsupported native params

* fix(settings): polish advanced whispercpp controls

* fix(installer): reuse existing whispercpp builds (#421)

* docs: clarify PyPI installation requirements (#423)

* chore(deps): bump next (#429)

Bumps the npm_and_yarn group with 1 update in the /web directory: [next](https://github.com/vercel/next.js).


Updates `next` from 15.5.15 to 15.5.18
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.15...v15.5.18)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.18
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(installer): repair pywhispercpp library loading (#433)

* fix: play stop sound immediately on release (#426)

* fix(recognition): play stop sound after audio thread joins to prevent capture on short recordings (#436)

Moves play_stop_sound() to after audio_thread.join() but before the
final buffer processing. This closes the race where the audio thread's
in-flight chunk could capture the stop cue for short (≤ ~192 ms) recordings,
while still feeling immediate compared to the original placement after all
recognition cleanup.

* fix(install): correct openSUSE Tumbleweed dependencies (#420)

* fix(install): correct openSUSE Tumbleweed dependencies

* style: format openSUSE installer changes

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>

* fix(installer): harden Debian compatibility layer (#437)

Add missing pywhispercpp build prerequisites (libssl-dev, autoconf,
automake, libtool, patchelf) to both Debian and Ubuntu apt package lists.
These are not pulled in transitively on clean Debian installs and cause
CMake bootstrap failures during source compilation.

Add runtime smoke test for pywhispercpp to verify_installation() so
"installs but does not run" failures surface at install time with
actionable diagnostic steps instead of silently at first launch.

Treat Debian as a first-class target — remove the "primarily designed for
Ubuntu" warning for Debian systems. Remain conservative for non-Debian,
non-Ubuntu distros.

Add DEBIAN_FRONTEND=noninteractive to all apt install commands to prevent
interactive debconf prompts from blocking unattended installs.

Improve ydotool failure messaging on Debian with compile-from-source
instructions since ydotool is not packaged in Debian standard repos.

Add Debian-specific troubleshooting section to DISTRO_COMPATIBILITY.md
covering build deps, ydotool compilation, scoped source builds, and
libwhisper.so resolution diagnostics.

References: https://medium.com/@cslev/talking-to-my-linux-box-without-talking-to-the-cloud-vocalinux-on-debian-without-the-tears-10bf053ea21b

* fix(install): refresh ldconfig after openSUSE typelib install, clarify python3XY placeholder convention (#438)

- Add 'sudo ldconfig' after zypper install in suse_install_appindicator_runtime()
  to ensure GI typelib cache is refreshed so newly installed typelib packages
  are immediately discoverable by GObject introspection
- Add section-level comment in distro-package-map.yaml explaining that
  python3XY is a placeholder convention resolved at install time by
  suse_python_package_prefix() / suse_python_package_candidates()

Follow-up polish from review of #420.

* fix(whispercpp): reduce CPU threads and ensure GPU backend builds in dev mode (#439)

* fix(whispercpp): reduce CPU threads and ensure GPU backend builds in dev mode

- Reduce default n_threads from cpu_count() (16) to max 8 (CPU) / max 4 (GPU)
- Add runtime pywhispercpp GPU capability detection (_detect_pywhispercpp_gpu_backend)
- Make n_threads configurable via whispercpp_n_threads config option
- Fix install.sh regression: GPU source build was skipped in --dev mode
- Extract GPU install logic into install_whispercpp_with_gpu_support()
- Add is_pywhispercpp_gpu_capable() to detect GPU libs in installed pywhispercpp
- Force rebuild pywhispercpp when GPU is detected but lacks GPU support

Fixes CPU spiking to 100% on GPU systems due to excessive thread count
and ensures dev installs get GPU acceleration.

* fix(audio): skip output-only HDMI devices when selecting input device

Add _resolve_valid_input_device() that filters devices by
maxInputChannels > 0, falling back preferred → default → first valid.

Previously, HDMI output devices (0 input channels) could be selected
as the default audio input, causing 'Invalid number of channels' errors.
Now both _record_audio() and _attempt_audio_reconnection() validate the
device has input channels before probing or opening the stream.

* fix(whispercpp): treat n_threads=0 as auto-detect, not literal zero

Config default whispercpp_n_threads=0 was being passed directly
to whisper.cpp as n_threads=0, causing 'cannot create std::vector
larger than max_size()' transcription errors. Now 0 and negative
values are treated as auto-detect, falling through to the smart
thread count logic (max 4 with GPU, max 8 on CPU).

* test: add whispercpp_n_threads to mock assertions and config defaults

Update test_main.py mock to include the new whispercpp_n_threads=0 kwarg
and test_config_manager.py to assert the default config value.

* fix(audio): harden device enumeration against test mocks

The new HDMI-filter helper and GPU-backend detector crashed unit tests
that pass MagicMock() for the PyAudio/pathlib surfaces:

- _find_pywhispercpp_shared_library_dirs hit TypeError when tests
  monkey-patch os.stat (Path.is_dir internally calls S_ISDIR on a
  non-int st_mode). Widen the catch to OSError/TypeError/ValueError so
  unconfigured stat results just skip the candidate dir.

- _resolve_valid_input_device crashed with 'MagicMock > int' on
  audio.get_device_count() / info.get('maxInputChannels'). Coerce the
  count, treat zero/unknown as 'use system default', and include
  non-dict info objects rather than filtering everything out.

No production behavior changes: real PyAudio always returns int counts
and dict device info, so the HDMI filter still works as designed.

* test(recognition): cover new HDMI filter and GPU backend detector

- TestResolveValidInputDevice: 9 cases for the HDMI-filter resolver
  (preferred passthrough, HDMI fallback, no-default fallback, no-input
  None return, and three mock-tolerance escape hatches).
- TestDetectPywhispercppGpuBackend: 4 cases for runtime GPU library
  detection (cpu/vulkan/cuda/empty dirs).
- TestFindPywhispercppSharedLibraryDirsResilience: proves the widened
  exception catch survives a TypeError from pathlib.Path.is_dir.

* test(text-injection): wait for IBus init thread to avoid race on slow runners

The IBus environment promotion runs in a background daemon thread spawned
by TextInjector.__init__. The two TestIBusRuntimeFallback cases asserted
on injector.environment immediately after construction, which raced the
thread on Python 3.10 GHA runners and produced intermittent failures.

Join the init thread (with a generous timeout) before asserting, so the
test exercises the post-warmup state deterministically.

* chore(release): prepare v0.11.0-beta (#440)

- Bump version from 0.10.2-beta to 0.11.0-beta
- Update all version references across codebase
- Add What's New section for v0.11.0-beta in README and docs/UPDATE.md
- Add v0.11.0-beta entry to website changelog
- Update SECURITY.md supported versions to 0.11.x
- Update website schema, badge, and package version
- Update RELEASE_PROCESS.md version history table

* fix(startup): allow launch without pynput backend (#448)

* feat(vad): add Silero VAD and drop silence-only buffers (#447)

* feat(vad): replace amplitude-based VAD with Silero neural network

Replace the simple amplitude threshold VAD with Silero VAD, a lightweight
neural network model that classifies audio as speech/non-speech. This
addresses the root cause of #302 (silence not stopping recording) and

- Add silero_vad.py wrapper using ONNX Runtime with bundled Silero v5 model
- Accumulate 512-sample chunks with 64-sample context window per official API
- Map existing vad_sensitivity (1-5) to Silero thresholds (0.8-0.3)
- Use max() across sub-chunks to avoid missing speech onsets
- Fall back to amplitude VAD when onnxruntime is unavailable
- Add onnxruntime as optional dependency (pip install vocalinux[vad])

Closes #344

* address review: add tests, LICENSE, logging, validation fixes

- Add unit tests for SileroVAD (process, reset, fallback, sensitivity mapping)
- Add MIT LICENSE for bundled Silero VAD model in data/
- Log which VAD backend is active on init
- Validate chunk length in SileroVAD.process()
- Skip is_speech check until a full Silero chunk is processed
- Add thread safety note to SileroVAD docstring
- Document resampling rate effect on VAD resolution
- Comment explaining why load_silero_vad() catches broad Exception

* address review: bundle license, document [vad] extra, surface backend in UI

- Pack data/LICENSE into the wheel (pyproject package-data) so attribution
  travels with the model
- README: short note on enabling neural VAD via vocalinux[vad]
- Settings: VAD Sensitivity row subtitle shows the active backend; if
  onnxruntime is missing, an info-row beneath the group spells out the
  install command and the restart requirement
- silero_vad.is_silero_available() probes onnxruntime + model presence
  w/o instantiating an InferenceSession
- CI installs .[dev,vad] so Silero paths get exercised
- Integration test for _record_audio Silero branch (mocked pyaudio +
  scripted SileroVAD), covering silence flush, speech reset, and
  sensitivity threshold mapping at sens=1 vs sens=5
- Tests no longer skip when other modules globally mock numpy: pop the
  top-level numpy ref so a fresh import gives the real package, and
  swap sys.modules["numpy"] inside the _record_audio call so the lazy
  import inside the function picks up real numpy too

* fix(vad): drop silence-only recording buffers

* test(vad): align stop buffer expectations

* test(vad): preserve real numpy module

* fix(install): install vad support opportunistically

* test(vad): cover recognition manager vad edges

---------

Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com>

* fix(web): clarify speech demo browser support (#449)

* fix black format check

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: jay <sevenjay@users.noreply.github.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
Co-authored-by: Fakhareddine <fe.sabbari@claranet.com>
Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: parko636 <alex@alexparkinson.net>
Co-authored-by: juanfradb <94815906+juanfradb@users.noreply.github.com>
Co-authored-by: Pep Locanto <peplocanto@gmail.com>
2026-06-06 15:52:46 -07:00
3f5a5effff feat(vad): add Silero VAD and drop silence-only buffers (#447)
* feat(vad): replace amplitude-based VAD with Silero neural network

Replace the simple amplitude threshold VAD with Silero VAD, a lightweight
neural network model that classifies audio as speech/non-speech. This
addresses the root cause of #302 (silence not stopping recording) and

- Add silero_vad.py wrapper using ONNX Runtime with bundled Silero v5 model
- Accumulate 512-sample chunks with 64-sample context window per official API
- Map existing vad_sensitivity (1-5) to Silero thresholds (0.8-0.3)
- Use max() across sub-chunks to avoid missing speech onsets
- Fall back to amplitude VAD when onnxruntime is unavailable
- Add onnxruntime as optional dependency (pip install vocalinux[vad])

Closes #344

* address review: add tests, LICENSE, logging, validation fixes

- Add unit tests for SileroVAD (process, reset, fallback, sensitivity mapping)
- Add MIT LICENSE for bundled Silero VAD model in data/
- Log which VAD backend is active on init
- Validate chunk length in SileroVAD.process()
- Skip is_speech check until a full Silero chunk is processed
- Add thread safety note to SileroVAD docstring
- Document resampling rate effect on VAD resolution
- Comment explaining why load_silero_vad() catches broad Exception

* address review: bundle license, document [vad] extra, surface backend in UI

- Pack data/LICENSE into the wheel (pyproject package-data) so attribution
  travels with the model
- README: short note on enabling neural VAD via vocalinux[vad]
- Settings: VAD Sensitivity row subtitle shows the active backend; if
  onnxruntime is missing, an info-row beneath the group spells out the
  install command and the restart requirement
- silero_vad.is_silero_available() probes onnxruntime + model presence
  w/o instantiating an InferenceSession
- CI installs .[dev,vad] so Silero paths get exercised
- Integration test for _record_audio Silero branch (mocked pyaudio +
  scripted SileroVAD), covering silence flush, speech reset, and
  sensitivity threshold mapping at sens=1 vs sens=5
- Tests no longer skip when other modules globally mock numpy: pop the
  top-level numpy ref so a fresh import gives the real package, and
  swap sys.modules["numpy"] inside the _record_audio call so the lazy
  import inside the function picks up real numpy too

* fix(vad): drop silence-only recording buffers

* test(vad): align stop buffer expectations

* test(vad): preserve real numpy module

* fix(install): install vad support opportunistically

* test(vad): cover recognition manager vad edges

---------

Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com>
2026-06-02 12:43:51 -07:00
Jatin K MalikandGitHub c65cc14a47 fix(whispercpp): reduce CPU threads and ensure GPU backend builds in dev mode (#439)
* fix(whispercpp): reduce CPU threads and ensure GPU backend builds in dev mode

- Reduce default n_threads from cpu_count() (16) to max 8 (CPU) / max 4 (GPU)
- Add runtime pywhispercpp GPU capability detection (_detect_pywhispercpp_gpu_backend)
- Make n_threads configurable via whispercpp_n_threads config option
- Fix install.sh regression: GPU source build was skipped in --dev mode
- Extract GPU install logic into install_whispercpp_with_gpu_support()
- Add is_pywhispercpp_gpu_capable() to detect GPU libs in installed pywhispercpp
- Force rebuild pywhispercpp when GPU is detected but lacks GPU support

Fixes CPU spiking to 100% on GPU systems due to excessive thread count
and ensures dev installs get GPU acceleration.

* fix(audio): skip output-only HDMI devices when selecting input device

Add _resolve_valid_input_device() that filters devices by
maxInputChannels > 0, falling back preferred → default → first valid.

Previously, HDMI output devices (0 input channels) could be selected
as the default audio input, causing 'Invalid number of channels' errors.
Now both _record_audio() and _attempt_audio_reconnection() validate the
device has input channels before probing or opening the stream.

* fix(whispercpp): treat n_threads=0 as auto-detect, not literal zero

Config default whispercpp_n_threads=0 was being passed directly
to whisper.cpp as n_threads=0, causing 'cannot create std::vector
larger than max_size()' transcription errors. Now 0 and negative
values are treated as auto-detect, falling through to the smart
thread count logic (max 4 with GPU, max 8 on CPU).

* test: add whispercpp_n_threads to mock assertions and config defaults

Update test_main.py mock to include the new whispercpp_n_threads=0 kwarg
and test_config_manager.py to assert the default config value.

* fix(audio): harden device enumeration against test mocks

The new HDMI-filter helper and GPU-backend detector crashed unit tests
that pass MagicMock() for the PyAudio/pathlib surfaces:

- _find_pywhispercpp_shared_library_dirs hit TypeError when tests
  monkey-patch os.stat (Path.is_dir internally calls S_ISDIR on a
  non-int st_mode). Widen the catch to OSError/TypeError/ValueError so
  unconfigured stat results just skip the candidate dir.

- _resolve_valid_input_device crashed with 'MagicMock > int' on
  audio.get_device_count() / info.get('maxInputChannels'). Coerce the
  count, treat zero/unknown as 'use system default', and include
  non-dict info objects rather than filtering everything out.

No production behavior changes: real PyAudio always returns int counts
and dict device info, so the HDMI filter still works as designed.

* test(recognition): cover new HDMI filter and GPU backend detector

- TestResolveValidInputDevice: 9 cases for the HDMI-filter resolver
  (preferred passthrough, HDMI fallback, no-default fallback, no-input
  None return, and three mock-tolerance escape hatches).
- TestDetectPywhispercppGpuBackend: 4 cases for runtime GPU library
  detection (cpu/vulkan/cuda/empty dirs).
- TestFindPywhispercppSharedLibraryDirsResilience: proves the widened
  exception catch survives a TypeError from pathlib.Path.is_dir.

* test(text-injection): wait for IBus init thread to avoid race on slow runners

The IBus environment promotion runs in a background daemon thread spawned
by TextInjector.__init__. The two TestIBusRuntimeFallback cases asserted
on injector.environment immediately after construction, which raced the
thread on Python 3.10 GHA runners and produced intermittent failures.

Join the init thread (with a generous timeout) before asserting, so the
test exercises the post-warmup state deterministically.
2026-05-30 16:44:53 -07:00
Jatin K MalikandGitHub 86766d0ea0 fix(install): refresh ldconfig after openSUSE typelib install, clarify python3XY placeholder convention (#438)
- Add 'sudo ldconfig' after zypper install in suse_install_appindicator_runtime()
  to ensure GI typelib cache is refreshed so newly installed typelib packages
  are immediately discoverable by GObject introspection
- Add section-level comment in distro-package-map.yaml explaining that
  python3XY is a placeholder convention resolved at install time by
  suse_python_package_prefix() / suse_python_package_candidates()

Follow-up polish from review of #420.
2026-05-30 15:08:13 -07:00
Jatin K MalikandGitHub 59ab9fe587 fix(installer): harden Debian compatibility layer (#437)
Add missing pywhispercpp build prerequisites (libssl-dev, autoconf,
automake, libtool, patchelf) to both Debian and Ubuntu apt package lists.
These are not pulled in transitively on clean Debian installs and cause
CMake bootstrap failures during source compilation.

Add runtime smoke test for pywhispercpp to verify_installation() so
"installs but does not run" failures surface at install time with
actionable diagnostic steps instead of silently at first launch.

Treat Debian as a first-class target — remove the "primarily designed for
Ubuntu" warning for Debian systems. Remain conservative for non-Debian,
non-Ubuntu distros.

Add DEBIAN_FRONTEND=noninteractive to all apt install commands to prevent
interactive debconf prompts from blocking unattended installs.

Improve ydotool failure messaging on Debian with compile-from-source
instructions since ydotool is not packaged in Debian standard repos.

Add Debian-specific troubleshooting section to DISTRO_COMPATIBILITY.md
covering build deps, ydotool compilation, scoped source builds, and
libwhisper.so resolution diagnostics.

References: https://medium.com/@cslev/talking-to-my-linux-box-without-talking-to-the-cloud-vocalinux-on-debian-without-the-tears-10bf053ea21b
2026-05-30 15:02:27 -07:00
a47747d6a4 fix(install): correct openSUSE Tumbleweed dependencies (#420)
* fix(install): correct openSUSE Tumbleweed dependencies

* style: format openSUSE installer changes

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-05-30 14:48:36 -07:00
Jatin K MalikandGitHub 722fc23403 fix(installer): repair pywhispercpp library loading (#433) 2026-05-21 21:37:04 -07:00
Jatin K MalikandGitHub ff9fd0a961 fix(installer): reuse existing whispercpp builds (#421) 2026-05-06 22:48:05 -07:00
Jatin K MalikandGitHub 492f64ad8d fix(install): improve openSUSE dependency handling with fallbacks (#418)
The suse case in install_system_dependencies() previously installed all
packages blindly, causing the entire install to fail if a single package
was unavailable (e.g., glslang on some openSUSE repos).

Changes:
- Check each package individually with rpm -q before installing
- Add graceful fallback for glslang -> glslang-devel
- Add graceful fallback for libappindicator-gtk3 -> typelib-1_0-AppIndicator3-0_1 -> libayatana-appindicator3-1
- Continue installation with warning instead of hard failure

Fixes #407
2026-05-03 23:34:53 -07:00
Jatin K MalikandGitHub 79ae8e45d9 fix(install): validate pyproject.toml/setup.py content before entering local repo mode (fixes #396)
Fix false local repo detection when users run `uv init` before installation. Changed detection from naive file existence check to content validation:
- For pyproject.toml: check for `name = "vocalinux"`
- For setup.py: check for "vocalinux" string
- Falls through to remote clone mode if neither matches

Fixes #396
2026-05-02 17:28:32 -07:00
Jatin K MalikandGitHub 1c2fb4dadf fix(install): add missing dependencies for Pop!_OS and Ubuntu 24.04+ (#379)
Add cmake, libcairo2-dev, and dynamic libgirepository version detection
to fix installation failures on Pop!_OS (Cosmic DE) and newer Ubuntu.

Three issues addressed:

1. cmake: Required by pywhispercpp to build whisper.cpp from source.
   Added to all distro package lists (apt, dnf, pacman, zypper, emerge,
   apk, xbps, eopkg).

2. libcairo2-dev: Required to build pycairo. Was present in the Debian
   package list but missing from the Ubuntu list, which Pop!_OS uses.

3. libgirepository version: Ubuntu 24.04+ and Pop!_OS Cosmic+ ship
   libgirepository-2.0-dev instead of libgirepository1.0-dev. Added
   runtime detection to select the correct package, mirroring the
   existing version-aware handling already in place for Debian.

Reported-by: DavidHoenisch
Ref: https://github.com/jatinkrmalik/vocalinux/discussions/378
2026-04-02 14:59:00 -07:00
Jatin K MalikandGitHub c1f12db845 chore(release): prepare v0.10.1-beta (#358)
* chore(release): prepare v0.10.1-beta

* chore(release): update release notes with new fixes from main
2026-03-30 00:31:08 -07:00
Jatin K MalikandGitHub 860a9f9ae6 fix: Bundle resources in package to fix missing system tray icons (fixes #349) (#354)
* fix: bundle resources in package to fix missing icons in system tray (fixes #349)

The resources directory (icons and sounds) was not included in pip-installed
packages because it lived outside the src/ package directory. This caused
the resource_manager to find an empty icon directory at runtime, resulting in
the ellipsis (three dots) icon in the GNOME system tray.

Changes:
- Copy resources into src/vocalinux/resources/ so setuptools bundles them
- Add 'vocalinux.resources/**/*' to package-data in pyproject.toml
- Add MANIFEST.in for sdist builds
- Update resource_manager to check package-bundled resources first
  (module_dir.parent / 'resources') before checking external paths
- Add install_resources_to_venv() to install.sh for curl-based installs,
  copying resources to /share/vocalinux/resources

This fixes both pip installs (resources bundled in the wheel) and curl-based
installs (resources copied to venv share directory).

* fix: ensure bundled resources are included in sdist and pip installs

- Add 'vocalinux.resources' to setuptools packages list so the
  subpackage (and its __init__.py) is recognized by setuptools
- Add __init__.py to src/vocalinux/resources/ to make it a proper
  Python subpackage for reliable discovery
- Add 'recursive-include src/vocalinux/resources *' to MANIFEST.in
  so sdist archives include the bundled icons and sounds
2026-03-27 11:07:19 -07:00
84ec79c769 chore(release): prepare v0.10.0-beta (#345)
* chore(release): prepare v0.10.0-beta

Bump versions and release references across app, docs, installer, and website for the next beta release. Add v0.10.0-beta release notes entries covering reliability fixes and CI/test improvements since v0.9.0-beta.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs(release): align Python baseline references to 3.9+

Synchronize stale Python 3.8 references in agent guidance and distro dependency docs/scripts with the current 3.9+ minimum used by project metadata and release docs.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-25 23:31:26 -07:00
Jatin K MalikandGitHub fa17b88f74 chore: bump minimum Python from 3.8 to 3.9 (#336)
* chore: bump minimum Python from 3.8 to 3.9 and adopt PEP 585 builtins

- Update requires-python, classifiers, mypy target, and black target in pyproject.toml
- Replace Python 3.8 with 3.9 in CI matrix and nightly workflow
- Replace typing.List/Dict/Tuple/Set with builtin list/dict/tuple/set (PEP 585)
- Update all 3.8 version references in README, docs, website, and install.sh
- Fix test_wayland_no_tools to mock IBus availability
- Remove List from common_types re-exports and update test

* style: reformat test files for Black 26.x and pin version in CI

Black 26.x introduced parenthesized context manager formatting.
Reformat 7 test files and pin Black>=26,<27 in CI to prevent
future drift from unpinned version upgrades.
2026-03-21 12:15:10 -07:00
Jatin K MalikandGitHub 6bdbb3deb4 fix(install): handle piped bash by redirecting stdin to /dev/tty (#315)
When running via `curl | bash`, stdin (fd 0) is the pipe — not the
terminal. The script was setting INTERACTIVE_MODE='ask' and then trying
to `read` from the pipe, which returned EOF/garbage, defaulting to
interactive mode, which then immediately errored because there was no TTY.

Fix: if stdin is a pipe but /dev/tty is accessible, do `exec < /dev/tty`
to redirect stdin to the real terminal so user prompts work correctly.
If there is truly no terminal (headless/CI), fall back to automatic mode
without erroring.

Also fix the misleading error message which suggested running the exact
same curl|bash command that just failed.
2026-03-14 14:16:17 -07:00
Jatin K MalikandGitHub 81623ca6d3 fix(install): dynamically resolve latest release tag via GitHub API (#313) 2026-03-14 14:01:19 -07:00
Jatin K MalikandGitHub e2fdf5ddf2 fix(clipboard): copy to clipboard after IBus text injection (#311)
The IBus injection path had an early return that skipped the clipboard
copy logic. Store the result, copy to clipboard if enabled, then return.

Also add xclip and wl-clipboard to install.sh for all supported distros
so clipboard tools are available out of the box.
2026-03-14 13:37:04 -07:00
Jatin K MalikandGitHub 0469defae6 chore(release): prepare v0.8.0-beta (#280)
* chore(release): prepare v0.8.0-beta

* docs(web): align release messaging with v0.8.0-beta features

* docs(web): tighten homepage messaging for v0.8.0-beta
2026-02-28 20:58:55 -08:00
Jatin K MalikandGitHub 6e02e928ae chore(release): prepare v0.7.0-beta (#255)
* chore(release): prepare v0.7.0-beta

## Version Bump (v0.6.3-beta → v0.7.0-beta)

This is a major beta release with significant new features including autostart support, tabbed settings, and improved GPU detection.

### Core Version Files
- Updated src/vocalinux/version.py to 0.7.0-beta
- Updated pyproject.toml classifiers (already correct - Beta)

### Documentation
- Updated README.md with new features, bug fixes, and version references
- Updated docs/INSTALL.md with v0.7.0-beta install commands
- Updated docs/UPDATE.md with comprehensive 'What's New' section
- Updated SECURITY.md supported versions table (0.7.x now supported)

### Website
- Updated web/src/app/page.tsx version badge (v0.7.0 Beta)
- Updated web/src/app/page.tsx softwareVersion schema
- Updated web/package.json version to 0.7.0
- Updated web/src/app/changelog/page.tsx with v0.7.0-beta release entry

### Other Updates
- Updated install.sh default tag to v0.7.0-beta
- Updated src/vocalinux/main.py error message install command
- Updated AGENTS.md branch naming example

## Changes Since v0.6.3-beta

### New Features (13 commits)
- Autostart on login support via XDG autostart
- Tabbed settings dialog with 5 organized tabs
- Intel GPU compatibility detection with automatic CPU fallback
- Single instance prevention
- Evdev device management to prevent CPU spin
- Improved GPU detection avoiding false positives
- IBus fallback when daemon not running
- Fedora dnf check-update fix
- Web SEO - 7 new optimized pages

### Contributors
- Jatin K Malik (maintainer)
- Antoine Monnet (contributor)
- jatinkrmalik (contributor)

### Bug Fixes
- #254: Improved GPU detection to avoid false positives
- #251: Skip IBus setup when daemon not running
- #243: Prevent dnf check-update from exiting script on Fedora
- #240: Raise exception on IBus setup failure
- #234: Removed leading space from first speech transcription
- #242/#253: Removed disconnected evdev devices to prevent CPU spin

## Verification
- [x] Lint passed
- [x] Black formatting passed
- [x] isort check passed
- [x] Version imports correctly as 0.7.0-beta

* docs: update version history in RELEASE_PROCESS.md

Add entries for 0.6.0-beta through 0.7.0-beta to the version history table.
2026-02-22 21:30:05 -08:00
Jatin K MalikandGitHub 8d898de5e8 fix(installer): improve GPU detection to avoid false positives (#254)
* fix(installer): improve GPU detection to avoid false positives

- Filter out software renderers (llvmpipe, swiftshader, lavapipe, etc.)
  when checking Vulkan compatibility - these are CPU implementations
  and should not affect hardware GPU detection
- Skip Vulkan compatibility check for NVIDIA GPUs since they use CUDA
  instead of Vulkan for acceleration
- Reorder backend priority: CUDA > Vulkan > CPU
- Only show incompatible GPU warning for actual Vulkan-based GPUs
  (AMD/Intel) that lack VK_KHR_16bit_storage support

Fixes issue where RTX 5080 + llvmpipe combination was incorrectly
flagged as incompatible due to llvmpipe lacking 16-bit storage support.

* fix(installer): recommend GPU even without dev libraries installed

The installer installs dev libraries (libvulkan-dev, glslc) in
install_system_dependencies(), which runs AFTER the backend selection.
So we should recommend GPU if there's a compatible GPU regardless
of whether dev libraries are currently installed.

Changes:
- NVIDIA GPU detected → recommend CUDA (even without nvcc)
- Vulkan-compatible GPU → recommend Vulkan (even without libvulkan-dev)
- Only fall back to CPU if no compatible GPU detected or GPU is
  genuinely incompatible (old Intel Gen7)
2026-02-22 21:03:54 -08:00
Jatin K MalikandGitHub 58b635b8b9 feat: detect incompatible Intel GPUs and fallback to CPU (#239)
* feat: detect incompatible Intel GPUs and fallback to CPU

Implements two-layer protection for users with older Intel GPUs that lack
VK_KHR_16bit_storage support required by whisper.cpp Vulkan backend.

Affected GPUs: Intel Gen7 and older (Ivy Bridge, Haswell, Sandy Bridge)
- Intel HD Graphics 2500/4000 (Ivy Bridge)
- Intel HD Graphics 4400/4600 (Haswell)

Layer 1: Install-Time Detection (Prevention)
- New check_vulkan_gpu_compatibility() function in install.sh
- Parses GPU name from vulkaninfo output
- Checks against known incompatible patterns
- Shows clear warning and recommends CPU backend
- Updates detect_whispercpp_backends() to skip Vulkan for incompatible GPUs

Layer 2: Runtime Fallback (Safety Net)
- Added error handling in recognition_manager.py _init_whispercpp()
- Catches RuntimeError with "16-bit storage" or "Unsupported device"
- Automatically disables Vulkan/CUDA and retries with CPU
- Shows desktop notification explaining the fallback

Fixes #238

* fix(gpu): improve compatibility detection and add tests
2026-02-22 20:29:06 -08:00
Jatin K MalikandGitHub 9886e8cb9d feat(ui): add auto-start on boot support (#207)
* feat(ui): add auto-start on boot support

- Add --start-minimized CLI flag for autostart entry
- Create autostart_manager.py to handle XDG autostart desktop entry
- Add autostart config option in config_manager.py
- Add Start on Login toggle in Settings dialog
- Add Start Minimized toggle in Settings dialog
- Add Start on Login checkbox in system tray menu
- Add first-run dialog to ask about autostart preference

Closes #184

* fix(test): add first_run: False to mock config settings

The tests were failing with StopIteration because main() now checks
for first_run and shows a dialog if True. By setting first_run: False
in the mock config, we skip the first-run dialog code path.

* fix(ui): harden autostart behavior across Linux sessions

Use XDG-compliant autostart paths and robust Exec command resolution, keep first-run prompt semantics correct, and fix GTK toggle handling so state changes are applied reliably.

* docs(autostart): document XDG startup model and limits

Clarify that Vocalinux uses desktop-session XDG autostart, not systemd services, and explain compatibility expectations for mainstream DEs versus minimal/custom WM sessions.

* feat(ui): add first-run quick-start guidance text

Expand the welcome dialog with concise first-time usage guidance and clear direction to the tray menu Settings entry while preserving the existing autostart decision flow.

* fix(uninstall): remove XDG autostart desktop entry

Ensure uninstall removes ~/.config autostart registration and reports leftover autostart entries during verification.

* fix(install): ignore defunct processes in running-check

Filter installer process detection to exclude defunct entries and installer/uninstaller shells so stale zombie rows do not block reinstall.

* fix(uninstall): ignore defunct processes in running-check

Use filtered process detection so zombie entries and installer shells do not trigger false running-process warnings during uninstall.
2026-02-21 15:06:20 -08:00
f3bcffad60 fix(install): prevent dnf check-update from exiting script on Fedora (#243)
dnf check-update returns exit code 100 when system updates are
available, which causes the script to exit immediately due to set -e.
Added '|| true' to prevent this non-error condition from stopping
the installer.

Fixes #34 (Fedora installation failure)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-21 11:17:29 -08:00
Jatin K MalikandGitHub d7d91a7db2 chore(release): prepare v0.6.3-beta (#235)
* chore(release): prepare v0.6.3-beta

- Fix #220: Installer default tag now points to correct version
- Update version from 0.6.2-beta to 0.6.3-beta
- Update documentation version references
- Update website schema and badges
- Add v0.6.3-beta changelog entry

The v0.6.2-beta installer was defaulting to v0.6.1-beta which is
missing the psutil dependency, causing fresh installs to fail.

* docs(update): simplify update guide, remove duplicate version history
2026-02-18 23:07:00 -08:00
Jatin K MalikandGitHub ca6ba56318 feat(scripts): add process check and interactive prompts (#230)
install.sh:
- Add check_running_processes() function to detect running Vocalinux
- Prompt user to kill processes before proceeding with installation
- Auto-kill in non-interactive mode

uninstall.sh:
- Add confirmation prompt before killing processes
- Add interactive prompt for keeping data (models, config)
- Simplified --keep-data flag behavior (also keeps config)

This prevents issues during install/upgrade when Vocalinux is
still running, and gives users more control over data preservation.
2026-02-18 22:48:49 -08:00
Jatin K MalikandGitHub 68062713cf fix(install): remove premature pkg-config check (#228)
pkg-config is included in the package list to install, so the script
should not exit with an error if it's not already installed. The check
was preventing installation on fresh systems like WSL Ubuntu 24.04.

The script already has proper fallbacks for when pkg-config is not
available (detect_vulkan and detect_typelib_path both check before
calling pkg-config).
2026-02-17 23:06:41 -08:00
a1725f7cfc feat(text-injection): add IBus engine for Wayland text injection (#211)
* feat(text-injection): add IBus engine for Wayland text injection

Add a custom IBus input method engine for reliable text injection on
Wayland, solving keyboard layout issues with xdotool/wtype.

Key features:
- IBus proxy engine that passes through normal keyboard input
- Text injection via Unix socket IPC
- User-level component installation (no sudo required)
- Auto-reactivation when user switches keyboard layouts
- Graceful fallback when IBus is unavailable

Changes:
- New ibus_engine.py with VocalinuxEngine and IBusTextInjector
- TextInjector now prefers IBus on Wayland for best compatibility
- Add IBus GObject bindings to system dependencies in install.sh
- Add comprehensive tests for IBus engine

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* security: secure IBus socket with permissions and peer verification

- Set directory to 0o700 (owner-only access)
- Set socket to 0o600 (owner-only read/write)
- Add SO_PEERCRED verification to reject connections from other users

This provides defense-in-depth: even if directory permissions are
bypassed, the socket server verifies the connecting process UID.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address PR review feedback for IBus text injection

- Fix infinite loop in socket server by adding _server_running flag
- Use sys.executable instead of hardcoded python3 for venv compatibility
  - Also inherit environment (env=os.environ.copy()) for venv deps
  - Fix component XML to use sys.executable in <exec> tag
- Replace unsafe pgrep with PID file tracking and /proc verification
- Integrate TextInjector.stop() in TrayIndicator._quit() for cleanup
- Remove unused is_engine_registered import
- Fix redundant hasattr check to use proper None comparison
- Auto-detect and update stale IBus component XML on startup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: AntoineMontane <monnet.antoine@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-17 22:15:06 -08:00