Move unbounded Stories and Audiobook project data to revisioned IndexedDB storage with bounded local fallback, durable clear tombstones, migration/recovery safeguards, and deadline-safe persistence before exits and relaunches.
Closes#1636.
Defer and coalesce omnivoice.app and omni_ui persistence behind a 250 ms
quiet window with a 1,000 ms hard maximum, preserving storage schemas,
synchronous pending reads, legacy formats, Factory Reset semantics,
widget read-only ownership, and lifecycle (pagehide/visibilitychange)
durability. Adds scheduler, restore, reset, StrictMode, concurrent-render,
role-ownership, and migration regression tests plus an opt-in
production-bundle responsiveness harness.
Lands #1541 by @bultodepapas (maintainer landing branch; the out-of-scope
attribution-policy commit was dropped).
Co-authored-by: Gius <bultodepapas@gmail.com>
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.
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.
* docs(spec): updates-in-status-bar design (move pill to LogsFooter + Updates panel)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(plan): updates-in-status-bar implementation plan (11 tasks, TDD)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(plan): pin i18n task to scripts/translate_all.py backfill
* feat(update): pure chip + release presentation helpers
* feat(update): listReleases + fetchAppVersion wrappers
* feat(update): transient releasesSlice composed into store
* feat(update): app version + channel in updaterSlice
* feat(update): list_releases Tauri command (GitHub releases)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(update): UpdateStatusChip bar indicator
* feat(update): UpdatesPanel (live row + channel + releases list)
* feat(update): mount chip+panel in LogsFooter, retire floating UpdateBadge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(update): Settings channel switcher shares store value (auto-sync)
* i18n(update): add updates.* keys across 21 locales
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(deps): lock reqwest for list_releases command
* polish(update): a11y radiogroup on channel switch, safer release key, drop dead test seam
Addresses final-review nits (non-blocking): role=radiogroup/radio + aria-checked
on the channel Segmented; key={r.name||r.version} to avoid collisions; remove the
unused vi import + __loader seam in releasesSlice.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(update): i18n the channel-set error + correct flagged updates.* translations (PR #240 review)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(update): add 10s timeout to list_releases HTTP client (PR #240 review)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(update): guard chip Restart against in-flight dub job (greptile P1, PR #240)
The always-visible status chip's one-click Restart (ready state) called
installUpdate→relaunch without the dub-busy guard the panel uses, so a user
with a dub/transcription job running could lose in-flight work. Mirror the
panel's gate: toast update.busy and bail when dubStep === 'generating'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(update): surface channel-switch failures in the Updates panel (greptile, PR #240)
Mirror Settings' error handling: the panel's stable/preview switch now catches a
failed set_update_channel and toasts settings.channel_set_failed instead of an
unhandled rejection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase A: stamp each preview build with a unique monotonic semver prerelease
(<base>-preview.<run_number>) via an ephemeral tauri.conf.json rewrite on the
preview path. Today every preview reported the static 0.3.0, so the updater
never saw a newer version and never delivered preview updates. The prerelease
ordering makes each new preview offer-able and converges to stable when <base>
ships. (Windows MSI ProductVersion strips the prerelease — caveat noted to
verify; mac/linux unaffected.)
Phase B (rollback) is captured as a design spec for review, not implemented:
per-version preview releases + retention, an in-app Preview-builds picker, an
allow_downgrades install path, and the alembic-head data-safety boundary.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(gallery): designed-voice archetype gallery + neutral importer
Adds a browsable library of ~1,100 designed voice archetypes (no real
people), generated from OmniVoice's own voice-design taxonomy and
organized ElevenLabs-style: 24 curated Featured voices plus a
facet-filtered "Browse all" explorer (595 English + 504 Chinese-dialect).
Every generated instruct is built from the validator's own vocabulary, so
none can trigger the issue-#89 synthesis crash.
Backend:
- core/archetypes.py: catalog engine (featured + generated, implausible
combos pruned, stable hashed ids); loads the taxonomy by file path to
stay torch-free in tests.
- api/routers/archetypes.py: categories / list+filter+paginate / get /
preview (render-on-demand + disk cache) / use (materialize a voice
profile). Preview/use reuse generation.py's proven inference path.
- gallery.py: drop the celebrity/character catalog; the importer is now a
neutral, user-driven "My Imports" (paste a URL you have the rights to).
No project-shipped directory of named real people.
Frontend:
- Gallery UI rewrite: Archetypes zone (featured grid + facet filters +
favorites) and My Imports zone; per-card Use voice / Open in Designer.
- api/archetypes.ts, useArchetypes/useArchetypeCategories hooks (v5
placeholderData:keepPreviousData), gallerySlice, en.json keys.
Tests: 27 new (engine contract + API), full backend suite green (72);
CJK guard allowlists the one functional Chinese preview sample.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(gallery): clear new bandit alerts (sha1 + SQL false-positive)
The PR's code-scanning "Bandit" check fires on NEW alerts vs main's baseline.
The archetype work introduced three:
- archetypes.py / core/archetypes.py: hashlib.sha1 used to derive a
deterministic preview-cache key and archetype id (not a security digest) —
flagged B324 (HIGH). Add usedforsecurity=False; the digest is unchanged.
- gallery.py: the UPDATE query interpolates only static, code-controlled column
fragments ("is_favorite = ?", "description = ?"); every user value is bound
via a ? placeholder — flagged B608 (false positive). Annotate `# nosec B608`
with the justification.
Behavior-preserving. Net new bandit alerts after this: zero (verified with
bandit -ll -ii; only main's pre-existing baseline remains).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(gallery): resolve PR #203 CI (SHA-256 ids, log sanitization, CJK allowlist)
All failures stemmed from the initial commit:
- Bandit + CodeQL (2 high): SHA-1 weak-hash on the archetype id and the
preview cache key. These are deterministic identifiers, never security
digests — switched to SHA-256, which the SAST scanners accept.
- CodeQL (log injection): the render-failure logs echoed the raw
user-supplied archetype_id; log the catalog's canonical a["id"] instead
(untainted — it comes from the trusted in-memory catalog, not the request).
- CodeQL (superfluous argument): declare createGallerySlice's StateCreator
store param so its arity matches the 3-arg call site.
- Tests (test_no_hardcoded_cjk): the committed design spec's Chinese-dialect
reference table tripped the guard; allowlist it under documentation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(gallery): clear CodeQL clear-text-logging on archetype render errors
CodeQL's sensitive-data heuristic flags any request-derived value
interpolated into a log call (it persisted even after switching the raw
id to the catalog's canonical a["id"]). Log a static message with
exc_info=True instead: the full traceback still reaches the backend log
for debugging, but no data expression remains for the query to flag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(spec): Stories Editor pro-studio design (line cards, auto-cast, pro output, projects)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(stories): Phase 1 — real audiobook output, cast, persistence, reorder, i18n
First phase of the pro-studio Stories Editor (spec:
docs/superpowers/specs/2026-05-30-stories-editor-studio-design.md). Makes the
editor actually produce audiobooks and remember your work:
- Persistence: storiesSlice (tracks + cast) via zustand persist -> localStorage;
transient fields (generating/audioUrl) stripped on persist; id counter reseeds
from persisted tracks. Dropped the hardcoded sample seed -> clean empty state.
- Cast: editable CastMember[] (name, color, voice) with a Cast panel; each line
picks a character and inherits its voice (per-line override still available).
- Real Generate: exportStoryAudio() stitches every line + [pause] gaps into one
WAV via the Web Audio API (job-less /generate per chunk) with a % progress
indicator and download. Per-line preview already shipped (#176).
- Reorder: native HTML5 drag-and-drop (pure reorder() helper).
- i18n: all Stories strings via t('stories.*') (en + zh-CN).
- Tests: storiesSlice reducers, storyCast resolution, storyExport WAV/concat/
silence, storyReorder. 18 new unit tests.
No DB/alembic; localStorage only. Same-origin + PIN-safe synth (apiFetch). No
new deps. Cross-platform-identical default behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(spec): network sharing + Tailscale remote access design
Same-state LAN sharing via a second in-process uvicorn listener on a
dedicated share port (no restart, model/jobs preserved), PIN-gated for
non-loopback clients, with QR + all-LAN-addresses panel. Tailscale serve
for private remote access. Supersedes the raw 0.0.0.0 default-flip in #125.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(spec): control endpoints reuse existing require_loopback gate
Security review of #157 confirmed the /system router is already loopback-gated
via Depends(require_loopback) (non-spoofable request.client.host). The network
control endpoints inherit it and /system/set-env is auto-protected from the
LAN listener — no new guard needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(network): share-listener module — LAN enumeration + PIN + lifecycle
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(network): loopback-only control endpoints + /system/info sharing fields
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cjk): scan git-tracked files only, not untracked vendored dirs
The no-hardcoded-CJK guard walked the filesystem, so local untracked
vendored experiments (research/voice-pro etc. with JP issue templates)
caused false local failures while CI (committed files) passed. Scan via
git ls-files so local-only and CI behavior match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(network): PIN middleware — gate non-loopback API access when sharing on
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(network): inject X-OmniVoice-Pin globally + capture ?pin= from QR URL
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(network): remote PIN gate on 401
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(network): add qrcode dep for share QR
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(network): footer Local/Network toggle with LAN addresses, QR, copy/open
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(tailscale): CLI status + serve enable/disable + endpoints
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(network): Settings → Sharing & Remote Access panel (LAN + Tailscale)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(network): sharing & remote access guide (LAN PIN/QR + Tailscale)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(network): enable() tears down and raises if the share listener never binds
Defensive guard (spec §7): if the second uvicorn server doesn't reach
'started' (e.g. the share port was taken in the race after the free-port
probe), cancel the task, reset state, and raise — so the API surfaces the
failure and the UI stays Local rather than reporting a dead 'Network' state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(network): use globalThis (not Node global) in client.test.ts for tsc
CI runs 'tsc --noEmit --checkJs false', which type-checks .ts files; Node's
'global' isn't typed there (TS2304). vitest (esbuild) tolerated it locally.
Use globalThis (standard, typed) + cast the mock.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(network): apiFetch leaves opts untouched when no PIN set
The unconditional headers merge changed the request shape for callers with
no headers (e.g. FormData posts), breaking the legacy 'apiPost passes
FormData without Content-Type override' node test. Only spread opts +
inject X-OmniVoice-Pin when a PIN is actually present; otherwise pass opts
through unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>