Commit Graph
207 Commits
Author SHA1 Message Date
Palash Debnath 04410a458d Release VoiceStudio 5.0.0 (#1487)
Complete the VoiceStudio identity, release documentation, assets, version mirrors, and safe cross-platform development startup.
2026-08-11 04:05:45 +00:00
Palash Debnath 95a35b8e07 feat(indextts): add native IndexTTS 2.5 support (#1485)
* feat(indextts): add native 2.5 sidecar support

* fix(indextts): preserve legacy language metadata

* docs(indextts): state model license terms accurately

* fix: preserve IndexTTS upgrades and duration controls

* fix: complete IndexTTS upgrade safeguards
2026-08-11 01:26:25 +00:00
Palash Debnath 7cde2fcd06 fix(desktop): make recording and dubbing reliable (#1481)
* fix(ui): keep scaled desktop shell responsive

* fix(linux): support desktop microphone capture

* fix(ui): update the centered VoiceStudio brand

* fix(audio): fall back when recorder start is unsupported

* fix(desktop): use the app header as titlebar

* feat(audio): add live microphone input controls

* fix(dub): recover from missing transcription models

* fix(dub): make pipeline stages actionable

* fix(asr): recover low-memory transcription

* docs: record desktop reliability fixes

* fix(ui): use semantic error banner border

* fix(dub): harden recovery and recording fallbacks
2026-08-10 22:43:37 +00:00
debpalash f21b76350b Merge remote-tracking branch 'origin/main' into codex/pr1442 2026-08-10 10:41:24 +00:00
debpalash 586650e326 Merge remote-tracking branch 'origin/main' into fix/ghas-path-boundary
# Conflicts:
#	CHANGELOG.md
2026-08-10 09:21:17 +00:00
debpalash c1793dac41 Merge remote-tracking branch 'origin/main' into codex/pr1442 2026-08-10 09:20:35 +00:00
debpalash 566f822260 Merge remote-tracking branch 'origin/main' into fix/ghas-path-boundary
# Conflicts:
#	CHANGELOG.md
2026-08-10 08:10:23 +00:00
debpalash 252c6fd149 Merge remote-tracking branch 'origin/main' into codex/pr1442 2026-08-10 08:09:29 +00:00
debpalash 820817ad68 Merge remote-tracking branch 'origin/main' into codex/pr-1464
# Conflicts:
#	CHANGELOG.md
2026-08-10 08:04:56 +00:00
debpalash aeda504a03 fix(security): authorize export filesystem sinks 2026-08-10 07:32:50 +00:00
debpalash 501450e8ec Merge remote-tracking branch 'origin/main' into codex/pr1442 2026-08-10 07:21:53 +00:00
debpalash 4259806d02 Merge remote-tracking branch 'origin/main' into codex/pr-1464
# Conflicts:
#	CHANGELOG.md
2026-08-10 07:04:10 +00:00
debpalash fe29fe0966 Merge remote-tracking branch 'origin/main' into fix/ghas-hf-revisions
# Conflicts:
#	CHANGELOG.md
2026-08-10 07:01:42 +00:00
debpalash 4d2595eb00 Merge remote-tracking branch 'origin/main' into fix/ghas-path-boundary 2026-08-10 06:28:06 +00:00
debpalash 9ee30766db Merge remote-tracking branch 'origin/main' into fix/ghas-path-boundary
# Conflicts:
#	CHANGELOG.md
2026-08-10 05:52:12 +00:00
debpalash 1a71fbea72 Merge remote-tracking branch 'origin/main' into fix/1274-docker-python-runtime 2026-08-10 05:52:09 +00:00
debpalash f8f992ed9c Merge main and address Docker review findings 2026-08-10 04:53:37 +00:00
debpalash c2b9951f28 Merge remote-tracking branch 'origin/main' into fix/ghas-path-boundary
# Conflicts:
#	CHANGELOG.md
#	backend/core/path_authorization.py
#	frontend/src-tauri/src/commands.rs
#	tests/test_network_share.py
2026-08-10 04:32:23 +00:00
debpalash 3b808ed20d fix: harden cookie import lifecycle 2026-08-10 04:01:44 +00:00
debpalash 25c84da606 Merge remote-tracking branch 'origin/main' into fix/1274-docker-python-runtime
# Conflicts:
#	CHANGELOG.md
2026-08-10 03:51:12 +00:00
debpalash e6b68aaf11 Merge remote-tracking branch 'origin/main' into fix/ghas-hf-revisions
# Conflicts:
#	CHANGELOG.md
2026-08-10 03:50:11 +00:00
debpalash 74e6d4582b fix(dub): keep cookie credentials off remote plaintext HTTP 2026-08-10 03:16:35 +00:00
debpalash bfbf78a270 Merge remote-tracking branch 'origin/main' into fix/1429-youtube-cookie-file
# Conflicts:
#	CHANGELOG.md
2026-08-10 03:15:59 +00:00
debpalash 5be4a26903 fix(appimage): ship the compatibility launcher 2026-08-09 23:12:04 +00:00
Palash Debnath ae46f187d0 fix(security): move host-path authority into the desktop shell (#1448)
Close unauthenticated remote mutation paths and keep filesystem destinations behind one-shot Tauri capabilities. CodeRabbit and Greptile findings were fixed on-branch; all review threads are resolved. Full CI, Security, Rust, and cross-platform smoke checks are green.
2026-08-09 21:55:43 +00:00
debpalash 74511db595 fix(security): redact share PIN from remote discovery 2026-08-09 21:30:02 +00:00
debpalash fa3ba2f366 fix(security): authorize host paths through native IPC 2026-08-09 21:23:00 +00:00
debpalash 7abc07e7f3 fix(security): keep host paths desktop-only 2026-08-09 21:10:51 +00:00
debpalash f6f2bc5dcd fix(security): pin curated Hugging Face revisions 2026-08-09 21:07:01 +00:00
debpalash 9a770b15d1 test(pockettts): verify pinned install on four platforms 2026-08-09 21:02:30 +00:00
debpalash f8eeb5a963 fix(security): require API key for remote admin writes 2026-08-09 21:01:14 +00:00
debpalash 034dd2333d fix(pockettts): complete first-use terms gate and smoke 2026-08-09 20:55:53 +00:00
debpalash b1b18dd1ba fix(dub): support explicit YouTube cookie exports (#1429) 2026-08-09 20:42:47 +00:00
debpalash b991e59a0f fix(docker): keep ROCm runtime on guarded Python 2026-08-09 20:39:46 +00:00
Palash Debnath 1eb59c6f18 feat(diagnostics): say why a GPU host fell back to CPU (#1274) (#1425)
The About page reported 'Compute device: cpu / GPU active: no / VRAM 0.00 GB' on a machine with a working GPU. Every line was true and none was usable — it is also exactly what a machine with no GPU at all reports, so the report could not distinguish a driver that isn't loaded from a container that cannot open the device from a ROCm older than the card.

The probe already knew all of it; torch.cuda.is_available() returning False simply produced no note. Each cause now reads differently: a missing device node names the --device flags, a permissions failure names --group-add and how to find the host's real render/video GIDs (copied numbers are the most common way this ends up on CPU in Docker), a card newer than the shipped ROCm points at rocminfo, an HSA_OVERRIDE_GFX_VERSION that is doing more harm than good is named first because it is both likelier and cheaper to test, and an unreachable NVIDIA driver gets its own advice.

The probe never diagnoses from a measurement it did not complete: when torch.cuda.is_available() itself raises, the exception is reported and no device findings are asserted beside it. Metadata access that raises is contained too — this runs on the path whose whole job is to explain a failure, so it cannot become one.
2026-08-08 15:23:38 +05:30
Palash Debnath bcb547b9f2 fix(engines): a slow venv probe is not a broken venv (#1414) (#1421)
Every subprocess engine confirms a candidate interpreter by spawning it and importing the engine package. For IndexTTS that is 'import indextts.infer_v2', which pulls in torch and transformers — seconds with a warm page cache, tens of seconds on a first run, a spinning disk, a network share, or Windows with real-time AV scanning every DLL.

The bound was 10s (15s for three peers), and elapsing it was treated as a negative: the candidate was discarded exactly as if the import had raised. A working OMNIVOICE_INDEXTTS_DIR install was reported as 'IndexTTS-2 is not installed', or fell through into the lazy bootstrap and reinstalled over a working clone. Only successful resolution was memoised, so every retry re-ran the probe and failed identically — which is why all three reported repro paths look like one bug.

A timeout is the absence of evidence, not evidence of breakage. The probe is now tri-state: yes (imported), no (ran and failed), unproven (did not finish). An unproven candidate is kept as a fallback and used only after every candidate has had its chance, so a wedged user clone cannot shadow a healthy bootstrapped venv. If an unproven venv really is broken it now fails at the sidecar handshake with a real error rather than a confident lie about the install.

Fixed as a class: backend/engines/_venv_probe.py replaces the drifted copy in each of the four bootstraps, and the bound is tunable per engine, defaulting to 60s. Zero and negative values are ignored — an unbounded probe would let one wedged candidate hang engine resolution forever.

Reported with a precise root cause by @OracleNightmare. (#1414)
2026-08-08 05:27:47 +05:30
Palash Debnath 46c47b68ce fix: restore generation and startup broken by the rename (#1417) (#1420)
Two bugs from the v0.4.2 rename sweep:

- The lazy model import was rewritten to `from omnivoice.models.omnivoice import VoiceStudio`, a class the library does not export. ImportError is not ModuleNotFoundError, so the #564 source fallback never caught it and /generate 500'd on every default-engine request. The class keeps its library name — it is a checkpoint-referenced identifier, not branding.
- alembic resolved a bare relative script_location against the process cwd, and the desktop shell launches the backend from frontend/src-tauri, so a pending migration killed startup. script_location and prepend_sys_path are now anchored with %(here)s, with path_separator = os so Windows drive letters and paths with spaces survive. alembic floor raised to >=1.16.

Regression tests verified fail-before/pass-after for both.
2026-08-08 02:40:52 +05:30
Palash Debnath 0c82167839 feat(firstrun): let the user choose the portable folder (#766)
Portable mode put everything in OmniVoiceStudio-Data beside the app — the only storage row on the setup screen you could look at but not change, while installed mode had a picker for all three of its directories.

The pin had a reason: portable_base() is computed from the executable's location and never read from config, which is what makes a portable install self-discovering. A user-chosen folder breaks that, because the only record of the location would live inside the folder being located.

So it is recorded somewhere findable, in order: a portable.path marker beside the app; then portableDir in the per-user config for app folders that are read-only; then the historical default, so existing portable installs resolve byte-identically.

The marker stores a RELATIVE path whenever the folder sits inside the app's own directory — the USB-stick case portable mode exists for, where app and folder move as a unit and the mount path is free to change. Anywhere else only an absolute path can be stored, and the setup screen says the install is tied to it rather than promising portability it cannot keep.

Two traps worth naming, both caught in review: the per-user fallback would recurse forever through load_config (config_path to portable_config_file to portable_base), so it reads the platform config file directly; and clear_portable_dir was reading through that same chain, silently failing to clear the machine record so the old folder kept winning after the user chose the default.

This also retires the greyed-out Portable option after a default Program Files install (#766) — you point it at a writable disk and get the machine-bound variant.

Rust 109 passed, frontend 1705 passed. All 21 locales in lockstep; docs/install/windows.md rewritten.
2026-08-07 07:52:05 +05:30
Palash Debnath 93025d9a81 fix(dictation): stop the widget stranding an empty square, repair the swept data dirs (#1398)
The dictation hotkey could leave a blank dark square stuck on the desktop with no way to dismiss it. Three defects compounded: the tray listener's effect depended on [state], so it detached across an await on every state change and a press landing in that gap was lost; an idle pill renders null, so the window Rust had already shown was empty; and the opaque chrome background made that empty window a hard-edged square. Nothing could hide it — dismiss() is only reachable from the X button, Esc, or a post-session timer, none of which exist for a session that never started.

Fixed at the invariant rather than the call sites: the listener subscribes once for the component's lifetime, the widget window's chrome background is transparent, and an idle-but-visible window reconciles itself to hidden. The reconcile is polled (a dropped press changes no React state, so there is nothing to key an effect off) and aborts if its effect is torn down mid-check, so it can never hide a dictation that has just started.

Also in scope:

- The rename sweep had repointed three data-dir literals at a brand-named directory that does not exist, so smoke-test.sh verified a directory the backend never writes and desktop-prod.sh silently stopped clearing backend state on Windows. Both invisible on macOS, where they are usually run. A guard test now pins the assignments specifically.
- The dictation model picker's download sizes were wrong for all seven models, in both directions — Parakeet TDT v3 (the recommended default) understated 180 MB against an actual 670 MB, while the low-RAM fallbacks were overstated threefold, discouraging exactly the choice that would have helped. Measured from the published repos and pinned by a test.
- The 0.6B Parakeet models now decode on more threads, capped by host cores and still overridable.
- uninstall.ps1 gained a UTF-8 BOM (Windows PowerShell 5.1 mis-decodes its non-ASCII output without one), and sponsor.yml lost its last OmniVoice references.
2026-08-07 05:39:41 +05:30
Palash Debnath 74796bb2ab chore(agents): record issue tracker, triage labels and domain-doc layout (#1400)
Scaffolds the per-repo agent configuration the engineering skills assume: where issues live (GitHub Issues via the gh CLI), the five canonical triage labels, and the single-context domain-doc layout. Adds docs/agents/{issue-tracker,triage-labels,domain}.md and an '## Agent skills' block in both CLAUDE.md and AGENTS.md so the two stay in sync.

Documentation only — no runtime code is touched.

Review fixes: CONTEXT.md is now described as 'read it when it exists' rather than stating its absence as permanent, and both directory-tree fences carry a language identifier (markdownlint MD040).
2026-08-07 05:35:09 +05:30
Palash Debnath 43d3537adf fix(asr): stop a missing cuDNN 8 from killing the backend process (#1371)
CTranslate2 (the engine under WhisperX and faster-whisper) requires cuDNN 8 while torch ships cuDNN 9. When the side-loaded cuDNN 8 is absent it does not raise — it __fastfail()s, killing the whole backend with 0xC0000409 and no traceback, so the shell restarts it and the next attempt dies the same way.

The defect was that the backend computed the answer and discarded it: the preload passed silently on a missing directory and on every OSError, then called into a library that treats the same condition as fatal. The answer is now kept, and the CTranslate2 engines report themselves unavailable so auto-detect falls through to pytorch-whisper.

Two more instances of the same class went with it: the crash-isolated ASR sidecar is a child process that never preloaded at all (failing every transcribe, quietly), and the preload only searched <project root>/.venv, missing any other interpreter.

Conservative by design — a false positive costs WhisperX's forced alignment, so ROCm is excluded via torch.version.hip before cuda.is_available(), which is True on HIP builds.

14 regression tests; backend suite 4374 passed.
2026-08-07 05:24:05 +05:30
Palash Debnath 5cab8e0149 feat: rename the product to VoiceStudio (previously OmniVoice-Studio)
Renames what users see. The app, the installers, the window title, the
docs and all 21 locales now say VoiceStudio, with "(previously
OmniVoice-Studio)" noted near the title of each doc surface so people
recognise it.

Deliberately NOT renamed, because renaming any of them silently breaks
an existing install — there is no legacy-path fallback anywhere in this
codebase:

  - bundle identifier com.debpalash.omnivoice-studio (MSI UpgradeCode,
    macOS TCC grants, managed venv, WebView localStorage, the
    single-instance lock)
  - data directories OmniVoice / .omnivoice and omnivoice.db
  - the ~150 OMNIVOICE_* environment variables
  - the X-OmniVoice-* HTTP headers (a wire protocol)
  - the published Docker image paths
  - the OmniVoice ENGINE, which is a model name and not this product

tests/test_identity_paths_survive_the_rename.py pins every one of those
so a future well-meaning sweep cannot orphan a user's library.

Linux .deb users install a new package name and should apt remove
omnivoice-studio; that note is in the changelog.
2026-08-07 01:30:58 +05:30
Palash Debnath a23e69d014 chore: point every repo reference at github.com/debpalash/VoiceStudio (#1394)
The repository was renamed. 724 references across 59 files now point at the new URL — README badges, docs, install guides, the updater's releases API call, CONTRIBUTING, the Colab link and the probe harness. GitHub redirects the old URLs, so nothing was broken in the meantime.

Deliberately NOT renamed, because each breaks something on a user's machine: the Tauri bundle identifier (the path to every existing user's data), /usr/lib/omnivoice-studio and the compose container names, and the published Docker image paths.

The image path needed a code change to STAY still: docker.yml derived it from github.repository, so the next build would have published to ghcr.io/debpalash/voicestudio while Docker Hub, a hardcoded literal, stayed put — everyone pulling the documented GHCR path would have kept receiving the last pre-rename image forever. It is now pinned, with a test that fails if it ever derives from the repo name again.

Also makes the probe's repo-name assertion shape-based: it hardcoded the old name and failed on every PR after the rename while the code it tests worked perfectly.
2026-08-06 18:15:56 +05:30
Palash Debnath eeffe6c2d1 fix(gguf): a source-built runtime must actually run (#1348) (#1384)
A meticulous report from an LXC/CPU-only source install surfaced three real defects: the build script deleted the libggml shared libraries a dynamically-linked build needs (first spawn died with exit 127), the hardcoded 120s per-spawn kill switch reaped legitimate CPU-only renders, and OMNIVOICE_ALLOWED_ORIGINS — the only fix for cross-origin browser access — was documented nowhere.

All platform branches of scripts/build-omnivoice-tts.sh now copy the shared libs next to the binary, the CI artifact glob uploads them, and the backend puts bin/ on the loader path for every spawn of the engine binary. The timeout defaults to 600s (above the pool guard's well-diagnosed 300s deadline), is tunable via OMNIVOICE_GGUF_GENERATE_TIMEOUT_S with non-finite values rejected, and the timeout error names the knob. CORS documented in api-auth.md with a pointer from remote-gpu.md. Regression tests pin the spawn-env rule, the per-branch copy rule, the artifact glob, and the timeout behavior.
2026-08-06 04:32:40 +05:30
Palash Debnath d23e56a5ec fix(errors): the transformers-import advice names torchvision now (#1376) (#1377)
The TRANSFORMERS_IMPORT hint and the ASR pipeline error told users to reinstall torch + torchaudio + transformers. torchvision — the package whose ABI mismatch actually produces this exact lazy-import wording (#1357's torchvision::nms, wrapped into "Could not import module 'AutoFeatureExtractor'") — was the one package the advice omitted. Following it to the letter left the broken package untouched (#1376).

Both surfaces now name the mismatch as a cause and prescribe the pinned reinstall with literal versions (desktop installs ship no deploy/, so the constraint-file form fails there) targeting the venv explicitly. A lockstep test asserts the exact command on every advice surface against deploy/torch-constraints.txt, so a pin bump stays red until the advice matches. Docs gain the same-wording-different-cause section (1a-bis).
2026-08-05 17:39:19 +05:30
acd36badee feat(engines): PocketTTS CPU-only sidecar shape (#1306) (#1328)
* feat(engines): PocketTTS CPU-only sidecar shape (#1306)

Sidecar SHAPE for review, mirroring omnivoice-subprocess: PocketTTSBackend(SubprocessBackend) (CPU-only, parent interpreter, optional-dep gate) plus a stdio sidecar (ready/ping/synthesize/shutdown, lazy TTSModel.load_model, per-ref voice cache, generate_audio to int16 PCM). Registered in services/tts_backend.py. Batch protocol; streaming raised as a follow-up. CI smoke, gated-weights preflight, 4-platform install, licence-accept gate deferred to on-top after shape review.

* feat(engines): PocketTTS sidecar handles 6 languages (en/fr/de/pt/it/es)

load_model(language=...) per language (cached), maps OmniVoice's language value to a pocket-tts model language, and picks the default preset voice per language when no ref clip is given. Represents PocketTTS accurately: it is multilingual, not english-only. The HF model card's 'English only' line is stale, confirmed by the GitHub README and pocket-tts 2.1.0.

* fix(engines): list pockettts in docs inventory; drop unused logger

docs/features.yaml tts_engines now includes pockettts, clearing the docs-drift test that failed CI (every registered engine must be in the inventory). Removed the unused logger line CodeQL flagged. No readme/doc entry yet, matching opt-in engines like supertonic3 and omnivoice-gguf; a doc page can land with the rest of the integration.

* fix(engines): address PocketTTS sidecar review findings

- Cold-load watchdog: heartbeat progress frames during the gated weights download so the parent does not kill a healthy sidecar mid-load, plus a 600s recv timeout on the backend.
- Unsupported language: raise a clear error instead of silently falling back to English and mispronouncing.
- Voice-state cache: LRU-bounded to 8 entries so a long session cannot leak memory.
- ref_audio SSRF: reject URLs (local file paths only) to preserve local-first.

Addresses the 3 Greptile P1 + 1 CodeRabbit Major on #1328.

* fix(engines): invalidate voice cache on ref-file change; reject non-finite recv timeout

- Voice-state cache key now folds the ref_audio file mtime+size, so a file replaced at the same path no longer returns a stale voice from the previous contents (Greptile P1).
- recv_timeout_s rejects inf/nan env values via math.isfinite and falls back to 600s, so the deadline can't be silently disabled (CodeRabbit Major).

* fix(engines): nanosecond mtime in voice cache fingerprint

int(st.st_mtime) lost sub-second precision, so a file replaced at the same path within one second with the same size kept the old key and returned a stale voice. Use st.st_mtime_ns for full resolution (Greptile P1 on the follow-up fix commit).

* fix(engines): raise on multi-channel audio instead of unsafe downmix

The defensive mean(axis=0) assumed channels-first; on channels-last (N,2) it averaged across time, producing garbage. The engine returns mono, so the branch is unreachable in practice. Raise on ndim>1 so an upstream shape change surfaces as a loud error frame instead of silent noise. (debpalash review on #1328)

* fix(engines): include import error in pockettts is_available message

CodeRabbit Minor on #1328: the exception was caught as 'e' but never shown.

* fix(engines): lock _send to prevent concurrent-write framing corruption

Greptile P1 on #1328: the cold-load heartbeat thread and the main loop both call _send (stdout write). The stop+join serializes the normal case, but a join timeout leaves a window where both threads write length+body segments concurrently, interleaving the wire framing. Add a threading.Lock around the write so concurrent _send calls are serialized regardless.

* test(engines): cover the PocketTTS sidecar's silent failure modes

The four review findings fixed on this PR are all silent by construction:
an unsupported language rendered fluent, confident, wrong audio; the
channels-last downmix produced noise; interleaved frames desynchronized
the pipe permanently; a re-recorded clip kept serving the old voice. None
of them raise, and none would be caught by an end-to-end smoke test that
only asserts audio came back.

49 tests over the sidecar's pure logic — language selection, PCM
conversion, wire framing, the LRU voice cache — plus the backend surface
(recv-timeout guards, CPU-only declaration, sample-rate lockstep with the
sidecar, lazy registration). The model is mocked and the sidecar is
stdlib-only at import time, so none of it needs the optional pocket-tts
wheel or a child process.

Verified fail-before/pass-after by reverting the lock and the multi-channel
guard: the framing test fails with a length header decoded from inside
another frame's body, which is the corruption itself rather than a proxy
for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: debpalash <nizam4103@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 00:34:30 +05:30
Palash DebnathandClaude Opus 5 810b598739 fix(appimage): let the host GStreamer win, and stop sharing its registry (#1333) (#1354)
* fix(appimage): let the host GStreamer win, and stop sharing its registry (#1333)

Recording from the AppImage failed with "No microphone found" on a Debian
13 host whose audio stack the reporter verified healthy (pactl, wpctl,
gst-launch with both pulsesrc and pipewiresrc), while the same build`s raw
binary recorded fine. GST_DEBUG=2 named it:

  WARN GST_REGISTRY gst_registry_binary_check_magic:
    Binary registry magic version is different : 1.23.90 != 1.3.0
  GStreamer element appsink not found. Please install it.

linuxdeploy bundles libgstreamer-1.0 because WebKit links it, but not the
plugins: those are dlopen`d, so nothing static can see them to copy. The
bundled core falls back to the host plugin directory, whose plugins were
built against the host core, the version check rejects them, and the scan
yields nothing. appsink is one of the casualties and it is the element
WebKit hands a capture stream to, so getUserMedia() rejects NotFoundError.

Same class as #1258 (frozen bundled library against a host that moved on)
in a different library, which is why OMNIVOICE_PREFER_SYSTEM_WEBKIT=1 did
nothing for the reporter. Since we ship no plugins, the host core is the
only one that can agree with the plugins that will load — so prefer it,
with OMNIVOICE_PREFER_SYSTEM_GSTREAMER=0 as the escape hatch.

Also isolate the registry cache. GStreamer keys ~/.cache/gstreamer-1.0/
registry.<arch>.bin by architecture alone, so two cores of different
versions clobber each other`s file: that makes the failure depend on which
app ran last, and the AppImage corrupts the cache for every other
GStreamer app on the machine. Both directions go away with a private path.

AppRun.test.sh covers host-present, host-absent and opt-out; all three
fail against the previous AppRun.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(appimage): cover the ldconfig discovery path; docs fixes

CodeRabbit, all three valid:

- every GStreamer case forced ldconfig to fail, so the runtime-only-host
  fallback (no -dev package, hence no .pc file) was never exercised. The
  cases now select their discovery path, and the new ldconfig one fails if
  that branch is removed.
- MD040: the GST_DEBUG fence had no language tag.
- the registry cache path follows XDG_CACHE_HOME when set; ~/.cache is only
  the default. Documented, along with WHY the shared file is a problem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(appimage): compose LD_LIBRARY_PATH once; host WebKit stays first

CI caught a real regression, not a flaky test. The GStreamer block prepended
its own directory, which put it AHEAD of the host WebKit dir — and "host
WebKit first" is the invariant #1258 turns on. On a host where the two
libraries live in different directories that silently changes which WebKit
resolves.

It only showed on Linux because the WebKit ldpath cases do not stub away a
real host GStreamer, so the runner had one to find and macOS did not.
Reproduced locally with an ldconfig shim, and confirmed the ordering is what
fixes it: with the old order the suite is 19/2, with this one 21/0.

Both decisions now compose one path in one place — host WebKit, host
GStreamer, bundle, inherited — so neither preference is weakened and the
ordering is stated where it is applied rather than implied by two
independent prepends. Same directory for both (the common case) is not
listed twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(appimage): preload the host GStreamer instead of hoisting its libdir

greptile P1, valid. The host GStreamer lives in a general system library
directory (/usr/lib/x86_64-linux-gnu on Debian), so putting that directory
ahead of ${HERE}/usr/lib replaced EVERY other bundled library with the
host copy — loader symbol errors, startup crashes, or a blank window on a
distro we never built against. One library needs to come from the host and
the mechanism has to be that narrow.

LD_PRELOAD names exactly that library and leaves the search path alone, so
the WebKit ordering from #1258 is untouched too (and this removes the
composed-LD_LIBRARY_PATH block that only existed to keep the two
prepends from fighting). The preload is inherited by the Python backend,
where nothing links GStreamer and it is inert — the accepted cost.

Tests now assert both halves: the library IS preloaded, and the libdir is
NOT hoisted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(appimage): verify the host GStreamer loads before preloading it

greptile P1, valid. The host core links GLib and the bundle ships GLib too,
resolved bundle-first — so a host GStreamer built against newer GLib than
we bundle fails its relocations and the app does not start at all. That is
strictly worse than the broken microphone this PR fixes. Taking host GLib
as well is not an option either: GLib is what WebKit is built against, so
pulling it from the host reopens #961/#1258.

Rather than predict the pairing, test it. The loader processes LD_PRELOAD
for any binary, so running `true` under the exact environment the app will
get is a complete check of whether the library loads there — a missing
dependency or an unresolved version tag ("version GLIB_2.84 not found")
fails it and nothing else runs. On failure the preload is skipped, the app
starts on the bundled core, and a warning names the mismatch so the user
has a thread to pull rather than a silent half-fix.

OMNIVOICE_APPRUN_PRELOAD_PROBE lets the suite choose the outcome, matching
the existing OMNIVOICE_APPRUN_WK_MARKER precedent; the new case fails if
the guard is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:48:47 +05:30
Palash DebnathandClaude Opus 5 168e8e5c61 fix(macos): declare the floor the app actually delivers (13.3, not 12) (#1314)
* fix(macos): declare the floor the app actually delivers (13.3, not 12)

The app declared minimumSystemVersion 12.0 and the docs promised Monterey,
while the frontend required Safari 16.4 in three independent places: Vite's
default build target (baseline-widely-available = safari16.4), Tailwind v4's own
documented floor, and `@property` throughout its generated utilities. On
Monterey's WKWebView 15.6 the focus ring and accent surfaces resolve invalid,
and a bundled dependency ships a RegExp lookbehind that is a PARSE-time
SyntaxError no polyfill can reach.

Option B — actually supporting 15.6 — means setting build.target back,
replacing 64 color-mix() calls, dropping Tailwind v4 and replacing that
dependency, indefinitely, for an OS that stopped receiving security updates in
late 2024. The council was unanimous on A, and the precedent is uniform (Chrome
117, Electron 27, VS Code, Firefox 116).

minimumSystemVersion is also the guard: macOS itself refuses to launch a bundle
below it, so a Monterey user gets an explicit OS refusal rather than an app that
opens to a blank window — which matters because the Tauri updater has no
per-OS gating of its own.

Docs updated in the same change (README support table, docs/install/macos.md)
and the webCompat floor assertion re-derived to 16.4, so the post-floor API
list must be revisited the next time the floor moves.

Closes #1268

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

* fix(macos): raise the floor in the macOS overlay too, and assert it

Greptile P1, and correct: Tauri merges tauri.macos.conf.json OVER the base
config for a macOS build, and that file carried its own
minimumSystemVersion: 12.0. Changing the base config alone decided nothing —
the shipped bundle would have stayed Monterey-installable while the base
config, the README and the install docs all said 13.3.

Worse, the guard I added read only the base config, so it would have gone on
passing. A test that validates the wrong file is not a guard; it now asserts
both, with a comment saying why the overlay is the one that ships.

Also per review: the changelog entry was an editorial paragraph rather than a
one-line entry, and the section was missing ### Docs. Both fixed.

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

* docs(webcompat): the module header still described the old 12.0 floor

The floor moved to 13.3/Safari 16.4 in this PR and the test was re-derived,
but webCompat.js still told the next reader the oldest supported WebView was
15.6 — which would make every fill here look mandatory instead of retained
for Linux's unpinnable WebKitGTK (CodeRabbit).

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 01:44:41 -07:00
Palash DebnathandClaude Opus 5 26bcd95088 docs(engines): publish the engine-acceptance bar (#1319)
The project carries 14 TTS + 11 ASR engines across 4 platforms with one
maintainer. That breadth is an asset only while every one of them still works;
otherwise it is a pile of support queues, and the first-run promise is what pays
for it.

So: engines are hired for a named job, not added to a list. Documents the job
map (each job has one holder), the seven conditions, the deprecation rule for
engines that lose their steward and their smoke test, and the out-of-tree path.

The point is to make "no" a property of the bar rather than a judgement of the
contributor — and to make "yes" fast when a proposal clears it. #1306 is the
first proposal judged against it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:41:31 -07:00
Palash Debnath 15afc6611d fix(linux): AppImage blank window on Mesa 26.1+ hosts (#1258, #1244) (#1265)
* fix(linux): AppImage blank window on Mesa 26.1+ hosts (#1258, #1244)

The AppImage bundles an Ubuntu-built WebKitGTK but ships no libEGL, so that
bundled WebKit runs against the HOST's Mesa. On Mesa >= 26.1 it calls
eglGetPlatformDisplay() in a way the newer driver rejects and the app dies
before it paints:

    Could not create default EGL display: EGL_BAD_PARAMETER. Aborting...

No environment variable helps, because the failure is in EGL display creation
— before WebKit consults any rendering-path flag. #1258 confirmed
WEBKIT_DISABLE_DMABUF_RENDERER, WEBKIT_DMABUF_RENDERER_FORCE_SHM,
WEBKIT_SKIA_ENABLE_CPU_RENDERING, EGL_PLATFORM=surfaceless and
MESA_LOADER_DRIVER_OVERRIDE=swrast all fail identically.

Chasing the build runner's WebKit (#961 bumped 22.04 -> 24.04) cannot fix this
class: what we bundle is frozen and host Mesa keeps moving. So when the host
has a WebKitGTK at least as new as ours, let it win — the bundle still fills
every gap, and a host without WebKitGTK is untouched. That is exactly why
building from source works on the hardware where the AppImage does not.

The compositing workaround is re-decided against whichever library ends up
running, and AppRun.test.sh — which had never been wired into CI — now runs
there, so this logic stops being a regression test nothing executes.

* fix(review): the ordering change was a no-op; name the host libdir explicitly

CodeRabbit Major — correct, and it made the whole fix inert. LD_LIBRARY_PATH is
searched AHEAD of the linker's default paths no matter where in that variable a
directory sits, so on a normal launch (empty LD_LIBRARY_PATH) the bundle
remained the only explicit search directory and still won. Merely appending it
changed nothing. The host's WebKit libdir is now named explicitly, ahead of
ours. The new tests fail 3/3 against the previous version.

Greptile P1 — a host with the runtime but no -dev package has no .pc file, so
pkg-config can't answer and the check rejected a perfectly good system WebKit.
The libdir probe now falls back to ldconfig, and OMNIVOICE_PREFER_SYSTEM_WEBKIT
gives those users an explicit opt-in (=0 opts out) rather than gambling on an
unverified version, which would risk the #961 regression.

CodeRabbit — my changelog script had also inserted the CI entry into the
published 0.4.0 section. Removed; it belongs only under Unreleased.

CodeRabbit — the docs' source-build fallback used 'cd frontend', not the
repo-root flow the rest of the page documents. Fixed.
2026-07-29 15:36:45 -07:00