Commit Graph
108 Commits
Author SHA1 Message Date
Palash Debnath e2446c3e61 fix(release): keep Preview ahead of Stable (#1763)
Closes #1762.
2026-09-02 07:49:22 +05:30
Palash Debnath 51bbf50ce3 Ship a supported per-user Windows installer (#1730)
Closes #1713

Adds a separately identified per-user MSI and updater channel, non-administrator install/uninstall verification, and fail-closed WebView2 handling for current-user installs.
2026-09-01 22:03:33 +05:30
Palash Debnath e1101255bd fix: make WebView2 MSI bootstrap controllable 2026-08-30 21:25:12 +05:30
Palash Debnath 0d3c81b596 feat(gguf): add Linux ARM64 runtime support (#1641)
Add linux-aarch64 platform detection, Vulkan-preferred source builds with CPU fallback, ARM64-safe PyTorch dependency markers, native artifact CI, regression coverage, and synchronized architecture documentation.
2026-08-23 15:07:52 +05:30
Palash Debnath e38b5f5741 feat(install): binary-first installer with version picking and --source opt-in (#1628)
* feat(install): prebuilt-app installs by default, --source opt-in, --version picker

- install.sh: default mode now downloads the verified release asset
  (dmg/AppImage + SHA256SUMS check) instead of cloning and building;
  --source keeps the previous clone-and-build flow; --version pins a release
- install.ps1: same split — msi download with checksum verification and
  setup wizard by default; -Source (or VOICESTUDIO_INSTALL_MODE) for the
  source build; VOICESTUDIO_VERSION picks a release
- worker landing page documents the modes

* fix(install): CI smoke covers binary + source modes; hdiutil output parse

- drop -quiet from hdiutil attach (it suppresses the mount-point line the
  script parses — caught by the macOS smoke)
- install.ps1 runs msiexec silently under CI, wizard interactively
- smoke verifies binary installs per OS (app bundle / AppImage / MSI
  registry entry) and keeps full source coverage behind --source

* ci(install): check HKCU/WOW6432Node too — Tauri MSI registers per-user

* fix(install): rename $version — collides with bun installer's $Version under iex
2026-08-21 14:30:33 +00:00
Palash Debnath 7640f42dce feat(install): one-command installer URL (.sh + .ps1) + 3-OS install smoke (#1627)
* feat(install): one-command installer URL (.sh + .ps1) + 3-OS install smoke

- scripts/install.ps1: Windows source installer (winget deps, uv, bun,
  clone, uv sync, frontend build); honors OMNIVOICE_PYTHON/OMNIVOICE_REGION
- infra/install-redirect: Cloudflare Worker serving /install with
  User-Agent sniffing (curl -> sh, PowerShell -> ps1, browser -> landing
  page); proxies live from main; /install.sh + /install.ps1 aliases
- scripts/install.sh: fix stale advertised URL (main/install.sh never
  existed) and repo-root resolution so a local run no longer clones a
  duplicate repo into ~/VoiceStudio (verified on macOS arm64)
- .github/workflows/install-smoke.yml: run both installers end-to-end on
  ubuntu/macos/windows when they change
- docs-sync: install one-liners lead each platform guide; STRUCTURE.md

(#1626)

* fix(install): don't let a failed bun download pass silently

curl | sh runs an empty script and exits 0 when the download fails, so
a bun.sh hiccup surfaced much later as 'bun: command not found' (seen
on the macos-latest smoke runner). Fetch to a temp file, verify, fall
back to npm -g bun when node exists, and die with the manual command.
Same post-install verification for uv.

* fix(install): UTF-8 BOM for install.ps1 + quiet-style changelog entry

- tests/scripts/test_uninstall_ping.py requires shipped PowerShell
  scripts with non-ASCII text to carry a UTF-8 BOM (Windows PowerShell
  5.1 mis-decodes otherwise); same treatment uninstall.ps1 already gets
- test_changelog_style caps entries at ~400 chars
2026-08-21 11:47:39 +00:00
Palash DebnathandClaude Fable 5 3d0c9605df test(shell): backend-lifecycle fault-injection harness (#1551)
* test(shell): backend-lifecycle fault-injection harness

Runs spawn_backend_and_wait/supervise_backend against REAL dying child
processes and asserts the user receives the correct NAMED diagnosis —
not merely that recovery happens. Wrong/missing explanation was 61% of
the historical "can't reach the backend" class; this rig is the
permanent regression harness for every future lifecycle fix.

Seam: OMNIVOICE_BACKEND_CMD (JSON argv or whitespace form) runs any
command as "the backend" — venv bootstrap and ffmpeg resolution are
skipped, everything else (err-log run offsets, drainer threads, env
pinning, real OS pipes, spawn-failure diagnostics) stays real. Plus
OMNIVOICE_LOG_DIR (per-test log+marker dirs, also a support tool) and
harness-only timing overrides OMNIVOICE_STARTUP_BUDGET_S /
OMNIVOICE_SUPERVISOR_POLL_MS whose production defaults are pinned by
unit tests. Lifecycle fns genericized over tauri::Runtime for the
MockRuntime app; behavior-neutral with the env unset (unit-pinned).

Scenarios (tests/backend_lifecycle.rs, scenario children = this test
binary re-invoking itself; serial by mutex + CI --test-threads=1):
- port conflict (exit 78) → the detectHints-matchable port phrasing
- generic chained traceback → root cause survives into the diagnosis
  and the crash marker
- spawn failure → spawn diagnostic reaches the user, NO bogus marker
- slow start past budget → timeout names the budget + last stderr
- post-Ready crash loop → 3 restarts announced, markers before restarts,
  "kept crashing" diagnosis naming the last exit
- SIGKILL (unix) → named as signal 9
- deliberate kill → supervisor yields silently, no marker, never Failed
- deferred-startup FATAL → the named step reaches the user, forensics,
  and the splash narration

CI: harness added to the 3-OS tauri-cross-platform matrix.

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

* chore(ci): temporary Windows loader bisect probe for the harness binary

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

* fix(shell): embed Common-Controls v6 manifest into Windows test binaries

Bisected on #1551: EVERY integration-test binary of this crate died at
load on Windows with STATUS_ENTRYPOINT_NOT_FOUND (0xc0000139) — cargo
gives test binaries no manifest, so the loader resolves comctl32 v5,
which lacks the TaskDialogIndirect entry point tauri's dialog/tray stack
imports. build.rs now embeds tests/windows-test.manifest via
rustc-link-arg-tests on Windows targets. Bisect probe removed.

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

* fix(test): scenario gate is PID-valued — the parent can't self-inject

CodeRabbit on #1551: in a parallel local `cargo test`, the parent's own
scenario_child test could observe the armed env and start playing the
backend in-process (binding the port, idling 600s). The gate value is
now the arming process's PID; a matching PID stays inert, so only the
spawned child — a different process — runs the scenario.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 18:45:27 +00:00
Palash DebnathandClaude Fable 5 420bc73e78 fix(release): harden the AppImage repair — pinned tooling, final-writer manifest (#1545)
* fix(release): harden the AppImage repair step

All three review findings on #1544, fixed before the tag re-runs it:
- appimagetool pinned to the immutable 1.9.1 release with a verified
  SHA-256 — a mutable 'continuous' binary must not execute with the
  updater signing key and a release-write token in its environment
- the release tag reaches the script as env data, never interpolated
  into shell source (zizmor template-injection)
- a failed latest.json download now fails the step unless the asset is
  confirmed absent, and the patch refuses to upload unless at least one
  linux signature was actually replaced — a repacked AppImage can never
  ship paired with stale updater metadata

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

* fix(release): the updater manifest gets one final writer, after the matrix

CodeRabbit + Greptile on #1545: every tauri-action leg re-uploads the
shared latest.json, so patching it inside the Linux leg races the other
platforms — a later leg's upload could resurrect the stale pre-repack
signature. The manifest patch moves to a post-matrix job that runs once
after all legs: it aligns the manifest's linux entries with the .sig
asset that actually shipped (self-verifying — no cross-job state), and
no-ops when they already agree. The leg keeps asset repack/re-sign only.

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

* fix(release): a failed .sig download fails the manifest-align job

Same fail-closed rule as the manifest itself: absence is decided by the
asset list; any other download failure must not exit 0 with a stale
signature left in latest.json.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 01:55:10 +00:00
Palash DebnathandClaude Fable 5 fb46fa4788 fix(release): repack the AppImage with a real .DirIcon file, re-sign, re-upload (#1544)
The v0.5.0 tag build — the first real release since #1518's guard — proved
the files-map fix loses: linuxdeploy re-links .DirIcon to an ABSOLUTE
build-machine path after tauri places the real bytes, and the guard
correctly refused to publish. tauri-action's atomic build+sign+upload
leaves only a post-upload seam, so the Linux job now repairs the packed
artifact: extract, replace .DirIcon with the icon bytes as a regular file
(nothing left to dangle), repack with appimagetool, re-sign with the
updater key, clobber the draft release's asset and patch the linux
signature inside latest.json. The existing smoke then validates the
repaired AppImage. No-ops cleanly when .DirIcon already resolves.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 00:59:14 +00:00
Palash DebnathandClaude Fable 5 0ee62b2261 feat(gallery): save gallery voices as profiles, with validated audio references (#1542)
* feat(gallery): save gallery voices as profiles, with validated audio references

Work-in-progress lifted from the concurrent gallery session at the
owner's request (its uncommitted working tree, preserved verbatim from
base 92b1ee5d; safety snapshot remains at rescue/gallery-wip):

- gallery voices can be saved as local profiles: audio is copied into
  the profile store with content-addressed filenames, existing profiles
  are detected and refreshed only when the source clip changed
- backend/core/audio_validation.py: symlink-rejecting, root-contained
  resolution for persisted profile WAV references, with tests
- archetype/community routers and the Voice Gallery UI updated for the
  save-as-profile handoff (spec: docs/specs/longform/26-gallery-use-handoff.md)
- locale updates for the new gallery strings across all 21 files

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

* chore: drop a stray local screenshot script that rode in with the tree copy

* fix(community): explain the tolerated Content-Length parse failure; drop an unused import

CodeQL on #1542: the empty except now says why it is safe (the streamed
byte counter enforces the same cap regardless), and the test file loses
an unused Path import.

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

* fix(gallery): review findings — copy outside the write lock, no stale completions

CodeRabbit on #1542, all findings addressed:
- the profile-audio copy stages to a .part temp BEFORE BEGIN IMMEDIATE
  and publishes via atomic os.replace inside it — other backend writers
  no longer block for the duration of an audio copy; a mid-copy failure
  leaves no temp droppings and no profile row (both pinned by tests)
- VoiceGallery async ops carry per-operation generation tokens: a
  preview or save-as-profile that resolves after unmount (or after a
  newer operation) can no longer play audio, redirect into a workspace,
  or touch state — three fail-before regression tests
- VoiceGalleryActions imports the page at test runtime; the e2e locator
  uses a stable data-testid instead of a translated string; symlink
  tests skip cleanly where the OS can't create symlinks; the changelog
  line carries its PR ref

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

* ci: static ffmpeg fallback when the chocolatey feed is down

Third feed outage to break a PR run (2026-07-20, 2026-07-28, today —
three attempts, three 'installed 0/1'). Chocolatey is a distribution
channel, not the dependency: after the retry loop exhausts, fetch the
static gyan.dev build from its GitHub release mirror and put it on
PATH — same binary, no feed in the path. URL verified live (HTTP 200).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 22:48:05 +00:00
Palash DebnathandClaude Opus 5 41c098e009 feat(demos): ship the demo audio and video the app already advertises (#1517)
* feat(demos): ship the demo audio and video the app already advertises

Every demo asset in the app was a dead link on anything but a Mac.

`personalities.py` has carried a `preview_url` for each of the seven
voice-design presets since they were added; DictationDemo.jsx posts three
bundled WAVs to /transcribe so the feature can be shown without microphone
permission; the Dub workspace reads a manifest and plays a source video plus
four dubbed languages. None of those files were committed, because the tooling
that renders them (scripts/build_demos.sh, scripts/build_dub_demo.sh) hard-
requires macOS `say` — it even carries a `TODO: add espeak-ng path for Linux
contributors`. So the presets returned 404, the replay buttons did nothing, and
the dubbing demo never loaded.

Rendered with VoiceStudio's own engine, which runs wherever the app does:

- 7 voice-design previews (2.2 MB)
- 3 dictation replay clips (1.1 MB) — verified by transcribing them back:
  the conversational and French clips round-trip exactly
- dubbing demo: source + 4 dubbed videos with subtitles and manifest (9.6 MB)

Tooling fixes this turned up:

- build_dub_demo.sh wrote to backend/assets/demo/dubbing, but main.py mounts
  backend/assets/samples at /demo_audio — so the frontend's
  /demo_audio/demo/dubbing/manifest.json could never have resolved even after
  a successful Mac build. Output moved under the mount.
- `say` is now the fallback rather than the requirement: the new
  scripts/render_dub_demo_audio.py renders the five tracks with the engine and
  the shell script picks them up.
- The five demo paragraphs lived in two files. They are now one JSON both read
  — two copies is one edit away from a video whose subtitles disagree with it.
- render_demos_omnivoice.py peak-normalized, which a single-sample transient
  defeats: the Helpdesk preset landed at -30 dB RMS against -17 dB for its
  neighbours, so the preview row played at wildly different volumes. Now EBU
  R128 at -18 LUFS with a -1.5 dBTP ceiling.
- …and pinning the output rate, because loudnorm resamples to 192 kHz
  internally and writes there unless told otherwise, which turned 2.1 MB of
  previews into 17.5 MB of identical-sounding audio.
- update_manifest() looked for a manifest at a path nothing writes, so it
  always printed "not found" and did nothing.
- Dictation is rendered here now too. It was excluded on the grounds that
  `say` was good enough and engine TTS was overkill — true only on macOS.

tests/test_demo_assets_exist.py resolves every advertised URL against the
directory main.py actually mounts, and checks each dubbing subtitle matches the
script its manifest entry claims. A missing static file is not an import error
and not a failing request; nothing would have caught this otherwise.

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

* docs(changelog): stamp the demo-asset entries with their PR ref

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

* fix(demos): watermark rendered demo audio, and harden the render scripts

Review findings on #1517:

- Greptile P1: the renderers wrote engine output straight to disk, so a
  re-render shipped demo audio with no provenance mark. These clips play
  back to users as VoiceStudio output — they are synthetic audio leaving
  the app like any other, and now go through mark_synthetic (#1169), the
  one chokepoint every producing route uses. It runs on the file AFTER
  loudnorm, since loudnorm re-encodes what it is handed, and says so
  loudly when marking is unavailable rather than committing an unmarked
  asset. The dubbing renderer shares the same helper.
- CodeRabbit: build_dub_demo.sh checked only source.src.wav before
  deciding it could run without macOS `say`, so a Linux or Windows run
  with four of five tracks present reached a missing one, called `say`,
  and left a half-built bundle. It now requires all five.
- CodeRabbit: shutil.move over an existing path delegates to os.rename,
  which raises FileExistsError on Windows — os.replace overwrites
  atomically everywhere.
- CodeRabbit: the preview test discovered presets in a parametrize
  argument, importing app code at collection time and leaving
  core.personalities in sys.modules for later tests. Discovery moved into
  the test body.

CI: the rendered dub bundle's zh/ja subtitles, its manifest and the
script source are dubbing CONTENT, not UI strings — allowlisted in
test_no_hardcoded_cjk.py with that justification.

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

* fix(demos): a render that cannot be watermarked fails instead of warning

CodeRabbit and Greptile, #1517: mark_synthetic degrades rather than
raising — correct for generation, wrong for a render script, whose whole
job is to produce files a human then commits. A printed warning on a
scrolling console is not a gate, so both scripts exited 0 with unmarked
assets sitting on disk ready to commit. They now raise, with the reason
and the fix; OMNIVOICE_DEMO_ALLOW_UNMARKED=1 stays for a local listen.

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

* ci: stop a flaky dependency fetch from failing green runs

en-core-web-sm resolves to a direct GitHub release URL, and github.com
intermittently answers `http2 error: refused stream before processing
any application logic`. uv's own three retries all land within the same
few seconds and fail together, so the whole job dies on a dependency
that has nothing to do with the change under test — it cost #1518 and
#1517 an otherwise-green run tonight.

Two changes: back off between whole `uv sync` attempts, which is what
actually clears it, and pass --no-sync to the pytest steps. `uv run`
re-resolves the environment before running, so every test step was a
fresh chance to hit the same fetch even though the install step had
already synced — that is exactly how #1518 failed, in the isolated
backend/tests step, with all 5467 tests already passed.

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

* ci: one retry seam for every uv sync, not just the job that failed last

en-core-web-sm resolves to a direct GitHub *release* URL rather than a
package index, and github.com intermittently answers `http2 error:
refused stream before processing any application logic`. uv's own
retries all land inside the same ~10 seconds and fail together, so a job
dies on a dependency unrelated to the change under test. Tonight that
cost four otherwise-green runs across #1515, #1517 and #1518 — and the
first fix only covered the Tests job, so the next failure simply moved
to Smoke (Linux), which syncs separately.

The fetch is per-job, so the fix has to be per-job: scripts/uv-sync-retry.sh
backs off between whole attempts (15s, 45s, 90s) and every workflow that
syncs now goes through it — ci.yml (tests + the platform matrix),
release.yml, security.yml, evals.yml. It still fails loudly after four
attempts, so a genuinely broken lockfile is not disguised as a flake.

The Tests job also lacked the UV_HTTP_TIMEOUT / UV_HTTP_RETRIES the smoke
matrix has always set, which is part of why it was the one that kept
dying; it has them now.

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

* test(ci): pin the Intel-Mac contract by intent, not by command spelling

test_ci_verifies_intel_mac_as_the_documented_remote_only_host asserted
the literal line `run: uv sync --extra pockettts`, so routing every sync
through scripts/uv-sync-retry.sh read as a broken Intel-Mac contract. The
contract it exists to protect is that the pockettts extra installs ONLY
on backend_supported legs — which the regex now pins, while leaving how
the sync is invoked free to change.

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

* ci: keep every uv run out of the resolver, and bound the retry budget

CodeRabbit, #1517:

- `uv run` re-resolves before running, so the smoke suite, the
  worker-artifact tests, the release test run and the eval run were each
  a fresh chance to hit the flaky direct-URL fetch outside the retry
  loop. All of them pass --no-sync now; the environment is already
  synced by the step that owns the retries. security.yml's
  `uv run --with pip-audit` is deliberately left alone — it layers an
  ephemeral package rather than running the project's own tests.
- The retry count multiplied uv's own budget (UV_HTTP_RETRIES=5 with a
  120 s timeout on the smoke matrix). Three attempts and 60 s of total
  backoff outlast the refusals actually observed while staying well
  inside the jobs' timeout-minutes.
- The Intel-Mac contract test pinned the smoke command literally too, so
  --no-sync tripped it exactly like the sync line did. Same fix: assert
  the contract (smoke runs only on backend_supported legs), not its
  spelling.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 23:29:19 +00:00
debpalash 64973d1829 fix(release): validate wrapped AppImage launcher 2026-08-12 11:24:01 +00:00
velixio 673e544812 Merge origin/main into feat/worker-protocol-v1
Three conflicts, all additive on both sides — resolved by keeping both
rather than choosing, since either side's entries were real shipped work:

  * CHANGELOG.md — remote-GPU entries against branding, IndexTTS 2.5 and
    the recording-input work
  * setup/download.py — the per-target progress reset against main's
    active-install tracking; both belong in the same finally block
  * docs/features.yaml — the remote-worker and model docs against
    docs/branding.md

Backend 5349 passed, frontend 1871 passed. `bun install --frozen-lockfile`
reports no changes, so the Docker build sees the same tree CI does.
2026-08-11 18:36:51 +05:30
velixio aa1d739843 feat(workers): dubbing goes remote, and the protocol stops lying to old workers
The remote-GPU line, verified on hardware rather than asserted.

**Dubbing renders on the worker.** dub_generate.py dispatches the coarse
`dub_segments` operation through the gateway, following the audiobook
pattern: per-unit local fallback after consecutive remote failures, one
aggregated notice rather than one per segment. A 40-minute dub that loses
its worker at segment 200 degrades instead of producing 200 error rows.

**An out-of-date worker is now refused by name.** This was the worst
defect in the plan and it was silent: an un-upgraded worker registered
cleanly, then ignored `inputs` and rendered a clone with NO reference
audio — returned as success. A plausible wrong result with nothing
anywhere to surface it. Workers now declare features, and one missing
them is turned away with the features named and `no task was run`.
Verified live: a worker one commit behind was correctly refused.

**"Offline" and "cannot run this" are different facts.** Asking a live
worker for an engine it lacks answered "is offline or cannot be reached.
Wake the selected worker" — while that worker reported ready, one free
slot and 3.6 ms latency. The user was sent to wake a machine that was
already awake. The scheduler now distinguishes absent from present-but-
incapable, and names the engine rather than the operation, because the
engine is the thing a user can install.

**An engine with no catalog entry is no longer hidden.** A `repo_ids`
non-emptiness check had been implemented as a runtime filter, so a worker
silently refused to advertise any engine lacking a models.yaml entry —
which is four registered engines, including CosyVoice. Users with those
already installed would have lost remote support with only a log line.
Empty `repo_ids` now means "not downloadable here", never "not runnable".

**And a script so this stops being done by hand.**
scripts/verify-remote-worker.sh runs the per-phase acceptance checks
against a live worker, non-destructively. Its preconditions are the
mistakes that cost the most time: exactly one listener on the control
port (two instances silently shared it), and never detecting the worker
with a pgrep pattern that matches the ssh shell running it.

Its first real run found the dubbing picker claiming remote placement.
That turned out to be the CHECK being stale, not the picker — the port
had landed since it was written. It now asserts self-consistency instead:
the picker may claim remote only for an operation the control plane
actually advertises as remotely producible, which cannot rot the next
time an op is ported.

Backend 5291 passed, frontend 1812 passed. Acceptance script: no
automated failures across Phases 4-8 on an RTX 4090. Four checks remain
MANUAL by design — true airplane mode, concurrent downloads, killing a
worker mid-audiobook, and the model-list UI — and are reported as
unverified rather than passed.
2026-08-11 17:39:46 +05:30
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
debpalash 9f2c8ac0a4 fix(pockettts): close license review findings 2026-08-10 09:24:37 +00:00
debpalash c1793dac41 Merge remote-tracking branch 'origin/main' into codex/pr1442 2026-08-10 09:20:35 +00:00
debpalash fb78d87ad9 test(appimage): verify packaged WebKit marker 2026-08-10 04:32:08 +00:00
debpalash 5be4a26903 fix(appimage): ship the compatibility launcher 2026-08-09 23:12:04 +00:00
debpalash c53f1a8d1c fix(ci): keep Intel Mac smoke contract honest 2026-08-09 22:01:38 +00:00
debpalash 9a770b15d1 test(pockettts): verify pinned install on four platforms 2026-08-09 21:02:30 +00:00
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 9877e7c218 fix(ci): bind the preview manifest to its run, not to sibling upload times (#1387)
The nightly preview build had been refusing to publish its own healthy manifest since 2026-08-05 — all four matrix legs green, but the macOS bundles uploaded a few minutes ahead of the slowest versioned artifact, and the freshness check compared the version-less darwin tarballs against their siblings with two minutes of slack. Legs finishing minutes apart is normal, so the comparison itself was wrong, and Preview-channel users quietly stopped getting builds.

The tarballs are now tied to the run that produced them: anything uploaded after this run's first job began executing belongs to it. A concurrency group serializes preview runs so that holds, and the anchor is the earliest job start rather than the run's created_at (which is stamped while a run is still queued, and would let a queued run claim the previous run's uploads). The preview-notes job also gains the actions: read scope its run-metadata lookup needs, with a warning-and-degrade path so a permissions regression cannot take the channel down again.

Regression tests cover the 2026-08-05 shape, the genuinely stale case, clock skew at the boundary, the no-timestamp fallback, per-ref concurrency scoping, and the required permission.
2026-08-06 14:12:44 +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
c117bca09e fix(release): rebuild + cryptographically verify the preview updater manifest (#1327) (#1362)
* fix(release): rebuild + cryptographically verify the preview updater manifest

Since ~2026-07-13 every nightly matrix leg logs 'Signature not found for
the updater JSON. Skipping upload...' - tauri-action uploads the bundles
and .sig companions but never refreshes latest.json. Combined with the
'Clear this arch's stale preview updater bundle' step (which deletes and
replaces the version-less macOS tar.gz every night), the preview
manifest's darwin signatures no longer match the published files: macOS
Preview users hit 'The signature verification failed' on every update
(latest.json frozen at 2026-07-13, tar.gz replaced nightly).

Two changes, both in the single post-matrix preview-notes job (no
per-leg race):

1. Rebuild latest.json from the release's real assets and their .sig
   companions, then clobber-upload. The manifest can no longer drift
   from the files it describes, regardless of what tauri-action's own
   updater-JSON path does or skips.

2. Extend the existing manifest verification with a cryptographic
   check: every signature in latest.json must verify (minisign
   file sig + trusted-comment sig) against the artifact it points at,
   using the updater pubkey from tauri.conf.json. Parity and version
   format both passed for 2+ weeks while every darwin entry was
   unverifiable - this is the check that was missing.

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

* ci(release): refuse a preview manifest built from two different runs

Bot review findings on this branch, all fixed here:

- The AppImage and MSI were picked independently by highest run number and
  the larger N became *the* version, so a matrix where one leg failed or
  was re-run published a manifest advertising X.Y.Z-5 while handing Windows
  users the -4 MSI. That is the same manifest/artifact drift this job
  exists to end, reintroduced by the fix for it. Require both legs to come
  from one run and fail loudly otherwise: leaving the previous manifest in
  place is a visible, already-understood state; shipping a mismatched one
  is not. The darwin tarballs carry no run number, so the signature check
  in the following step is what pins those to the published bytes.
- persist-credentials: false on the checkout — nothing here pushes to git.
- Floor-pin the cryptography install; this step decides whether a signed
  manifest is trustworthy, so it is the one dependency worth a bound.

tests/test_release_preview_manifest_rebuild.py runs the step body extracted
from release.yml against stubbed gh, so it cannot drift from the workflow.
Fails before / passes after on the mismatch case.

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

* docs(changelog): note the preview updater manifest fix (#1327)

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

* ci(release): verify the preview manifest before publishing it, not after

Two more review findings on this branch, both valid, both about the
manifest being wrong in a way the existing checks structurally cannot see.

greptile P1 — verification ran AFTER the clobber-upload. A manifest that
failed the check was already live and stayed served; the job merely went
red, and every macOS Preview user stayed broken until someone noticed.
Verification now runs against the file about to be published, and the
upload is the last thing in the step. A refusal leaves the previous
manifest in place, which is a visible, already-understood state.

CodeRabbit — the darwin entries were not tied to this run. The version
comes from the AppImage name; the macOS tarballs were only checked for
existence. Signature verification cannot help there, because a stale
tarball and its stale .sig match each other perfectly — so a run whose
macOS legs never uploaded would advertise this version while serving Mac
users the previous build, and since those clients keep reporting the old
version the updater would re-offer it forever. They are now bound by
upload time, with two minutes of slack for legs that finish apart.

The selection rules move out of the YAML heredoc into
scripts/build_preview_manifest.py. Three findings in a row have been about
WHICH artifacts may be described together, and a heredoc can only be
tested by extracting it and stubbing a shell — which is what the previous
test file did, asserting against gh stubs rather than against the rules.
build_manifest is pure: assets in, manifest out, ManifestRefused on
anything it will not describe.

14 tests, including both new refusals and two that pin the workflow still
calls the module and still uploads last — an inline copy would pass every
other test and ship the original bug.

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

---------

Co-authored-by: Pinkers01 <pinky.bouw@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 17:41:45 +05:30
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
debpalashandClaude Opus 5 d76c2ba675 ci: price every smoke leg for a cold uv sync, not a warm one
The Windows-only 25 came from a warm-cache measurement: Linux and macOS finish
in ~65s when setup-uv restores its cache, so 10 looked generous. Run
30439640107 then hit "Failed to restore: Cache service responded with 400",
Linux installed torch from scratch, and the leg was killed at 10m17s. The 65s
was the cache, not the platform — my per-leg split was reasoning from the
wrong baseline.

A cache miss is not rare enough to treat as an outage (the cache service 400s,
a lockfile change invalidates the key, a new runner image starts empty), and a
timeout here is self-perpetuating: the leg dies before the post-step saves the
cache, so the next run is cold too.

25 everywhere, still bounded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:06:29 +05:30
Palash DebnathandClaude Opus 5 3553965f41 ci(windows): make the ffmpeg retry test the outcome, not choco's exit code (#1290)
* ci(windows): make the ffmpeg retry test the outcome, not choco's exit code

The chocolatey feed 503'd; choco printed "Unable to find package 'ffmpeg'"
and "installed 0/0 packages" — then exited 0. The retry loop added on
2026-07-20 for this exact class was `choco install ... && break`, so it broke
out on attempt 1, no backoff ran, and the job died one line later on
`ffmpeg: command not found`. It took #1281 red on an unrelated change.

A retry that trusts a lying exit code is not a retry. The loop now exits on
`command -v ffmpeg` and still fails the job loudly when ffmpeg never arrives.

Tests extract the real step body from ci.yml and run it against a stubbed
choco; 2 of the 4 fail against the previous loop.

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

* test(ci): pin PATH to the stub dir so the retry test can't false-green

The harness inherited the ambient PATH, so a real ffmpeg satisfied
`command -v` and the loop exited on attempt 1 — every assertion passed
against a broken workflow. It happened twice: /opt/homebrew/bin locally, then
/usr/bin on the Linux runner, which is what took this PR red.

PATH is now the stub dir alone, with the few real tools the stubs need
symlinked in, and stub shebangs are absolute (`/usr/bin/env bash` cannot
resolve bash when PATH is one directory). test_harness_actually_hides_ffmpeg
asserts the sandbox is a sandbox, so the next leak fails loudly instead of
quietly passing.

2 of 5 fail against the old `&& break` loop.

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

* ci: give the Windows smoke leg its own timeout, per-leg not shared

Smoke (Windows) has been dying at 10m08s inside `uv sync`, and the shared
10-minute budget made it self-perpetuating: the leg is killed before the
post-step saves the uv cache, so the next run starts cold and dies the same
way. Nothing primes the cache, so it never gets faster.

Measured on run 30385710466 — Linux 65s, macOS 65s, Windows still installing
torch when the job was killed. Windows now gets 25 minutes, priced for one
cold install to finish and populate the cache; warm runs land nowhere near it.

Per-leg rather than raising the shared value, so a genuine hang on Linux or
macOS still fails fast instead of inheriting Windows' allowance.

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

* ci(windows): skip the backoff after the final attempt; tighten the tests

CodeRabbit, both valid:

- The loop announced "retrying in 90s" and slept after attempt 3, though no
  fourth attempt exists — 90s added to an already-doomed job.
- The retry tests asserted `attempts >= N`, so a regression that kept going
  after ffmpeg appeared would still pass. Pinned to exact counts, plus a case
  asserting the final attempt announces no retry.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 01:24:27 -07:00
Palash DebnathandClaude Opus 5 602ea6f53e ci(release): stop the macOS preview updater bundle colliding with itself (#1281)
* fix(engines): show the under-provisioned-VRAM warning instead of discarding it

Four of the open low-VRAM reports (#1240, #1246, #1248 on 4 GB cards; #1277
on 6 GB) share one shape: the user generates, waits out the entire 300s
compute budget, and is then told the job "was too heavy for the available
compute".

The warning existed the whole time. Routing computes it (#1226's `_caveat`:
"…has 4.0 GB VRAM; this engine wants about 6 GB. It will run, but expect slow
generations that may time out"), and `/engines/select` echoes it in
`routing_reason` — but notifyEngineSelected only surfaced a reason when
`routing_status === 'cpu_fallback'`. The VRAM caveat rides on an ACCELERATED
verdict, so it fell through to the green "switched" success toast and was
thrown away. The user was told everything was fine, then waited five minutes
to find out it wasn't.

Now any caveat on the echo raises a warn-tone toast naming it, with a longer
duration since it lists the ways around the limit. This covers the kernel-risk
caveat on the same path.

Deliberately still ADVISORY, not blocking — matching the routing layer's
documented contract (the driver can page to system RAM, and short inputs fit
where long ones don't). The engine is still selected; the user just finds out
now instead of after the timeout. This is the first-run path too: the wizard's
library step shares notifyEngineSelected.

Fail-before verified: both new tests fail against the previous version.

Known remaining gap: a user whose engine is already selected sees this only
when they re-pick. A generate-time preflight would close that, but it needs a
"once per session, not per generate" design — filed as follow-up rather than
guessed at here.

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

* ci(release): stop the macOS preview updater bundle colliding with itself

The nightly preview run has failed on both macOS legs since early July:

  Uploading OmniVoice Studio_x64.app.tar.gz...
  ##[error]Validation Failed: {"resource":"ReleaseAsset",
                               "code":"already_exists","field":"name"}

`preview` is a ROLLING release, reused every night, and macOS updater
artifacts are the only ones Tauri names without the version:

  OmniVoice Studio_0.4.1-103_x64.dmg   unique per run — uploads fine
  OmniVoice Studio_x64.app.tar.gz      constant — collides on run 2+

Consequences, verified against the live release: the macOS updater bundles on
`preview` were last written 2026-07-04 (x64) and 2026-07-05 (aarch64), and
latest.json 2026-07-13 — three weeks stale as of today. Preview-channel macOS
users had no working update path. The failure also lands AFTER the dmg
upload, so each run looked partly successful while going red.

Deletes this arch's updater bundle before the upload. Matches the STORED
asset name by querying the release rather than guessing the spelling — GitHub
rewrites spaces to dots, so "OmniVoice Studio_x64.app.tar.gz" is stored as
"OmniVoice.Studio_x64.app.tar.gz" and a literal delete-asset by the uploaded
name would silently no-op.

Scoped to the preview path (a v* tag creates a fresh release with nothing to
collide with) and to the job's own arch, so the parallel aarch64/x64 legs
can't touch each other's assets. Verified the filter against all 209 live
preview assets: it matches exactly the 4 colliding updater files and no
versioned artifact.

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

* ci(release): fail loud when the preview asset sweep can't do its job

The cleanup step treated every `gh` failure as "nothing to clear" — 401, 403,
429 and network errors included. That reintroduces the outage it was written
to fix, with the evidence removed: the stale bundle survives, the Tauri upload
dies with `already_exists`, and the one step that could have explained why is
green. Three weeks of broken macOS Preview updates started exactly this way.

Only an absent release/asset is benign now. A 404 on view means "no preview
release yet" (GH_TOKEN is scoped to this repo, so 404 really is absence); a
404 on delete means someone already removed it, which satisfies the goal. Every
other failure fails the step with the reason printed. An unexpected arch is
also fatal rather than a silent skip — same class of blind spot.

Adds tests/test_release_preview_asset_cleanup.py, which extracts this step's
real shell body from release.yml (so it cannot drift) and runs it against a
stubbed `gh`: 6 of the 8 cases fail against the previous version.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 01:16:47 -07:00
debpalashandClaude Opus 4.8 07bbdb778e fix(release): drop the "Ranked by merged pull requests" line from the strip
Keep the ranking (avatars still ordered by PR count) but not the explanatory
sentence — just "## Contributors" + "Thank you all 💜" + avatars. Live v0.4.0
release updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:22:28 +05:30
debpalashandClaude Opus 4.8 e38e27ad10 fix(release): credit contributors on stable releases — ranked, single section
Stable v* releases previously credited nobody: the Contributors avatar strip
was wired only for the preview channel (preview-notes job), so the release
page showed just GitHub's native widget — which lists only the externally
@-thanked PR authors, never the owner, and can't be ordered.

Add a `contributors-strip` job that appends one "## Contributors" strip to the
stable release, crediting every PR author for the tag including the owner,
ranked by merged-PR count (desc, ties by handle). It suppresses GitHub's
duplicate native widget by neutralising the inline "— thanks @user!" text
mentions in the RELEASE body only (the repo CHANGELOG keeps the @handles); the
strip's own @handles sit in HTML attributes, which GitHub does not count as
mentions, so avatars stay linked.

Appends via `gh release edit` on the existing release (never a second softprops
publish — that splits installers across two releases), runs once (no matrix
race), and is idempotent (strips any prior block + inline @thanks before
re-appending). Docs: RELEASING.md §5b release-body row updated.

The live v0.4.0 release has been corrected to match (single ranked strip,
native widget gone).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:20:30 +05:30
debpalashandClaude Opus 4.8 d49c56df4a fix(shell): root error boundary so an app-render throw can't blank the window
The mount chain (StrictMode → QueryClientProvider → RemoteAuthGate → App) had
no error boundary above App. App's per-tab <ErrorBoundary> wraps only cover
their own subtrees, so a throw in App's own render — its top-level hooks, store
access, restoreProjectExtras, or the header/sidebar chrome that renders before
any tab — escaped every boundary and left #root empty. The shell's blank_guard
could then only reload three times and paint a dead-end failure page: exactly
the "#root children = 0 after 3 reloads" a user hit.

Wrap the mount root in <ErrorBoundary name="app-root"> so such a throw shows the
in-app, recoverable error card (Reload / Report, data untouched) instead of a
blank window. The shell guard stays as the last resort for the rarer case where
even the boundary can't render.

Recurrence-proofing (the throw only had to happen once to blank the app, and the
#1178-class variant only bites the MINIFIED bundle that dev + jsdom never run):
- main-app.test.jsx: fail-before/pass-after — a throwing app tree must leave
  #root non-empty (the recovery card), not blank.
- Wire the existing production-bundle smoke (e2e-prod/prod-bundle-smoke.spec.ts)
  into CI so a pre-render crash in the shipped bytes fails the pipeline, not a
  user's launch. Fixed its IGNORABLE list to treat the no-backend WebSocket
  handshake error as the harness artifact it is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:36:20 +05:30
debpalashandClaude Fable 5 62afc20525 ci: retry chocolatey ffmpeg install — community feed 504s intermittently
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:35:51 +05:30
debpalashandClaude Fable 5 63fd497caf feat: TTS-only first run, platform-curated ASR, guided OS permissions, parakeet-mlx
Only the TTS model (~2.4 GB) is required on first run; ASR models are
per-platform curated picks (curated_on in models.yaml) installed on demand.
Every transcription surface returns a typed asr_model_missing error with a
one-click download CTA instead of silently pulling multi-GB Whisper weights.
Settings -> Models is a grouped, platform-aware catalog. New guided
permissions UX (wizard System Check + Settings -> Permissions + mic
pre-flight) with native mic-state checks and OS settings deep-links. New
parakeet-mlx engine brings Parakeet TDT v3 to Apple Silicon (language-gated
capture preference so multilingual dictation never regresses). Docs:
expressive-speech page, Flush/Unload + CPU-fallback triage, clone-length FAQ.
Hardening: preflight fails open for custom model pins, ROCm curation no
longer inherits NVIDIA picks, Windows mic probe reads the NonPackaged
consent key, CaptureWidget setup race fixed, offline-cache CI simulation
fixes so empty-cache runners stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:20:54 +05:30
debpalash 82ccb9e829 ci(docker): identical, more aggressive disk reclaim in both image jobs
The two builds flip-flopped against the runner disk ceiling: CUDA hit
ENOSPC this morning (fixed by adding the reclaim step), then ROCm hit it
this afternoon even WITH the original list. Reclaim everything neither
job can use (~40-45 GB: full hostedtoolcache, swift, boost, global
node_modules, apt cache) and keep the list byte-identical in both jobs
so they can't drift apart again.
2026-07-16 21:47:11 +05:30
debpalash 0e9180816b ci(docker): free runner disk in the CUDA job too — ENOSPC killed the image build
The ROCm job got the reclaim step when it shipped; the CUDA job kept
running on default free space and finally hit 'No space left on device'
during uv pip install (2026-07-16, two consecutive main builds red).
Mirror the same ~25-30 GB toolchain cleanup.
2026-07-16 21:11:40 +05:30
mergetestandClaude Fable 5 f609f57fbc chore(release): codify all deployment channels as release rules; preview always builds from main
A release now has an explicit channel checklist (docs/RELEASING.md §5b):
GH Release + stable updater manifest, preview updater channel, GHCR +
Docker Hub in both CUDA and ROCm flavors, and the Docker Hub overview
sync (whose continue-on-error step must be verified by step log — it
403s silently on tokens without description-edit scope).

Preview/RC policy is now enforced, not just documented: release.yml's
preview-gate fails publish_preview dispatches from any branch but main,
since the preview manifest and rolling Docker tags all track main.

Also fixes docs/RELEASING.md §4-5, which still described the pre-2026-06
versioning scheme (tauri.conf.json + Cargo.toml as sources, 'Tauri
ignores package.json') — the exact opposite of the current single-source
rule — and docs/update-channels.md, which invited previews off feature
branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 16:12:31 +05:30
99e01610bb feat(docker): publish ROCm/AMD GPU image variant (#1165) (#1166)
The Docker image was CUDA-only, so AMD GPUs (e.g. RX 7900 XTX under
Podman) silently ran on CPU. Every preview and release now also ships a
ROCm variant built from the same Dockerfile:

- deploy/Dockerfile: parameterize the runtime base with a BASE_IMAGE
  build-arg (default unchanged: pytorch/pytorch 2.8.0 CUDA). Add
  PIP/UV_BREAK_SYSTEM_PACKAGES for the ROCm base's PEP-668-marked
  Ubuntu 24.04 Python (no-op on the conda CUDA base), and a build-time
  GPU_FLAVOR guard asserting the dependency install did not clobber the
  base image's GPU torch/torchaudio — a future dep bump that forces a
  torch reinstall now fails the build instead of shipping a CPU-only
  "ROCm" image.
- .github/workflows/docker.yml: new build-and-push-rocm job (separate
  job for runner disk — the ROCm base is ~25 GB unpacked, so it frees
  the preinstalled toolchains first). Tags mirror the CUDA semantics
  with a -rocm suffix (:rocm rolling preview, :stable-rocm, :X.Y.Z-rocm,
  :X.Y-rocm, :sha-xxxx-rocm) on both GHCR and Docker Hub, same secret
  gating. flavor latest=false so release tags can't clobber :latest.
  No cache-to: the ROCm layers would blow the 10 GB GHA cache budget.
- deploy/docker-compose.yml: new opt-in 'rocm' profile passing the GPU
  through via /dev/kfd + /dev/dri, with HSA_OVERRIDE_GFX_VERSION=11.0.0
  documented (user-set, not baked in — backend auto-sets it for known
  consumer GFX IDs).
- Docs-sync: docker.md (ROCm quick start incl. Podman/Quadlet, tag
  table, troubleshooting), dockerhub-overview.md, README AMD note,
  linux.md ROCm section cross-link, CHANGELOG [Unreleased].

Base image: rocm/pytorch:rocm7.2.4_ubuntu24.04_py3.12_pytorch_release_2.8.0
— torch 2.8.0 exactly matches the CUDA image (identical resolution, so
uv keeps it), py3.12 satisfies requires-python >=3.11 (the ubuntu22.04
variants are py3.10 and do not).

Closes #1165

Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:34:28 +05:30
9ecb810946 fix(shell): version-gate crash markers, pin the WebView repair contract, run the Rust suite in CI (#1145)
* fix(shell): version-gate crash markers, pin the WebView repair contract, run the Rust suite in CI

Two deferred items from the recurrence audit, plus the CI gap that made
them possible:

- crash.rs: a persisted "backend crashed" marker now only surfaces for
  the release that wrote it. After an upgrade, markers from the previous
  version (quite possibly the build whose crash the upgrade fixed) are
  ignored and pruned on read instead of resurfacing unacknowledged as if
  the new build had crashed. backend_version gains #[serde(default)] so
  legacy version-less markers still deserialize — as "", which the gate
  treats as stale by design. Preview stamps (X.Y.Z-N) count as their
  release.

- commands.rs: the #879 WebView2 cache repair's filesystem half is
  extracted into clear_webview_cache_at() (paths + retry policy as
  parameters, zero behavior change) and its contract is pinned by tests:
  no marker → nothing touched; marker consumed first, unconditionally
  (one-shot — a failing repair can never loop across launches); missing
  cache is success; a locked cache is retried then abandoned with a log,
  never bricking startup.

- ci.yml: the Tauri shell check only ran `cargo check`, which neither
  compiles nor runs #[cfg(test)] code — so the shell's ~90 unit tests
  (crash.rs, reset.rs, bootstrap.rs, …) never executed anywhere in CI.
  `cargo test --lib` now runs them natively on all three OSes.

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

* fix(shell): crash-notice read path is strictly read-only — a prune-save there could destroy a fresh marker

Greptile's P1 is real, and hotter than stated: get_last_backend_crash is
not just a startup check — streamDropError (#1119) polls it every second
for 8 s after a stream drops, which is exactly when the death watcher is
inside record_crash's load→push→save. The previous commit's read path did
load→prune→save when stale-version markers existed (the post-upgrade
state), so a poll could load the pre-crash snapshot, lose the race, and
save over the freshly recorded marker — silently deleting the only
evidence of the crash it was being polled to find.

Smallest fix: reads never write. The read path (extracted as
read_notice_from(path, version) so the contract is testable) filters
stale-version markers in memory only; disk pruning stays on the write
paths (record_crash, acknowledge_backend_crash), where load-modify-save
already existed pre-PR and is paced by a crash or a user click rather
than a 1 Hz poll. Regression test pins the file as byte-identical across
reads, stale markers filtered and current ones surfacing as before.

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

---------

Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 01:24:10 +05:30
2d9ebe350a feat(analytics): wire posthog-js — consent-gated, autocapture OFF (#1123)
* feat(analytics): wire posthog-js — consent-gated, autocapture OFF

The owner supplied the standard snippet:

    posthog.init(TOKEN, { api_host, defaults: '2026-05-30' })

Shipping that verbatim would have broken the guarantee we just made, twice:

1. It initialises AT MODULE LOAD — it starts tracking every user before they
   have consented to anything. The README now says "OmniVoice sends nothing out
   of the box"; this would have made that false on the very next release.
   Analytics is therefore started ONLY after the user opts in (Settings →
   Privacy), and the stored consent is what restores it at launch.

2. posthog-js AUTOCAPTURES by default, and `defaults: '2026-05-30'` turns that
   on. Autocapture sends the text content of the DOM elements a user interacts
   with. In THIS app the DOM holds the script they are about to synthesise,
   their voice names and their file names — exactly the content we promise never
   leaves the machine. It is explicitly disabled, along with session recording
   (which records the screen) and pageview capture.

utils/analytics.ts: hardenedConfig() — autocapture false, disable_session_recording
true, capture_pageview/pageleave false, mask_all_text + mask_all_element_attributes
as defence in depth, and opt_out_capturing_by_default so init alone can never
capture. Events pass sanitizeProps(), mirroring the backend allowlist: a key not
on it is DROPPED and long strings refused, so a future caller cannot leak content
by adding a field. Backend down / no consent / no destination → stays off.

The token is taken from VITE_POSTHOG_KEY at BUILD time and is never committed —
a token-shaped literal trips the secret scanner and is a bad habit regardless.
release.yml injects it from a repo secret; the backend already reads
POSTHOG_PROJECT_TOKEN the same way. No token => no destination => the Privacy
toggle isn't offered and nothing can be sent, which is the right default for a
source build. A test fails if a phc_ literal is ever committed to that file.

posthog-js added to frontend/package.json; root bun.lock regenerated and
`bun install --frozen-lockfile` verified (the Docker gate).

11 tests: autocapture/session-recording/pageview off, starts opted-out, allowlist
drops text+paths+names, long strings refused, consent honoured in all three
failure directions, and no token literal in source. Frontend suite 1229 passed.

* test(analytics): guard the committed-token rule in the suite, not just in the scanner

The frontend typecheck failed on the guard I added: it reached for `node:fs`,
which has no type definitions in the frontend tsconfig (and would have been
cwd-dependent at runtime anyway). Wrong layer.

Source-scanning guards in this repo are Python tests (test_no_hardcoded_cjk,
test_no_literal_borders), so this one moves there — and gets strictly stronger
in the process: it scans every tracked file rather than analytics.ts alone, and
matches a PostHog key by SHAPE (phc_[A-Za-z0-9]{20,}), so a *different* key
can't slip through where the old test only knew about the one gitleaks caught.

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

---------

Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:04:58 +05:30
7705343386 fix(release): attach uninstall scripts with gh release upload, not a 2nd softprops publish (#1106)
v0.3.20 shipped with ONLY the Linux AppImage — the macOS dmg and Windows msi
were missing from the published release. Root cause: the uninstall-scripts job
(added in #1097) ran softprops/action-gh-release@v2 as a SECOND publish for the
tag, which raced tauri-action's per-matrix draft and split the platform
installers across two releases (a draft holding mac/windows, a published one
holding linux + checksums + the scripts). The updater manifest split too — each
release's latest.json covered only its half of the platforms.

Fix: attach the scripts with `gh release upload <tag> … --clobber` (which adds
assets to the EXISTING release and can never create a second one) instead of
softprops. `needs: [build]` guarantees the release exists first; --clobber keeps
a re-run idempotent.

(v0.3.20 itself was already repaired by hand — the mac/windows bundles were
re-attached from the draft, the latest.json manifests merged into one covering
all 8 platform keys, and the stray draft deleted. This prevents recurrence.)

Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:04:31 +05:30
0421be966e feat(settings): in-app uninstall — Settings → Storage → Remove all data (#1089) (#1099)
* feat(settings): in-app uninstall — Settings → Storage → "Remove all data"

The v0.3.19 uninstaller was a SCRIPT, which never reaches the people who need
it: anyone who installed the .dmg / .msi / AppImage has no repo to run
scripts/uninstall.sh from — exactly the reporter in #1089, an AppImage user.
"Where is uninstall in the app?" had no answer. Now it does.

New Tauri commands (uninstall.rs):
- uninstall_scan  — every folder this install owns, with real sizes, resolved
  through the same setup.rs helpers the app itself uses, so custom + portable
  locations are cleaned instead of the defaults being assumed.
- uninstall_purge — stops the backend (marking the kill intentional so the #567
  supervisor doesn't respawn one into the directories being deleted), removes
  the folders, and lets the UI quit the app: the Python env it runs on is gone,
  so there is nothing to return to.

This lives in the Rust shell, not the backend, because the biggest thing to
remove is the managed Python environment and the backend is RUNNING FROM IT — a
process can't delete its own interpreter (and Windows locks the files).

Safety: every path must pass is_recognizably_ours() before any remove_dir_all —
absolute, not `/` or $HOME, and carrying an OmniVoice-owned component (unit
tested both ways). The shared Hugging Face cache is reported separately and is
OPT-IN behind its own checkbox with the caveat spelled out: it's the standard HF
cache other ML tools share, so sweeping it up silently would delete models this
app never downloaded. Deleting voices/projects is irreversible, so the confirm
requires TYPING the word, not just a click.

Also fixes a real bug in what shipped in v0.3.19: the scripts and docs missed
where the BACKEND writes its logs — ~/.local/state/OmniVoice on Linux and
%LOCALAPPDATA%\OmniVoice\Logs on Windows (backend_log_path(), backend.rs) — so
every Linux/Windows uninstall left a stray log dir behind. Covered now in the
scripts, the docs, and the in-app scan.

And the scripts now ship as release assets, so cleanup is possible without
launching the app at all.

Rust: 2 new guard tests. Frontend: 6 new tests (the size on the confirm button
must equal what actually gets deleted); suite 1182 passed. Docs synced.

Refs #1089

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

* fix(settings): drop token border utilities from UninstallPanel (design guard)

tests/test_no_literal_borders.py::test_no_token_border_utilities_in_jsx is a
backend guard that scans JSX — so a frontend-only test run misses it. It forbids
`border-[var(--chrome-border)]` structural utilities: the app-wide border removal
converted every panel/row frame away from them, and they render a stray hairline
the moment the token doesn't resolve transparent.

Row dividers → spacing + an alternating `--chrome-hover-bg` tint; the opt-in
checkbox card → a background tint; the confirm input → the sanctioned arbitrary
`[border:1px_solid_var(--chrome-border)]` property form the other settings inputs
already use (explicitly not flagged by the guard).

Guard green.

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

---------

Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:35:17 +05:30
3f62471bad test: pay down export-router test debt + kill two test-order pollution classes (#1081)
Four pieces of test debt, root-caused and hardened:

1. exports.py test coverage (was: zero dedicated tests): new
   tests/test_exports_api.py (26 tests) covering /export, /export/record,
   /export/history, /export/reveal — happy paths, traversal/containment
   guards (incl. symlink escape), destination validation, error mapping,
   and the mp4 watermark-overlay branch with its plain-copy fallback.
   Two real bugs found and fixed in the router:
   - _safe_destination checked isabs() on realpath()'s output, which is
     always absolute — dead check; a relative destination silently exported
     to a cwd-dependent location instead of the documented 400.
   - _safe_source let "." / ".." through the basename guard (caught only
     later by realpath containment as a confusing 404); now 400 up front.

2. CI-Linux fp16 default-dtype leak (test_prefers_vocals_over_mix,
   test_final_dub_track_and_seg_wav_are_watermarked): not reproducible on
   macOS — instrumenting torch.set_default_dtype across both tests records
   zero non-fp32 sets locally. Both tests now carry an opt-in
   torch_dtype_isolation fixture (save/restore, so the leak can never
   spread), and the conftest guard is demoted to pure insurance. A cheap
   permanent recorder wraps torch.set_default_dtype /
   set_default_tensor_type once torch appears and captures the setter's
   stack only on a non-fp32 set; both fixtures print that stack when they
   fire, so the next CI occurrence names the exact culprit call chain.

3. Test-order pollution (both reported combos): root cause was
   collection-time sys.modules stubbing in backend/tests — seven modules
   installed bare ModuleType stubs for core.config (and test_capture_ws.py
   for services.model_manager/asr_backend/ffmpeg_utils, now all lazily
   imported by the router anyway). pytest imports test modules during
   collection, so the stubs leaked process-wide before any test ran:
   - combo (a): monkeypatch.setattr("core.config.OUTPUTS_DIR", ...) in
     test_longform_e2e died with AttributeError (core never gets a .config
     attribute when the import is satisfied straight from sys.modules).
   - combo (b): test_router_smoke's `from main import app` died with
     ImportError: cannot import name 'find_ffmpeg' (unknown location).
   Fix at source: new backend/tests/conftest.py sets a hermetic
   OMNIVOICE_DATA_DIR (mirroring tests/conftest.py, #878) and the real
   core.config is imported everywhere — zero sys.modules surgery. New
   backend/tests/test_no_module_stubs.py guards the whole class (verified
   fail-before/pass-after against the old stub). Stale rationale comments
   in pyproject.toml and ci.yml updated to match.

4. batched_tts.py TODO(#312): investigated, comment corrected only —
   #312 is closed (the live routes are engine-aware); this module has zero
   call sites and stays an unintegrated experiment. See PR notes.

Full tests/ suite: 2796 passed. backend/tests standalone: 130 passed.
Both pollution combos re-run green in the reported orderings.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:10:35 +05:30
f546f04c8e fix(ci): bump Linux release runner to ubuntu-24.04, fixing stale bundled WebKitGTK (#961) (#1007)
The AppImage bundles whatever libwebkit2gtk-4.1-dev the build runner's
apt repos resolve at build time (see the "Linux system deps" step) —
AppRun's LD_LIBRARY_PATH then makes that bundled copy take priority
over the host's system WebKitGTK at runtime. ubuntu-22.04's version
was stale relative to what current distros (Ubuntu 24.04+, Fedora 44)
ship, which is why a from-source build (linking straight against the
host's healthy system library) worked fine on the exact machine where
the shipped AppImage white-screened — the released binary was running
an older, buggier WebKitGTK under the hood regardless of the host.

Bumped the Linux release matrix entry to ubuntu-24.04, and ci.yml's
Tauri shell-check job to match (its own comment already says "Mirror
release.yml" — now it actually does, so a green PR check accurately
predicts the release build will also succeed).

Raises the AppImage's glibc floor from 2.35 to 2.39 (Ubuntu 24.04+) —
README's system-requirements table corrected from the now-false
"Ubuntu 20.04+" claim. No reports of anyone on a pre-2022 distro.

This does not fix the AppRun launcher's separate, related bug (its
WebKitGTK-version auto-detection reads the *system's* pkg-config
version, not the version actually bundled and running) — that would
need a reliable way to read the bundled .so's version from within the
AppImage, which isn't straightforward (WebKitGTK's soname doesn't map
1:1 to its release version) and isn't verifiable without a real Linux
build environment to test against. Left as a known, separate gap.

Cannot be verified from here on a real Ubuntu 26.04 machine — shipped
on the strength of the root-cause diagnosis, pending the reporter's
confirmation.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 17:21:50 +05:30
4aa9abe22a docs+scripts: install fixes — desktop-prod tauri resolution, Ubuntu white-screen guidance, honest GPU/prereq docs (#960 #961 #962) (#964)
* docs+scripts: install fixes — desktop-prod tauri resolution, Ubuntu white-screen guidance, honest GPU/prereq docs (#960 #961 #962)

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

* docs(changelog): add the install-fixes batch under [Unreleased] (#964)

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

---------

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 19:34:01 +05:30
85b0db65bc ci(release): version-first release titles so the tag shows in GitHub's truncated release list (#922)
GitHub's release-list sidebar clips the title mid-string, hiding the version
when it trails 'OmniVoice Studio'. Name stable releases 'vX.Y.Z — OmniVoice
Studio' and the preview 'Preview — OmniVoice Studio'. Existing releases were
renamed to match.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:24:27 +05:30
be1ec3ade0 fix(platform): declare Intel-Mac local backend unsupported — honest first-run gate + docs (#889); Windows portable-install docs (#766 follow-up) (#891)
torch >=2.3 ships no macOS x86_64 wheels (transformers 5.x needs torch >=2.6),
so `uv sync` can never resolve on an Intel Mac — per the platform-parity rule
the honest option is declaring the platform unsupported, not letting first
launch die in a raw resolver error:

- bootstrap.rs: pre-check on macOS x86_64 before any venv create / uv sync
  (first-run AND repair paths) fails fast with an actionable message
  (remote-backend escape hatch + docs link); healthy pre-torch-bump venvs are
  deliberately untouched. Unit test pins the message's load-bearing phrases.
- BootstrapSplash: routes the failure to a dedicated localized hint
  (bootstrap.hint_intel_mac, all 21 locales) and suppresses the useless
  Retry-oriented hints for it.
- README + docs/install/macos.md (+ troubleshooting #9): every Intel-Mac
  support claim now says UI-installs-but-backend-cannot-run, including the
  from-source path (also broken); remote backend documented as the only use.
- release.yml: #889 note on the macos-15-intel leg — artifact is UI-only;
  keep-or-drop is an owner call, deliberately not changed here.
- docs/install/windows.md: new "Portable install (Windows)" section promised
  in #766 — custom MSI wizard folder / msiexec INSTALLDIR=..., what lives in
  OmniVoiceStudio-Data next to the exe, and the Program-Files-greyed-out why.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:22:37 +05:30
Palash Debnathandmergetest f7b7e2c13a chore(version): pin main to 0.3.8 (revert the post-release auto-bump) (#858)
* Revert "chore(version): main -> 0.3.9 after v0.3.8 release"

This reverts commit 7489bef085.

* chore(release): gate the post-release version-bump behind AUTO_VERSION_BUMP (owner controls bumps)

Owner decision (2026-07-01): keep main pinned to the released version and bump
only on explicit request. The version-bump job now runs only when the repo
variable AUTO_VERSION_BUMP == 'true' (default off), so releasing no longer
auto-rolls main to +1. Documented the override in CLAUDE.md's versioning rule.

---------

Co-authored-by: mergetest <test@local>
2026-07-01 20:18:15 +05:30
5c9b7ca313 chore(format): adopt oxfmt for JS/TS/JSX + CI format gate (#774)
Adds oxfmt (Rust formatter, Prettier-conformant) — the repo had no formatter, so
this is a one-time normalization of the JS/TS/JSX code (257 files; purely
cosmetic — full suite stays 638/638).

Scope is deliberately narrowed in .oxfmtrc.json to JS/TS/JSX only:
- singleQuote:true + jsxSingleQuote:false — preserve the project's existing
  style (single-quoted JS, double-quoted JSX attrs), not oxfmt's double-quote
  default. (Flipping quotes globally also broke a source-string-parsing test;
  preserving them keeps featureCoverage green.)
- Excludes **/*.css (the CSS→Tailwind migration will rewrite those — formatting
  them now is wasted churn), **/*.json (avoids reformatting 20 i18n locale
  files + config), **/*.toml, and src-tauri/** (Rust/Tauri config — out of scope
  for a frontend JS formatter; oxfmt was reformatting Cargo.toml/tauri.conf.json).

Tooling:
- `bun run format` (write) / `bun run format:check` (verify).
- ci.yml: new "Frontend format check (oxfmt)" gate after the oxlint gate.

Verified: format:check clean; oxlint 0 errors; vite build; full suite 638/638;
bun install --frozen-lockfile in sync.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:18:01 +05:30