Synchronize VoiceStudio release metadata, lockfiles, installers, container references, documentation, and the dated v0.5.2 changelog after all planned fixes landed.
Prepare the tested main branch for the v0.5.1 patch release with synchronized version sources, mirrors, lockfiles, release notes, and install guidance.
Closes#1687.
Refreshes frontend, tooling, and Python dependencies; regenerates the frozen lockfiles and worker protocol stubs. Keeps compatibility caps for FastAPI, Oxlint, and Vitest where newer releases break repository contracts. Updates the Uvicorn bind-failure regression test for its new nonzero exit code. Fully tested after merging current main.
* chore: bump to 0.5.0
Owner-requested minor bump. package.json is the source of truth; the
three mirrors (Cargo.toml, pyproject.toml, _FALLBACK_VERSION), the three
lockfiles and the branding pin move in lockstep, and the accumulated
Unreleased section becomes the curated 0.5.0 release notes — quiet
Highlights first, one-liner subsections after, duplicates folded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: fresh README for the VoiceStudio era; 0.5.0 notes wear the release
README: 551 lines from 656 — a What's-new-in-0.5.0 section with real
captures, the engine tables corrected to the actual 16 TTS registrations,
a stale Settings path and a broken Colab link fixed, roadmap/FAQ/credits
trimmed to what earns its place.
Release notes: the quick-switch GIF and catalogue/gallery screenshots,
captured from the running app during the pre-bump test pass, embedded
after the Highlights; #1542's gallery work and the ffmpeg CI fallback
recorded in their subsections.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: the feature inventory's Remote Model Downloads mention survives the README trim
check-docs-drift requires every docs/features.yaml name verbatim in the
README; the overhaul folded the phrase away. It now lives in the remote
workers feature line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: one blockquote, and an architecture claim that survives the opt-ins
CodeRabbit on #1540: MD028 blank line inside adjacent blockquotes, and
'every layer is on your machine' contradicted the opt-in remote paths
documented two sections away — it now states local-by-default with the
opt-ins named.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Version files (package.json + the three mirrors) and both lockfiles bumped
0.4.1 → 0.4.2; [Unreleased] renamed to [0.4.2] — 2026-07-28.
Ships five user-facing fixes: the update toast, the restart guard that used
to discard in-flight work, the shutdown-is-not-a-crash 503, the self-healing
half-downloaded model, and the dub-history purge ordering — plus the
"Dismiss" mistranslation in five locales.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* release: v0.4.1
Seven user-reported issues fixed since v0.4.0 (#1221–#1229). Version bumped
across the single source of truth (frontend/package.json) and its three
toolchain mirrors; [Unreleased] renamed to the release section that
release.yml extracts verbatim as the GitHub Release body.
Docker tag examples in docs/install/docker.md and deploy/dockerhub-overview.md
updated to 0.4.1 (docs-sync rule).
* release: #1239 review — sync Cargo.lock to 0.4.1
Greptile: the manifest said 0.4.1 while Cargo.lock still recorded 0.4.0, so a
`cargo build --locked` (and the Tauri bundler's own locked build) would fail
on the mismatch. Regenerating locally updated it but it was never staged.
* release: re-sync [0.4.1] after the fix merges, date it 2026-07-27
Picks up everything merged since the section was first written: the first-run
wizard chrome (#1241), the MCP host allowlist (#1249), the macOS 12 startup
crash (#1245), and the six error-message fixes (#1247, #1251, #1254, #1256,
#1257, #1262).
Deliberately NOT included:
- the dub delete-resurrection fix (#1252, #1253) — split to #1270 after it
needed six rounds of correction, the last two finding that the fix did not
close the reported case and that its own bound reintroduced it;
- the Linux AppImage WebKit fix (#1258, #1244) — held on #1265 pending
confirmation on a Mesa 26.1 host, which nobody has run.
Bump the single source of truth (frontend/package.json) and the three
toolchain mirrors (Cargo.toml, pyproject.toml, backend/core/version.py) plus
the lockfiles (Cargo.lock, uv.lock) from 0.3.22 → 0.4.0, and rename the
CHANGELOG's [Unreleased] section to [0.4.0] with milestone highlights.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two independent causes of a blank app window.
Never SHIP a blank (the prod hole). v0.3.22 shipped a black screen: a minifier
temporal-dead-zone reorder threw before React mounted, leaving an empty #root
(#1178). It reached users because every existing check — vitest, node:test, and
the whole Playwright e2e suite — runs the UN-MINIFIED dev server, so a bug
living only in the minified bundle passes them all. playwright.prod.config.ts +
e2e-prod/ close that hole: build the real bundle, serve dist/ via vite preview,
and assert the app actually mounts (#root has children, renders visible text,
no pageerror). The core assertion is deliberately structural — "did anything
mount?" — because that is what a pre-render crash always breaks, whatever its
cause. retries: 0, so a blank screen can never be flaky-passed away.
Never DISPLAY a blank in dev (the collision). Running `bun desktop` while one is
already up does not fail politely, it cascades into a blank window. Reproduced
deterministically and measured over CDP: the healthy main window has #root
childElementCount 1; after a second launch it is 0. The new launch's port grab
makes the running instance's dev:api exit, and `concurrently --kill-others-on-fail`
then tears down that instance's whole stack including its Vite server — leaving
its window open, pointed at a dev URL that no longer answers. desktop-dev.mjs
now clears a leftover dev app first, loudly.
The safety boundary for that cleanup is `isDevAppProcess` in desktop-common.mjs:
it matches the cargo dev binary (`omnivoice-studio`) ONLY, never the installed
release app (`OmniVoice Studio`) — killing a user's real app would be far worse
than the bug being fixed. Unit-tested both ways.
Also makes the gate runnable off Linux: the dev e2e config hardcodes
/usr/bin/chromium, which doesn't exist on Windows/macOS. The new config falls
back to Playwright's own browser so a contributor can run the gate before a
release.
The ci.yml step that runs this gate is NOT in this commit — pushing workflow
changes needs a token scope this session lacks. It is provided separately for
the maintainer to apply.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dev): self-heal cargo PATH so `bun desktop` works from a stale terminal
`tauri dev` shells out to cargo, so `bun desktop` died with "failed to run
'cargo metadata' ... program not found" on any terminal opened before rustup
was installed — the shell holds a stale PATH snapshot without ~/.cargo/bin even
though cargo is installed and on the persisted User PATH (a new terminal finds
it). That's a confusing first-run-from-source papercut, hit repeatedly on
Windows.
The frontend `desktop` script now runs through scripts/desktop-dev.mjs, which
prepends ~/.cargo/bin when cargo isn't already resolvable, then launches
`tauri dev` with that healed env. Cross-platform (~/.cargo/bin everywhere), a
no-op when cargo is already on PATH, and it passes an explicit env with the
correct-case Path key (Bun doesn't propagate process.env mutations to children,
and Windows uses "Path" not "PATH"). If Rust isn't installed at all, it prints
an actionable install hint instead of the cryptic cargo error. Verified E2E:
from a cargo-less PATH, `tauri dev` now compiles instead of failing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(changelog): note the bun desktop cargo-PATH self-heal (#1180)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: debpalash <tapudattaht@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dubbing release. package.json (source of truth) + the three mirrors
(Cargo.toml, pyproject.toml, version.py fallback) to 0.3.22; uv.lock +
Cargo.lock refreshed; CHANGELOG's Unreleased section (29 entries) becomes
## [0.3.22] — 2026-07-14 with the headline, split Added blocks merged.
Gates on the frozen content, all green before any version mutation:
backend 3033 + 204, frontend 1253, format, lockstep 6/6.
Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 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>
package.json (single source of truth) + the three toolchain mirrors to 0.3.21;
Cargo.lock + uv.lock regenerated (version lines only). CHANGELOG [Unreleased]
renamed to [0.3.21] — 2026-07-12, "the memory release", sections merged into
house style (one Added, one Fixed).
Ships the 16 GB OOM class fixes end to end: idle-release the dictation ASR
(#1104) + one TTS engine resident at a time (#1105), plus the scoped
Settings → Storage reset/uninstall pair (#1089/#1099/#1100) and the release-
asset-split workflow fix (#1106) so this tag uploads to a single release.
test_app_version.py lockstep: 6 passed. bun install --frozen-lockfile: clean.
Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
package.json (single source of truth) + the three toolchain mirrors to 0.3.20;
Cargo.lock + uv.lock regenerated (version lines only). CHANGELOG [Unreleased]
renamed to [0.3.20] — 2026-07-12 with the release headline, sections merged into
house style (one Added, one Fixed).
Ships the #1101 stale-"ready" race fix (0.3.19 users are hitting it today), the
in-app uninstaller (#1099), and the Linux/Windows backend-log-dir fix.
test_app_version.py lockstep: 6 passed. bun install --frozen-lockfile: clean.
Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
package.json (single source of truth) + the three toolchain mirrors
(Cargo.toml, pyproject.toml, core/version.py) to 0.3.19; Cargo.lock + uv.lock
regenerated (version lines only). CHANGELOG [Unreleased] renamed to
[0.3.19] — 2026-07-12 with the release headline; all six entries carry their
(#PR) refs and extract cleanly as the GitHub Release body.
Rebuilt on current main so the freeze covers everything that landed after the
first cut: #1088 (streaming preview), #1097 (uninstaller), #1098 (stream-drop
honesty), plus the backfilled #1088 changelog entry.
test_app_version.py lockstep: 6 passed. bun install --frozen-lockfile: clean.
Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
19-issue GitHub sweep: 16 PRs merged since v0.3.11, most fixing reports
filed same-day or in the days prior. Highlights: MLX-Audio's 6 other
curated models are finally selectable (was silently stuck on Kokoro
regardless of what was downloaded), first-run no longer dead-ends behind
restricted networks or corporate TLS-inspecting proxies, dubbing/batch
TTS honor your active engine selection, and a run of sharp community
diagnoses (ROCm wheel index, Windows dictation focus-steal, a genuine
frontend crash regression) got fixed largely because reporters did the
hard diagnostic work themselves.
Full backend suite: 2390 passed, 0 failed. Full frontend suite: 918
passed, 0 failed. Version lockstep (tests/test_app_version.py): 6/6
passed. Docker frozen-lockfile parity (bun install --frozen-lockfile):
clean, no drift.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
package.json + three mirrors -> 0.3.11 in lockstep; Cargo.lock/uv.lock/
bun.lock regenerated; CHANGELOG [Unreleased] -> [0.3.11] — 2026-07-05
with the multi-language-release headline; nine entries since v0.3.10.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
package.json (source of truth) + the three mirrors -> 0.3.10, in lockstep;
Cargo.lock/uv.lock/bun.lock regenerated (one line each; bun --frozen-lockfile
verified). CHANGELOG [Unreleased] -> [0.3.10] — 2026-07-05 with the release
headline; nine fixes since v0.3.9, mostly same-day field-report turnarounds.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add a Settings → OpenAPI page that renders an interactive Scalar API
reference for OmniVoice's own local backend, plus a compact footer button
that opens it.
- New OpenApiPanel fetches the live spec from the resolved backend base
(getApiBase()+"/openapi.json", via apiFetch so it follows remote-backend /
LAN-share overrides), owns loading + unreachable-backend fallback (with
Retry), and hands the parsed spec inline to Scalar.
- Scalar is bundled via the @scalar/api-reference-react npm package — NO CDN
script tag. It is lazy-loaded (ScalarApiReference.jsx) so its ~heavy Vue
bundle stays out of the initial load and only downloads when the page opens.
- CDN-free hardening: withDefaultFonts:false (drops the fonts.scalar.com
@font-face rules), proxyUrl:'' (Test Request client goes direct to the local
backend, not proxy.scalar.com), spec passed as inline content (no external
spec fetch). The Tauri CSP is the hard backstop. Verified the built dist:
external hosts appear only as inert/gated strings inside the on-demand Scalar
chunks and are absent from the initial-load chunks.
- settingsCategories: new 'openapi' category (Braces icon, api/openapi/scalar/
rest/swagger/docs keywords) in the System group; Settings render case wired.
- LogsFooter: compact Braces icon button (openSettingsTab('openapi')) next to
the discord/mail cluster, chrome-muted → accent on hover, uniform 14px icon.
- i18n: all strings via t() with English defaultValue fallbacks (openapi.*,
logs.open_api*, settings.openapi); keys added to en.json.
- Test: OpenApiPanel.test.jsx (mocks the spec fetch + stubs Scalar) — renders
the reference container on success, shows the unreachable fallback on failure,
recovers on Retry.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 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>
Migrate four OmniVoice UI primitives onto shadcn/ui foundations while keeping
their exact legacy prop APIs, so no call site changes.
- Button: thin wrapper over src/components/ui/button.tsx. Extends the shadcn
CVA with the OmniVoice variants (primary/subtle/softGhost/danger/chip[+Active]/
preset[+Active]/iconBtn[+Active]) + sizes (omniSm/omniMd/chip/preset/iconSm/
iconMd), styled via palette token utilities. Maps variant/size/iconSize/active/
loading/leading/trailing/block/ref. Each variant sets an explicit border
(transparent where needed) since the app ships Tailwind without Preflight.
- Badge: new src/components/ui/badge.tsx; CVA carries the tones (neutral/brand/
success/warn/danger/info/violet) + xs/sm sizes. Wrapper maps tone->variant and
keeps the ui-badge / ui-badge__dot hooks so the Header --pulse animation works.
- Progress: new src/components/ui/progress.tsx (on @radix-ui/react-progress) with
indicatorClassName + indeterminate support. Wrapper keeps per-tone gradients,
sizes, shimmer overlay, and the ui-progress / has-shimmer / is-indeterminate
hooks (residual.css keyframes unchanged).
- Segmented: new toggle.tsx + toggle-group.tsx (adds @radix-ui/react-toggle). The
`seg` toggle variant reproduces the segmented look; wrapper preserves the
items/value/onChange/size API.
residual.css: drop the obsolete .ui-seg__opt:focus-visible rule (focus now falls
through to the global ring). Badge pulse + Progress shimmer/indeterminate rules
kept (still needed).
Visual baselines: Badge/Progress/Segmented render byte-identical to before;
only Button baselines updated (shadcn markup differs in padding/radius, palette-
coherent across default/midnight/catppuccin). All gates pass: oxlint, oxfmt, tsc,
vite build, vitest (641), test:visual (48), bun install --frozen-lockfile.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P1 of the shadcn/ui primitive migration (docs/shadcn-migration.md): route the
OmniVoice form/data primitives through the shadcn components in
src/components/ui/* while keeping their exact exports and prop APIs, so no call
site changes.
- input.tsx: export `inputBaseClass` (the shell) with no behaviour change —
ShadcnInput baseline stays byte-identical.
- New shadcn components: textarea.tsx, select.tsx (+@radix-ui/react-select),
slider.tsx, table.tsx.
- src/ui/Input.jsx (Input/Textarea/Select/Field): Input/Textarea now render the
shadcn components; a small `fieldSizeVariants` cva (named palette utilities,
tailwind-merge-clean) restores the OmniVoice padding-based sm/md/lg scale +
filled bg-bg-elev-2 over the shell. Select stays a NATIVE <select> wearing the
same shell — DubSegmentTable/CompareModal/GeneralTab depend on
onChange={(e) => …e.target.value}, which Radix's value-only Select would break;
the Radix select.tsx is added for new call sites only.
- src/ui/Slider.jsx: wraps the shadcn Slider, keeping the number-based onChange +
label/value-bubble chrome; track/thumb sized via the data-slot selectors.
- Table deliberately NOT rerouted: ui/Table.jsx is a flex-<div> chrome wrapper
whose .ui-table*/.segment-table global classes are a SHARED CONTRACT used
directly by ModelsTable/DubSegmentTable/EngineCompatibilityMatrix (virtualised
lists needing the div/flex layout, not a semantic <table>). table.tsx is
provided for new tabular data; Table.jsx + its globals are untouched. Its
toolbar inherits the shadcn-backed Input/Button for free.
Verification: only the 3 Input-* visual baselines moved (palette-coherent across
default/midnight/catppuccin); Slider/Table stayed within tolerance. vitest 641
green, oxlint 0 errors, oxfmt --check clean, vite build green, root bun.lock
regenerated and bun install --frozen-lockfile in sync (Docker).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lay the foundation for migrating OmniVoice's UI to clean Tailwind v4 + shadcn/ui
WITHOUT changing the look: shadcn primitives inherit the existing OmniVoice
palette (Gruvbox-pink default + every [data-theme] variant) through a semantic
token bridge. Foundation only — no existing component is replaced.
What landed:
- shadcn init for Tailwind v4 + Vite + React 19: frontend/components.json
(new-york, rsc:false, tsx:true), src/lib/utils.ts (cn = clsx + tailwind-merge),
and a @/* -> src/* alias in vite.config.js + tsconfig.json so future
`npx shadcn add` resolves.
- Token bridge in src/index.css: a single `@theme inline` block maps shadcn's
semantic vocab (--color-background/-foreground/-card/-popover/-primary/
-secondary/-muted/-muted-foreground/-accent-foreground/-destructive/-input/
-ring + --radius) onto the existing OmniVoice --color-* tokens. Because those
tokens are re-declared per theme in ui/themes.css, theme switching recolors
shadcn components automatically — no per-theme shadcn block. Existing
--color-accent/--color-border and the --radius-* scale are left intact.
- Two proof components: src/components/ui/button.tsx + input.tsx (verbatim
shadcn new-york), rendered across default/midnight/catppuccin in the visual
harness with committed baselines (brand-pink / purple / lavender confirmed).
- New deps: class-variance-authority, clsx, tailwind-merge, tw-animate-css,
@radix-ui/react-slot. Root bun.lock regenerated; `bun install
--frozen-lockfile` verified in sync (Docker-green).
- Migration plan at docs/shadcn-migration.md (bridge table, primitive->shadcn
mapping, prop-compat wrapper strategy, staged waves, honest risk/effort).
Verified: vite build, typecheck:ci, oxlint (0 errors), oxfmt --check, vitest
(641 pass), test:visual (48 pass incl. 6 new baselines), frozen lockfile in sync.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gating prerequisite for the CSS -> Tailwind v4 migration: a pixel-for-pixel
safety net so each utility conversion can be verified against a known-good
baseline. There were previously no visual tests.
Approach: a lightweight Vite-served harness (NOT @playwright/experimental-ct-react)
that renders one presentational leaf component in isolation, with no Python
backend. Chosen because it adds zero new deps (root bun.lock untouched -> no
Docker frozen-lockfile risk), reuses the existing @playwright/test + bundled
chromium, and renders through the project's real Vite 8 + Tailwind v4 + token
pipeline so snapshots reflect the actual build output. CT's experimental React
runner on Vite 8 + React 19 was an avoidable compatibility risk.
- harness.html / harness.jsx: isolated render target driven by ?component=&theme=
URL params; applies themes via [data-theme] (default = bare :root Gruvbox),
loads the same fonts + token layers as the app, signals font-ready for stable
shots.
- specs.jsx: registry of pure variant spreads for Badge, Button, Panel,
SettingRow, SettingsToggle.
- manifest.ts: COMPONENTS x THEMES (default, midnight, catppuccin) the spec
iterates -> 15 committed baselines in __screenshots__/.
- playwright.visual.config.ts: dedicated config (separate from e2e), own Vite
server on port 3902, animations disabled, caret hidden.
- scripts: test:visual / test:visual:update.
- README: how to add a component, how to update baselines after an intentional
change, and why this stays local/manual (font/anti-alias differences across
OSes) rather than a blocking CI gate for now.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Add knip (dead-code/dep finder) for the bun workspace, then act on what it
found. Complements the oxlint gate: oxlint flags per-file unused symbols; knip
finds whole dead files/exports/deps across the project.
Tooling:
- frontend/knip.json + `bun run knip` script. Ignores the legitimate false
positives: public/aec-worklet.js (loaded via a dynamic AudioWorklet URL),
/@react-refresh (Vite dev inject), and tailwindcss + the Rust-side
@tauri-apps/plugin-updater / plugin-window-state JS packages (used by the
native plugin, not imported in JS).
Removed (all verified — build + tests + tsc + oxlint green):
- Dead files: CastingView.{jsx,css}, UpdateStatusChip.{jsx,css} (no refs; the
latter only survived in a stale comment, now reworded), and ui/motion.js.
- Unused deps: @radix-ui/react-popover, @radix-ui/react-select, @eslint/js,
eslint-plugin-react-refresh (the last two orphaned when eslint.config.js was
stripped for the oxlint adoption).
- 44 dead exports + 56 dead exported types across api/*, store/*, ui/*, utils/*:
deleted where used nowhere; dropped just the `export` keyword where still
referenced in-file.
Kept (justified): Slider primitive (keeps @radix-ui/react-slider meaningful),
AppMode export (a test string-parses its source), tailwindcss (CSS @import +
vite plugin).
Verified: oxlint 0 errors; tsc clean; vite build; full suite 638/638;
bun install --frozen-lockfile in sync (Docker rule).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ESLint was misconfigured (only globals.browser → 47 false no-undef) and run
NOWHERE in CI, so 259 errors had accumulated unnoticed. Replace it with oxlint
(Rust, ~50-100x faster) as the primary linter AND a real CI gate so lint debt
can't silently pile up again.
Tooling:
- frontend/.oxlintrc.json — correctness=error; no-unused-vars with the existing
^[A-Z_] convention; node/vitest env overrides + AudioWorklet/__APP_VERSION__
globals (kills the false no-undef class); max-lines:500 (warn).
- package.json: `lint` → oxlint, `lint:fix`, `lint:hooks` (advisory eslint).
- eslint.config.js stripped to ONLY the React-Compiler rule family oxlint can't
do yet (set-state-in-effect etc.), run via `lint:hooks`, NOT gated. Drop once
oxlint's JS-plugin support leaves alpha.
- ci.yml: new "Frontend lint (oxlint)" step in the Tests job — the gate.
Real bugs oxlint caught (were buried in ESLint's noise):
- GlossaryPanel: <X/> close-icon used but never imported → the edit-row cancel
button threw ReferenceError at render. Imported X.
- Two use*-named NON-hooks (useEngine action, useArchetypeAsProfile API call)
tripped rules-of-hooks; suppressed with documented disables (renaming these
misleading names is a worthwhile follow-up).
Cleanup to reach a 0-error gate: removed 52 genuinely-dead vars/imports across
18 files (heavy in App.jsx — stale useState left over from prior refactors) and
4 behavior-preserving autofixes (no-useless-fallback-in-spread / no-useless-escape).
Verified: oxlint 0 errors; bun install --frozen-lockfile in sync (Docker rule);
vite build passes; full suite 638/638.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-v0.3.6 release sweep found frontend/package.json stuck at 0.3.5 while the
other three version files were 0.3.6. package.json drives the runtime
`__APP_VERSION__` (vite.config.js), so a v0.3.6 build was calling itself "v0.3.5"
in the first-run footer AND in every auto bug report (undercutting the bug-report
feature). Root cause: the release.yml version-bump job only bumped the trio
(tauri.conf.json / Cargo.toml / pyproject.toml), never package.json, and no test
guarded the lockstep.
- Bump frontend/package.json 0.3.5 → 0.3.6 (matches the trip; `--frozen-lockfile`
still passes — the version field doesn't affect the bun lock graph).
- Add frontend/package.json to the release.yml version-bump job (set absolutely
via jq so any prior drift self-heals on the next release).
- Add tests/test_app_version.py::test_all_version_files_in_lockstep — fails CI if
the four files ever diverge again.
- CLAUDE.md versioning rule updated: it's now FOUR lockstep files, not three
(docs-sync).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): first-run install gate — nothing installs until the user confirms a plan
New `setup` module parks first runs in BootstrapStage::AwaitingSetup instead
of auto-installing. complete_setup validates the user's InstallPlan and only
then starts the existing bootstrap:
- install modes: installed (platform dirs) / portable (one folder next to
the exe / AppImage, config.json travels with it)
- user-chosen storage: env dir, data dir (OMNIVOICE_DATA_DIR), model cache
(OMNIVOICE_CACHE_DIR) — None = legacy default, byte-identical behavior
- minimum-space gate: per-volume free-space check (fs4 statvfs), grouped by
filesystem so dirs sharing a disk sum their requirements; install refused
when short (9 GiB env + 7 GiB models + 1 GiB data, measured + headroom)
- custom mirrors (PyPI index, HF endpoint, python-build-standalone) take
precedence over region presets in the venv/sync/backend env wiring
- ROCm torch variant selectable via config (env var still wins)
- existing installs migrate silently: venv present → setup_complete=true,
no questions re-asked; dev trees skip the gate entirely
19 unit tests (disk probing, space grouping, mirror validation, legacy
config compat).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): first-run setup screen — mode, storage with space gate, mirrors, compute
FirstRunSetup renders when the Rust side reports awaiting_setup (lazy-loaded;
regular launches pay nothing). One screen, defaults all work:
- language picker first (rest re-renders translated), 21 locales shipped
- Installed / Portable mode cards (portable disabled with reason when the
exe-adjacent folder isn't writable)
- storage rows with live per-path free-space probes (debounced
check_install_target), 'needs ~X / Y free' readouts, folder pickers
- client mirrors the Rust per-volume space gate: Start installation is
disabled with an explicit reason until every volume fits
- compute (CUDA-auto / ROCm), update channel, region + custom mirror URLs
- complete_setup errors surface inline; on success the normal bootstrap
progress UI takes over on the next status poll
Verified on a wiped machine: gate parks (no spawn, no downloads), screen
renders, 450 GB ≥ 17 GB requirement → Start enabled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): studio-console redesign of the first-run screen
The setup screen now reads as powering on studio hardware rather than a web
form — true to a voice studio, and self-sufficient offline (every font and
asset is bundled; a first run may be on a restricted network):
- breathing waveform masthead (CSS-only, deterministic speech-cadence
silhouette, staggered per-bar delays)
- Source Serif 4 display headline + engraved IBM Plex Mono panel labels +
Inter body — the three faces the app already ships
- rack-unit panels with corner screws, engraved title rules, serial plate
(OVS · vX.Y.Z)
- disk space as segmented LED capacity meters: lit = what the install
consumes, alarm-blink red on insufficient volumes
- mode cards with indicator LEDs; 'armed' Start button — LED lights and a
halo pulses only once every volume passes the space gate
- atmosphere: corner accent glows + SVG film grain; staggered rise-in
choreography on load
- all motion transform/opacity only; prefers-reduced-motion holds every
frame still; theme-token derived colors; focus-visible rings throughout
No logic changes: same IPC calls, same i18n keys, same space-gate math.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): wide desktop deck, hardware-aware Compute + Update channel cards
Three pieces of feedback addressed:
- width: the console is now a 1240px two-column deck (storage rail left,
decision rail right) that uses desktop real estate; collapses to one
column under 980px and stacks fully under 620px
- no outer chassis box: panels float directly on the atmospheric backdrop,
each carrying its own rack-unit treatment
- Compute and Update channel split into separate cards with real
information: get_setup_state now detects hardware (nvidia-smi → CUDA
name, /sys/class/drm vendor 0x1002 → AMD/ROCm, Apple Silicon → MPS,
CPU cores + RAM via sysinfo; best-effort, never blocks) — the Compute
card shows a live 'Detected: …' readout, badges the option that matches
the machine, and pre-selects ROCm on AMD boxes; both cards use LED
radio options with full descriptions (6 new i18n keys × 21 locales)
Also pins playwright-core as an explicit devDep — bun did not materialize
it through @playwright/test, breaking programmatic browser use.
20/20 Rust tests · vite build · CJK guard green. Verified live (gate
engaged, responsive single-column) and at 1600×1000 via mocked-IPC
browser shot (two-column deck).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): move network (region + mirrors) into the masthead with language
Language and download region are the two 'where am I' choices — they now
sit together top-right of the masthead, with the custom-mirrors disclosure
tucked beneath the subtitle. The Network panel is gone, leaving a balanced
deck: Install mode + Storage left, Compute + Update channel right.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): strip the boxes — fills and rules carry the structure
One design rule now: borders only where state demands them. Panels lose
their boxes entirely (engraved mono title + rule separates sections);
option cards, storage rows, selects/inputs, the hw readout, the version
plate and the ghost buttons are all flat fills; active options glow with
an accent tint + LED; blocked rows and errors use a red tint + 2px inset
edge bar instead of a border. The badge chip is fill-only too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): quiet pass — every element earns its visual weight
- waveform becomes a whisper: 22px trace, 2px bars, ~half opacity — an
ambient signature instead of a billboard
- storage readouts collapse to one mono line ('needs ~9 GB · 449 GB free');
the LED meter now appears only when it carries information (install
would consume >35% of free space, or the volume is blocked) — at 449 GB
free a bar was a meaningless sliver
- Change… buttons go text-quiet (transparent until hover)
- custom-mirrors disclosure right-aligns under the region select it
extends, instead of floating under the subtitle
- version plate moves to the footer next to the disk total — the masthead
keeps only title, subtitle, and the two locale/region selects
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): platform-matrix awareness — distro+arch detection, ROCm gated to Linux, no Windows console flash
The install matrix is OS family × distro × arch × GPU vendor, and the
setup screen now both shows it and only offers choices valid for it:
- HardwareInfo gains os_name (distro PRETTY_NAME from /etc/os-release on
Linux, macOS/Windows elsewhere) and arch (x86_64/aarch64) — the detected
line reads 'CachyOS x86_64 · NVIDIA RTX 4070 · 32×CPU · 31 GB RAM',
exactly what bug reports cite
- SetupState gains os; the ROCm option renders on Linux only (wheels
don't exist elsewhere) and complete_setup clamps rocm→auto on
non-Linux as the server-side backstop
- nvidia-smi probe gets CREATE_NO_WINDOW on Windows — no cmd flash on
the first screen a user ever sees
- Apple Silicon → MPS, Intel mac → CPU, ARM Linux → CPU: all matrix
cells resolve through the same base constructor
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): unify the whole first-run journey under the studio-console system
Setup → Installing → Model wizard now read as one continuous experience:
the same atmosphere, whisper waveform masthead, serif/mono type, LED
language and quiet fills across all three acts.
- Installing (BootstrapSplash): rebuilt in frs-* — segmented LED journey
meter (completed steps + live byte progress), LED step rail (done=green,
active=pulsing accent, pending=dim), engraved ACTIVITY panel with the
quiet mono log (collapse/copy as text-quiet actions), failure act with
red-tint error + hints + armed Retry. All logic untouched: stage poll,
event subscription + backfill, dedupe, hints, region/language selects.
- Model wizard (SetupWizard): same masthead with the step rail as engraved
mono LED steps top-right, welcome cards as option-card surfaces,
preflight as LED check rows (pass/warn/fail), frs nav buttons with armed
primaries, embedded Model Store / Engines / Dictation panels scroll
inside the act. Old 556-line stylesheet replaced by ~60 lines of glue;
BootstrapSplash.css reduced to a resolving stub.
- FirstRunSetup.css is now the journey's shared design system (step rails,
log panel, banners, hints, wizard chrome, check rows appended).
- 2 new strings (Installing / Activity) translated across all 21 locales.
Validated end-to-end on this machine: setup screen → Start installation →
real venv bootstrap (~10 min) → backend healthy on 3900 → model wizard.
20/20 Rust tests · vite build · CJK guard green · installing act verified
via mocked-IPC screenshot at stage=installing_deps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): --setup re-entry flag + make the install-plan screen un-stealable
The setup stage is first-run-only by design (completed installs skip it),
but it must be reachable on demand and must actually win the mount when
engaged. Three fixes:
- 'omnivoice-studio --setup' parks the bootstrap in AwaitingSetup on any
launch — checked before the attach-to-healthy-backend shortcut, so a
running backend can't skip past it
- App routing: awaiting_setup now outranks everything (a live backend
answering /setup/status used to route straight to the model wizard);
the wizard additionally requires stage === 'ready' so it can't mount
during the initial stage race
- useBootstrapStage: a transient IPC miss no longer permanently declares
'ready' (which killed the poll loop and silently skipped the setup /
progress screens) — it retries up to 5 ticks before conceding
Plus journey-wide titlebar clearance (content never sits under the GTK
headerbar / macOS traffic lights / Windows controls) and drag-region
mastheads on all three acts.
Verified: mocked-IPC harness with stage=awaiting_setup + a LIVE backend
answering /setup/status renders the setup screen, not the wizard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(setup): remove backdrop decoration — flat surface, state-only emphasis
The corner accent glows and SVG film grain rendered as visible banding /
noise artifacts on many panels — both gone; the journey now sits on a
clean flat chrome background. Also swept the remaining decorative bloom:
the active option card drops its glow shadow (flat accent tint + LED carry
the state), and the armed Start button loses its pulsing halo (the lit LED
already signals actionable). Remaining shadows are functional micro-detail
only: 6px LED glows, meter track inset, red edge bars.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): journey rail + verbosity diet — clean, smooth, elegant
The setup page is now visibly stage 1 of the install flow: a quiet
breadcrumb rail (SETUP → INSTALLING → MODELS & ENGINES) sits between the
waveform and the headline on both the setup and installing acts, LEDs
marking done/active/pending — one continuous story across the journey.
Verbosity halved without hiding information:
- option descriptions unfold (260ms ease) only on the selected card; the
page shows exactly one explanation per group, collapsed cards keep the
text as a tooltip
- storage rows drop their always-on caption (label + path + readout +
Change… on one line; caption lives in the row tooltip)
The whole page now fits a laptop window without scrolling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): merge Models + Engines into one wizard act
Two tabs weren't necessary: models are the required gate, engines the
optional extras — now two stacked panels in a single 'Models & engines'
step (label reuses the journey-rail key, translated in 21 locales).
Wizard shrinks to 4 steps: Welcome → System check → Models & engines →
Dictation. Continue still gates on models_ready only; engines stay
optional. Welcome cards updated to the 3 remaining acts; static cards
keep their descriptions visible (the active-only fold is for radios).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(setup): wizard was skipped after first-run install — probe /setup/status on bootstrap ready
The models-needed probe started at mount with a ~30s retry ceiling. On a
first run, mount happens at the setup page — by the time the user reads
it and the multi-minute install finishes, the attempts were long burned,
so setupChecked landed as 'no wizard needed' and the studio rendered with
zero models on disk. The probe is now keyed on bootstrapStage and runs
when it hits 'ready' — the first moment a backend exists to answer.
Normal launches (backend up quickly) behave exactly as before.
Caught by running the full journey three times end-to-end: rounds 2–3
skipped Models & engines after install; with the fix the wizard mounts
with models_ready=false (Whisper large-v3 listed missing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): drop the Welcome step — wizard opens on System check
The welcome act had nothing left to say: the journey rail names the
stages, the setup page already oriented the user, and the cards repeated
both. The wizard is now three steps — System check (auto-runs on mount) →
Models & engines → Try dictation — landing the user directly on live
preflight results instead of a page about the pages to come.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): true unified library — models + engines as ONE list
'Merge them' meant one list, not two panels stacked — fair criticism.
The wizard's Models & engines act is now a purpose-built WizardLibrary:
every installable is a row of the same grammar (LED · name · chip ·
size · action):
- required models lead (REQUIRED chip, Download action, live SSE
progress bar + percent, green LED when installed) — they gate continue
- TTS engines follow (ENGINE chip): active engine glows accent,
available ones offer one-click Use (selectEngine), heavy installs
defer honestly to Settings ('install later in Settings' + reason
tooltip)
- the optional-model tail folds behind 'Show N optional models'
The full management surface (search, HF token, deletes, sorting) stays
in Settings — a first run needs a checklist, not a store. 9 new strings
× 21 locales. Verified against the live backend via the browser harness:
required/installed/engine/active/Use/defer states all render in one list.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the Functional Source License with the GNU Affero General Public
License v3 across the project, with a paid commercial license retained for
proprietary/closed-source use without AGPL obligations (open-core, like
Firecrawl).
- LICENSE: verbatim AGPL-3.0 text under an AGPL Notice + Scope header;
drops the FSL "Competing Use" framing and the 2-year Apache-2.0 conversion.
The bundled omnivoice/ TTS model stays Apache-2.0 upstream (AGPL-compatible).
- Manifests now declare SPDX AGPL-3.0-only: pyproject.toml, Cargo.toml
(normalized from bare AGPL-3.0), and both package.json (added license field).
- README.md / README_CN.md: badge, pricing, commercial-use FAQ, License section.
- en.json: in-app Commercial License copy reworded to AGPL; the false
"converts to Apache 2.0" FAQ removed (+ its renderer block in SupportPage.jsx).
Non-English locale strings still describe the old FSL model and are left for a
follow-up translation pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Patch release. Version bumped across all sources + lock files; [0.3.5] CHANGELOG.
Ships:
- #270 — speaker diarization fixed on PyTorch >=2.6 (weights_only=True rejected
the pyannote checkpoint's TorchVersion global); the loader now registers the
shared safe-globals allowlist before loading.
Tagging v0.3.5 triggers release.yml (desktop) + docker.yml.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Patch release. Version bumped across all sources + lock files; [0.3.4] CHANGELOG.
Ships:
- #255 — PyTorch-Whisper backend works as a standalone fallback (no cuDNN 8,
no OMNIVOICE_PRELOAD_TTS_ASR=1), unblocking Windows+NVIDIA users hitting the
cudnn_ops_infer64_8.dll error.
Tagging v0.3.4 triggers release.yml (desktop) + docker.yml.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Patch release. Bumps version across all sources + lock files; adds [0.3.3]
CHANGELOG.
Ships:
- #262 — Settings → About now shows the server's CPU architecture (was the
client browser's platform, e.g. "Win32", in Docker).
- Validates the bash-3.2 checksum CI fix on a real release (the macOS
SHA256SUMS should now upload automatically).
Tagging v0.3.3 triggers release.yml (desktop) + docker.yml (GHCR
:0.3.3/:0.3/:latest).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Patch release. Bumps the version across all sources + lock files and adds the
[0.3.2] CHANGELOG section.
Ships:
- #261 — "Loopback origin required" 403s in the Docker admin UI (and blank
version): the image now runs in OMNIVOICE_SERVER_MODE so the loopback gate
is relaxed for the headless deployment; desktop loopback boundary unchanged.
Tagging v0.3.2 triggers release.yml (desktop) and docker.yml (GHCR
:0.3.2/:0.3/:latest).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First tagged build of the 0.3 line off main. Bumps the version across all
sources (pyproject + frontend package.json + Tauri conf + Cargo + both lock
files + the source-checkout fallback in core/version.py) and adds the [0.3.1]
CHANGELOG section.
Ships:
- #256 — browser/Docker file-export crash (invoke undefined)
- #249 — version surfaced in web/Docker UI + desktop-only updater hidden
- #255 — transcribe stream now surfaces the real ASR/model-load failure
Tagging v0.3.1 triggers release.yml (desktop binaries) and docker.yml
(GHCR :0.3.1 / :0.3 / :latest).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(gallery): rename to "OmniVoice Gallery" + fix dark-theme dropdown colors
The gallery heading now reads "OmniVoice Gallery" (gallery.title, all 21
locales — brand prefix on each localized word).
The facet filter <select>s (Gender/Age/Pitch/Accent/Language) rendered with
the OS-default light control surface on the dark theme: .facet-select set
background/border from --bg-tertiary / --border-color, which are defined
nowhere. An undefined var() reads as transparent on the sibling <div> filters
(fine over the dark page) but falls back to the native light background on a
form control. Switch to the defined dark-chrome tokens and add
color-scheme: dark + an explicit dark option list so the popup matches across
WebKit / WebView2 / WebKitGTK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gallery): archetype previews render a noise buzz instead of voice
The Hype Host, The Podcaster and The Vlogger previews played a loud tonal
buzz, not speech. The preview renderer pinned num_step=16 and seed=42; the
"social" sample script at that exact point lands on a degenerate diffusion
trajectory and collapses to a near-pure tone. The blank-audio guard missed it
because the buzz is loud (peaks near -2 dBFS), not silent — so the garbage was
cached and served. The cache key is (instruct, language) only, so it never
self-corrected.
- Bump preview num_step 16 -> 32: reliably converges to speech across the
gallery's instruct/script space (one-time, cached render cost).
- Add a spectral-flatness floor (_is_unusable_audio) so a degenerate tonal
render is rejected like a blank one, reusing the existing retry-on-new-seed
path. Whisper/breathy voices are broadband (high flatness) so they're safe.
Verified: flatness Hype Host 0.001->0.050, Podcaster 0.0002->0.083,
Vlogger 0.004->0.039; whisper control (Calm Guide) 0.239, not flagged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gallery): stop preview playback replaying stale cached audio
Preview audio is re-rendered server-side when an archetype is fixed, but the
URL is stable and the response carried no Cache-Control — so the WebView's
HTTP cache replayed the first clip it ever fetched (e.g. the old buzz)
indefinitely, even after the server file was corrected.
- Frontend: fetch previews with { cache: 'no-store' } so playback always
pulls current bytes.
- Backend: send Cache-Control: no-cache on the preview response so any client
revalidates against the ETag instead of serving a stale clip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(e2e): add Playwright UI smoke + gallery specs and preview-quality unit test
UI testing system to catch regressions like "Use design → Importing a module
script failed" (a dead Vite/module server) and the noisy-preview bug.
- Playwright (frontend/e2e): drives the system chromium (no browser download)
against the Vite dev server. ui-smoke mounts all 12 routable views and fails
on any code-split/import failure, uncaught exception, or ErrorBoundary
fallback. gallery.spec asserts the "OmniVoice Gallery" heading, the dark
facet dropdowns (computed bg = rgba(255,255,255,0.04), not the OS-default
light surface), and that opening an archetype in the Designer mounts the
lazy CloneDesignTab. `bun run e2e`.
- backend/tests/test_archetype_preview_quality.py: unit-tests the
_spectral_flatness / _is_unusable_audio guard with synthetic signals
(tone < threshold < speech < noise; loud tone + silence are unusable) and
pins the render constants. CI-safe — no model/GPU.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(gallery): lucide icons + country flags + card redesign (replace emoji)
- backend archetypes emit lucide-react icon *names* (cross-platform; emoji
render inconsistently across OSes) for use-cases and the 24 featured voices.
- new frontend/src/utils/archetypeIcons.jsx: name→lucide map, accent→country
flag (country-flag-icons, tree-shaken to ~11), per-category color scale,
color-coded avatar tile, and a CSS-animated now-playing equalizer
(prefers-reduced-motion aware).
- card redesign: real elevated surfaces (cards were invisible on the dark bg),
avatar + name + facet sub-line, accent/flag chips, and a footer with
Preview / category-colored "Use voice" / Open-in-Designer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(marketplace): community voice gallery via omnivoice-gallery submodule
Offloads curated + community gallery content to the standalone
debpalash/omnivoice-gallery repo — added here as a submodule for authoring,
loaded at runtime via the jsDelivr CDN so the binary stays small.
Content repo (seeded + pushed separately): manifest.json (24-voice starter
pack generated from the featured archetypes), a JSON schema, CONTRIBUTING,
and GitHub submission templates carrying consent / no-impersonation guardrails.
Backend (api/routers/community.py): configurable sources (env var > file >
default), CDN fetch with offline disk cache, strict validation (invalid
presets and non-allow-listed audio URLs are dropped, so a bad community entry
can neither crash synthesis nor fetch from an arbitrary host), filtering, the
prefilled submit URL, and "use" (preset → archetype render path; voice →
sha256-verified download). 11 tests.
Frontend: a third gallery zone, "Community", reusing the redesigned card, plus
"Submit a preset / voice" buttons opening the prefilled GitHub forms.
Local-first preserved: network only on open/refresh; everything cached; the
built-in generated archetypes need no network, so the gallery is never empty.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(marketplace): address review feedback on the community gallery
- community_use: run the blocking manifest read + voice download in a thread
(asyncio.to_thread) so they don't stall the event loop (greptile P2).
- community_submit_url: validate the `source` override against an owner/repo
pattern, falling back to the configured default (greptile P1 hardening).
- rename the `type` query param to `item_type` (alias="type") so it no longer
shadows the Python builtin (coderabbit).
- frontend submit buttons use the canonical openExternal() (Tauri-aware) instead
of window.open, which doesn't open the system browser in the desktop app.
- lowercase the `currentcolor` CSS keyword (stylelint).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(marketplace): rename useCommunityItem -> addCommunityItem (not a hook)
Avoids the use-prefix on a plain API function (rules-of-hooks smell).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>