Commit Graph
22 Commits
Author SHA1 Message Date
Palash DebnathandClaude Opus 4.8 e1850b4bd9 feat(gallery): designed-voice archetype gallery + neutral importer (#203)
* 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>
2026-05-31 12:49:51 +05:30
Palash DebnathandClaude Opus 4.8 672f106f05 feat(update): Stable/Preview update channels with opt-in toggle (#199)
Adds a user-selectable updater release channel (Settings -> About -> Update
channel). Stable (default, every install + launch) tracks tagged vX.Y.Z
releases; Preview tracks the latest main build via a rolling "preview"
prerelease, falling back to stable if a stable release is ahead.

Why Rust: tauri-plugin-updater reads its endpoints from tauri.conf.json and
neither the JS check() nor the plugin's registration Builder can change them at
runtime (verified against the 2.10.1 source). The only runtime-endpoint API is
UpdaterExt::endpoints, so check+install move into two Rust commands that mirror
the plugin's own check/download_and_install -- the Stable path behaves
identically to the JS flow it replaces; only which manifest is consulted
changes. Switching is instant (channel is read per check), no restart.

backend (Rust):
- config.rs: update_channel field (default "stable", VALID_CHANNELS) +
  get/set_update_channel commands.
- updater_channel.rs: channel_endpoints() (preview -> [preview, stable]) +
  check_update / install_update commands; install emits update://progress.

frontend:
- utils/updateChannel.js (+test): single source of truth, normalizeChannel.
- utils/updater.js: routes the badge flow (#198) through the Rust commands via
  the same store contract -- UpdateBadge/App.jsx unchanged.
- Settings About: Stable/Preview segmented toggle, channel-aware endpoint row +
  diagnostics; Check-for-updates honors the live channel.
- i18n en + zh-CN.

release.yml: additive, workflow_dispatch-guarded preview publish to a rolling
"preview" prerelease. The v* tag-push stable path evaluates to its exact prior
values (verified) and is never affected. Preview builds are manual -- no
scheduled CI spend, nothing auto-published.

docs/update-channels.md.

Verified: cargo check (compiles clean), tsc, vitest 162/162, build, CJK guard,
release.yml YAML parses.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 07:49:49 +05:30
Palash DebnathandClaude Opus 4.8 06560e5555 feat(stories): Pro Studio Phase 1 — real audiobook output, cast, persistence, reorder, i18n (#177)
* 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>
2026-05-30 22:00:31 +05:30
Palash DebnathandClaude Opus 4.8 1b08c03da9 fix(docker): runtime API-base override so served deployments reach the backend (#174)
Docker users reported Settings -> Engines failing with 'Failed to load engines:
Failed to fetch'. Two problems: (1) the two API-base resolvers diverged
(client.ts honored VITE_API_URL; apiBase.ts honored VITE_OMNIVOICE_API), and
the docs documented VITE_OMNIVOICE_API -- which the Engines request path
ignored; (2) VITE_* is inlined at BUILD time, so a prebuilt ghcr.io image has
no working runtime override at all for reverse-proxy / split-origin deploys.

- backend: when OMNIVOICE_PUBLIC_API_BASE is set, inject it into index.html as
  window.__OMNIVOICE_API_BASE__ (core/spa_inject.py; validated to a plain
  http(s) URL so it can't break out of the <script>). Unset (default) ->
  StaticFiles serves index.html untouched (same-origin, zero overhead).
- frontend: both resolvers (client.ts _resolveApiBase + utils/apiBase.ts) now
  read the runtime global FIRST, then VITE_OMNIVOICE_API/VITE_API_URL, then
  fall through to same-origin. client.ts also strips trailing slashes and
  recognises __TAURI_INTERNALS__ (parity with apiBase.ts/external.ts).
- docs: docker.md + troubleshooting.md document OMNIVOICE_PUBLIC_API_BASE as the
  runtime override that works on the prebuilt image (the old VITE_OMNIVOICE_API
  docker run -e example never worked -- build-time inlining).
- tests: spa_inject helpers (inject + URL validation/breakout); resolver
  precedence for the runtime global + VITE_OMNIVOICE_API in both test files.

Default same-origin behavior is unchanged on every platform; override is opt-in.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:54:49 +05:30
Palash DebnathandClaude Opus 4.8 40cf9bf0e5 chore: gitignore Spec Kit/GSD local tooling; track network-sharing plan (#172)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 17:47:32 +05:30
Palash DebnathandClaude Opus 4.8 4af5d69111 fix(tailscale): HTTP serve fallback when tailnet lacks HTTPS certs + parallel dev launch (#161)
* fix(tailscale): serve over HTTP when tailnet has no HTTPS certs

Real-world failure: 'tailscale serve --https=443' on a tailnet without the
HTTPS Certificates feature (CertDomains: None) fails with 'error enabling
https feature: 404'. Detect cert availability from status --json and use
--https only when certs exist; otherwise serve over --http (the WireGuard
tunnel encrypts transport anyway). Also surface a clear note/error instead
of the raw 404, and a 'run tailscale up' hint when not running. Verified the
--http path live on a real tailnet. SharingPanel now shows the returned note.

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

* chore(dev): launch app in parallel with backend (drop wait:api gate)

dev/desktop no longer block the Tauri/vite launch on the API being HTTP-ready
— the window appears immediately and the frontend's setup-status check
already retries (30x1s) until the API answers. Matches prod, where the window
shows BootstrapSplash while the sidecar boots. Dev-only; no shipped change.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 12:25:38 +05:30
Palash DebnathandClaude Opus 4.8 fa1503c4eb feat: network sharing (PIN-gated LAN + QR) & Tailscale remote access (#125) (#159)
* 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>
2026-05-30 11:26:16 +05:30
Palash DebnathandClaude Opus 4.8 f757b77a59 docs(install): clarify macOS Gatekeeper "damaged" workaround (#134) (#155)
Reword the macOS install doc's Gatekeeper section so it's findable by the
exact symptom ("App is 'damaged' / can't be opened"), and spell out both the
GUI path (right-click -> Open, or System Settings -> Privacy & Security ->
Open Anyway) and the Terminal path (xattr -dr com.apple.quarantine ...).

Explains WHY macOS shows "damaged" (the build isn't notarised yet, so it gets
quarantined) and why the workaround is safe (downloaded from the official
repo/Releases). Proper fix remains Apple Developer signing + notarisation,
already wired in release.yml behind the documented secrets.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 08:12:35 +05:30
Palash DebnathandClaude Opus 4.8 79473c4c01 docs(#124): document AMD GPU (ROCm) install path (#151)
Detection already works (get_best_device + HSA_OVERRIDE_GFX_VERSION); the
gap was that the default install ships CUDA torch, so AMD users fell back
to CPU with no guidance. Document the opt-in ROCm wheel swap (rocm6.2),
the device-verify one-liner, and the HSA override for unsupported GFX.

Linux-only, opt-in — default cross-platform behavior unchanged. An
installer-integrated env-var-driven wheel selection is a tracked follow-up.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 19:20:23 +05:30
Palash DebnathandClaude Opus 4.8 adf486ee03 chore: set version to 0.3.0 across all sources (+ drop v0.4 references) (#145)
* chore: drop stray v0.4 references — everything ships on the v0.3.0 line

Per the project's versioning rule (no v0.4, no unprompted version chatter):

- backend/main.py + marketplace.py: the app reported version "0.4.0" (ahead of
  even pyproject's 0.2.7 and referencing a forbidden version). Aligned to
  "0.2.7" to match pyproject.toml / tauri.conf.json — a consistency fix, not a
  bump.
- errorDocsMap.ts / indextts/bootstrap.py / _secret_key.py: reworded "v0.4"
  deferral comments to version-agnostic "deferred / later hardening pass".
- docs/install/troubleshooting.md: the "tracked for v0.4" notarization line now
  matches macos.md (signing is wired; activates on the Apple cert secrets).

Note: historical planning records under .planning/ still contain "defer to v0.4"
notes; left as-is (a record of superseded decisions) — CLAUDE.md + the
constitution are the live source of truth.

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

* chore: set version to 0.3.0 across all sources (current dev line)

The current/upcoming version is v0.3.0 (0.2.7 is the prior stable). Bump every
version source so the codebase consistently reports 0.3.0 — the in-code dev
version; the git *tag* still happens later per the release cadence.

- pyproject.toml, frontend/src-tauri/Cargo.toml, tauri.conf.json,
  frontend/package.json: 0.2.7 → 0.3.0
- backend/main.py (FastAPI) + marketplace.py export metadata → 0.3.0
  (these had drifted to a phantom "0.4.0")
- CHANGELOG.md: "[0.2.7] — Unreleased" → "[0.3.0] — Unreleased"
- uv.lock + Cargo.lock reconciled (1-line each) so `--frozen` installs hold.

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

* refactor(version): read app version from package metadata (no more drift)

Greptile (#145): the FastAPI version + marketplace bundle metadata were bare
string literals — they'd go stale-wrong again at the next bump (the exact class
of bug this PR fixes; that's how "0.4.0" happened). Read once from
importlib.metadata.version("omnivoice") via core.version.APP_VERSION, with a
"0.3.0" fallback only for a non-installed source checkout. pyproject.toml is now
the single source of truth for the runtime version.

Tests: tests/test_app_version.py (semver + equals installed metadata). 2 pass.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:08:56 +05:30
Palash DebnathandClaude Opus 4.8 50954f7f43 feat(macos): wire Developer-ID signing + notarization; fix "app is damaged" docs (#134, #72) (#143)
The unsigned DMG triggers macOS Gatekeeper's misleading "app is damaged" block
(#134, #72). Two parts:

- release.yml: pass APPLE_CERTIFICATE / _PASSWORD / APPLE_SIGNING_IDENTITY /
  APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID to tauri-action. It signs +
  notarizes the macOS bundle when these repo secrets are set, and is a no-op
  (today's unsigned build) when they're absent — so this is safe to merge now
  and "activates" the moment the maintainer adds an Apple Developer cert.
- docs/install/macos.md: explain the "damaged" message is Gatekeeper (not
  corruption), give the `xattr -cr` + right-click→Open workarounds, and add a
  "For maintainers" table of the required secrets. Removed the stale "tracked
  for v0.4" line (versioning rule: everything's on v0.3.0).

The in-app error→docs deeplink (GATEKEEPER_QUARANTINE) already targets the
#gatekeeper-quarantine anchor.

Refs #134, #72.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:40:27 +05:30
Palash DebnathandClaude Opus 4.8 c34bc002a3 fix(bootstrap): mirror cascade + system-Python fallback for blocked networks (plan-03, closes #60) (#140)
* fix(bootstrap): mirror cascade + system-Python fallback for blocked networks (#130)

plan-03. First-run bootstrap downloaded managed Python from GitHub with no
mirror and a short retry budget, so a GitHub-blocked/unresolvable network
killed the install dead-on-arrival (#60).

bootstrap.rs (Rust/Tauri):
- apply_uv_http_env(): UV_HTTP_TIMEOUT=120 / CONNECT_TIMEOUT=30 / RETRIES=5 on
  both `uv venv` and `uv sync`.
- `uv venv` cascade: default GitHub → gh-proxy mirror (UV_PYTHON_INSTALL_MIRROR)
  → system Python (UV_PYTHON_PREFERENCE=only-system, only if a system Python
  >=3.11 is detected). First success wins.
- Actionable failure messages (install python.org Python / set a mirror / Clean
  & Retry) instead of a raw uv exit code.

Frontend: BootstrapSplash hint for the GitHub-blocked / can't-download-Python
case. Docs: troubleshooting.md restricted-network section (mirror env vars,
China PyPI index, honest VPN note) — referenced by the remediation text.

Tests: Rust #[cfg(test)] for parse_py_version + apply_uv_http_env (cargo test:
2 passed, crate compiles); docs-drift validator + frontend build green.

NOTE: the restricted-network E2E paths (mirror install, only-system fallback)
need MANUAL verification on a real GitHub-blocked network — not reproducible in
the dev/CI harness. cargo + the unit tests cover compile + the pure helpers only.

Closes #60. Addresses #130, #57, #127.

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

* fix(bootstrap): drop --python 3.11 pin on system-Python fallback (Greptile #140)

system_python_ge_311() accepts 3.12/3.13, but the fallback passed `--python
3.11`, forcing uv to find a 3.11.x interpreter exactly — so a machine with only
3.12/3.13 failed the fallback and wrongly hit the remediation. Drop the pin;
`only-system` + the project's `requires-python = ">=3.11"` lets uv resolve any
compatible system interpreter. cargo test: 2 passed.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:44:41 +05:30
Palash DebnathandClaude Opus 4.8 8162f52c08 ci(security): scanning workflow + CodeRabbit config + sweep design (PR 0) (#135)
* ci(security): add scanning workflow + CodeRabbit config + sweep design

PR 0 of the v0.3.0 stabilization sweep — establishes the automated
review + security gate every subsequent plan PR flows through.

- .github/workflows/security.yml: gitleaks (gating secret scan),
  CodeQL (Python + JS/TS), bandit (SARIF), pip-audit + bun audit.
  Only the secret scan gates; dep/SAST findings are reporting-only
  to stay consistent with the no-ceremony, continuous-to-main cadence.
- .coderabbit.yaml: path filters + constitution constraints encoded as
  review instructions (local-first, cross-platform parity, alembic,
  no secret/home-path leakage). Drafts excluded from auto-review.
- SECURITY.md: document the automated scanning + bot review.
- docs/specs: program design for the full sweep (plan-01..05 + PR triage).

CodeRabbit and Greptile apps are already installed and will review on
PR open.

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

* ci(security): install bandit[sarif] extra; pin JS actions to Node 24

The bandit SARIF formatter ships in the `bandit[sarif]` extra; plain
`bandit` rejects `-f sarif` (exit 2), so no SARIF was written and the
upload step failed. Install via `pipx run --spec 'bandit[sarif]'`.

Also add FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 (mirrors ci.yml) to silence
the Node 20 deprecation warning on checkout/setup-python/upload-sarif.

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

* ci(security): harden per bot review — persist-credentials, upload guard, bun pin

Addresses CodeRabbit + Greptile findings on #135:
- persist-credentials: false on all checkout steps (don't leave GITHUB_TOKEN
  in git config; none of these jobs need authed git after clone). [CodeRabbit]
- continue-on-error on the bandit SARIF upload so a missing SARIF can't fail
  this reporting-only job. [Greptile P1]
- pin bun-version "1.2" — `bun audit` only exists in bun >=1.2.x. [Greptile P2]

Declined: full-SHA action pinning. Meets the major-tag bar set in
.coderabbit.yaml and matches ci.yml/release.yml convention; SHA pinning
belongs in a repo-wide hardening pass with Dependabot, not one file.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 08:04:25 +05:30
Palash DebnathandClaude Opus 4.7 c3695e1668 Phase 2 Plan 02-03: IndexTTS on SubprocessBackend (closes #42) (#98)
Migrates IndexTTS-2 off the in-process import path and onto the
SubprocessBackend primitive shipped in Plan 02-01. Closes issue #42 with
a structural fix — the parent's transformers>=5.3 and IndexTTS's
transformers<5 now live in separate OS processes and can never collide.

* New: backend/engines/indextts/ — sidecar package (__init__.py hosts
  IndexTTS2Backend, main.py is the sidecar entrypoint, bootstrap.py owns
  the 3-step venv probe + lazy uv-based bootstrap).
* services.tts_backend: IndexTTS2Backend's in-process body removed;
  registry resolves the class lazily via a _LazyRegistry indirection +
  PEP 562 __getattr__ re-export. This breaks the import cycle that
  arose when both subprocess_backend and tts_backend tried to import
  each other at module load.
* docs/engines/indextts.md: install walkthrough + venv resolution order
  + common errors (linked from is_available()'s unavailable message).
* tests:
  - test_indextts_backward_compat.py (8) — probe priority, no-spawn
    discipline, HF cache marker preservation (ENGINE-07).
  - test_indextts_sidecar.py (17) — subclass shape, isolation_mode,
    parent-side emotion arbitration (vector/audio/text/description),
    coexist-with-OmniVoice (headline #42 closure), env forwarding.
  - tests/fixtures/mock_indextts_sidecar.py — stdlib-only sidecar
    mimicking the production wire protocol; emits 1 s sine wave.
  - test_issue_fixes.py: two obsolete in-process-conflict tests rewritten
    to assert the new subprocess contract (no indextts.* import in the
    parent).

Hard constraints honored: backend/services/sonitranslate.py and
gpu_sandbox.py are untouched (D1 / D4). Existing v0.2.7 users with
OMNIVOICE_INDEXTTS_DIR and a populated HF cache reach a working
generation with zero re-download and zero re-install.

44 tests pass across the four exercised files. Full suite: 391 passed,
10 skipped, 13 xfailed, 1 xpassed in 57 s. Smoke: 4 passed.

Closes #42. Requirements: ENGINE-02, ENGINE-03, ENGINE-04, ENGINE-07.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:26:46 +05:30
Palash DebnathandClaude Opus 4.7 715766cb04 Phase 1 Wave 2: per-OS install docs + Settings UI + error→docs deeplinks (#94)
* docs(install): per-OS install pages + drift validator + CI gate

Splits the 600-line README install section into self-contained per-OS docs
under docs/install/{macos,windows,linux,docker}.md plus a Top-10
troubleshooting index. Each OS doc is end-to-end: a user opens it and
reaches a working app following only commands inside that file.

Adds:
- docs/install/{macos,windows,linux,docker}.md  (OS-specific install paths)
- docs/install/troubleshooting.md               (top 10 install errors)
- docs/engines/cosyvoice.md                     (closes #55 docs half)
- docs/features/diarization.md                  (pyannote license flow)
- docs/setup/huggingface-token.md               (3-source cascade guide)
- scripts/validate-install-docs.py              (INST-06 docs-drift gate)
- tests/scripts/test_validate_install_docs.py   (B-5: validator self-tests)
- .github/workflows/ci.yml step running the validator on every PR

Implements INST-02 (README routing), INST-03 (macOS Gatekeeper anchor),
INST-12 docs half (Windows torch-compile-oom anchor), DOCS-01..05.

The validator is a one-way diff: every `<!-- validate -->`-tagged line
in docs must appear in scripts/desktop-prod.sh after normalisation
(prompt-prefix strip, CRLF, trailing whitespace, blank-and-comment skip).
A `<!-- validate: skip -->` marker opts out for human-readability blocks.
Its own 10 unit tests catch regressions in the gate itself.

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

* feat(deeplinks): links.py + error_docs_map (Python + TS mirror)

Adds the single source of truth for the project repo URL and the 4-class
error → docs taxonomy that both the in-app ErrorBoundary deeplink button
(Wave 2 Task 3) and the Phase 5 bug reporter will consume.

New:
- backend/core/links.py            — PROJECT_REPO_URL + BLOB_MAIN resolver
                                      (Tauri config first, pyproject fallback)
- backend/core/error_docs_map.py   — lookup(error_class) → docs URL
- frontend/src/utils/errorDocsMap.ts (TS mirror with classifyError helper)
- tests/backend/core/test_links.py + test_error_docs_map.py
- frontend/src/utils/errorDocsMap.test.ts

Resolves checker B-6 (links.py ownership) and Open Question #3 (which fork
the deeplinks resolve to — the Tauri updater endpoint wins, which points
at the desktop app fork debpalash/OmniVoice-Studio).

The TS BASE constant is documented as the second hardcoded URL drift site;
the keys-sync test (`test_keys_match_python_map` equivalent) guards the
4-class taxonomy contract between Python + TS halves.

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

* feat(ui): Settings → API Keys panel + ErrorBoundary docs deeplink

Wave 2 AUTH-03 UI half + ErrorBoundary deeplink wiring.

ErrorBoundary fallback now renders an "Open docs for this error" button
that classifies the thrown Error message (heuristic: pkg_resources → 401 /
HfHubHTTP → WebKit / white screen → quarantine / Gatekeeper) and opens the
matching docs anchor via Tauri shell.open (with a window.open fallback
in browser dev mode).

ApiKeysPanel consumes the Wave 1 resolver state endpoint:
  - 3 source rows (App / Env var / HF CLI) with set/unset indicator,
    masked token preview, whoami username + green check
  - "Active" badge on whichever source is currently serving the cascade
  - App-row only: Save (POST /api/settings/hf-token) +
    Clear (DELETE with optional "also clear HF CLI" confirm dialog)
  - "Test now" button refetches state (invalidates the resolver's
    validation cache via the same endpoint hit)

Panel mounted in the existing Settings → Credentials tab; the legacy
HF_TOKEN row from CREDENTIAL_FIELDS is filtered out so the two paths
don't fight over the same key.

Threat T-02-02: the panel never displays the full token. The masked
value comes from the resolver state endpoint; the full token only
crosses the IPC boundary on Save (POST) and is cleared from local
state on success.

Closes AUTH-03 fully (Wave 1 backend + this Wave 2 UI).

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

* feat(perf): INST-12 Disable torch.compile (Windows) toggle (backend + UI)

Wave 2 Task 4 — full INST-12 delivery per checker B-2/B-7 v0.3.0 fat-release
decision. Both the docs half (windows.md anchor, shipped in earlier commit)
and the runtime toggle are now in Phase 1.

Backend:
- backend/services/settings_store.py: adds get_text/set_text helpers for
  non-secret config (refuses to write to the encrypted hf_token key).
- backend/api/routers/settings.py: GET + PUT
  /api/settings/perf/torch-compile-disabled, both under the existing
  loopback guard (threat T-02-04).
- backend/services/engine_env.py: new `build_engine_env()` helper that
  centralises HF_TOKEN/YOUR_HF_TOKEN injection from the 3-source resolver
  AND injects TORCH_COMPILE_DISABLE=1 when the flag is set on win32.
  Phase 2 SubprocessBackend launchers should adopt the same helper.
- backend/services/sonitranslate.py: migrated to engine_env.build_engine_env()
  while preserving the source-level `env["HF_TOKEN"]` sentinel that
  test_sonitranslate_module_uses_resolver checks.

Frontend:
- frontend/src/components/settings/PerformancePanel.{jsx,css,test.jsx}:
  toggle UI with the explainer for #65; renders disabled with a "not
  applicable" badge on macOS/Linux.
- frontend/src/pages/Settings.jsx: mounts the panel into the Credentials
  tab alongside the API Keys panel.

Tests:
- tests/backend/test_perf_settings.py: 7 backend tests (default state,
  PUT persistence, T-02-04 non-loopback rejection, settings_store round-
  trip, env injection on win32, NO injection on macOS/Linux, NO injection
  when disabled).
- frontend PerformancePanel.test.jsx: 5 tests (renders from GET state,
  PUT on toggle, disabled on non-Windows platforms, pre-enabled state).

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

* docs(planning): Wave 2 SUMMARY + REQUIREMENTS status updates

- .planning/phases/01.../01-02-SUMMARY.md: full implementation report
  per template (truths, commits, tests, deviations, drift-site
  acknowledgments per W-3, launcher seam name for Phase 2,
  taxonomy keys for Phase 5).
- .planning/REQUIREMENTS.md: flips Wave 2 closures to Done:
    AUTH-03, INST-02, INST-03 (docs half), INST-06, INST-12,
    DOCS-01..05.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 06:22:10 +05:30
Palash DebnathandClaude Opus 4.7 766e2f7284 Phase 0 — Gates: cross-platform CI matrix + regression fixture + release smoke (#71)
* docs: initialize OmniVoice stabilization milestone project

* chore: add project config (yolo + balanced)

* docs: domain research for stabilization milestone

* docs: define v1 requirements for stabilization milestone

* docs: add GGUF + singing engine spike requirements (Phase 4 new)

* docs: roadmap revision + CLAUDE.md (7 phases, 62 reqs, +GGUF/SING spikes)

* docs(phase-0): add Gates phase RESEARCH.md

Phase 0 research synthesizes the cross-platform CI matrix, frozen
omnivoice_data fixture, installer post-build smoke, SHA-256 checksum
publishing, and PR-template extension into copy-paste-ready YAML and
Python snippets composed entirely from existing in-repo patterns.

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

* docs(phase-0): add Gates phase CONTEXT, PATTERNS, and PLAN

Phase 0 — Gates is the hard pre-condition for v0.3.x stabilization.
Lays cross-platform CI matrix (macos-14/windows-2022/ubuntu-22.04),
regression fixture (≤200 KB), installer smoke on tag push, SHA-256
checksums in release body + per-OS SHA256SUMS-*.txt assets, PR
template with RC cadence + fixture line, and the open-PR landing
for #51.

Plan covers GATE-01..06; structured into 7 slices (A–G) with explicit
Slice C → Slice G dependency reordering so the new smoke-matrix lands
on main before PR #51 (CONTEXT.md L86 interleave decision).

Plan-checker iteration 2: APPROVED — all 3 BLOCKERs + 3 MAJORs from
iteration 1 resolved (file truncation/Slice-G missing, GATE-06 sibling
PR verification, Slice C ordering, Truth #5 wording, macOS Tauri
WebView avoidance per Pitfall #5, Windows taskkill per Pitfall #2).

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

* test(00-gates): seed regression fixture (GATE-01)

- scripts/seed-test-fixture.py — deterministic builder for tests/fixtures/omnivoice_data/
  - wipes + rebuilds; fixed created_at=1700000000.0; all-zero PCM for byte-deterministic diffs
  - calls backend.core.db.init_db() directly (alembic versions/ is empty — see CONTEXT.md)
  - checkpoints WAL → DELETE on close so no -shm/-wal sidecars pollute git status
  - exits non-zero if fixture > 200 KB
- tests/fixtures/omnivoice_data/{omnivoice.db, README.md} — 8-table empty DB + 1 voice_profiles row
- tests/fixtures/omnivoice_data/voices/test-voice/{profile.json, sample.wav} — 1-sec 24 kHz mono silence
- .gitignore — explicit allow-list (!tests/fixtures/omnivoice_data/**) so the existing
  omnivoice_data/, *.db, *.wav patterns don't hide the fixture from git

Verifies: du = 144 KB on disk; sqlite_master lists 8 init_db tables + sqlite_sequence;
voice_profiles has exactly 1 row id='test-voice'; 0 rows in generation_history.

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

* test(00-gates): add tests/smoke/test_boot_smoke.py (GATE-01)

- tests/smoke/__init__.py — package marker so pytest treats tests/smoke/ as a module
- tests/smoke/test_boot_smoke.py — 4 in-process FastAPI TestClient smoke tests:
    * test_health_returns_ok — /health returns 200 + {status:ok, device:...}
    * test_profiles_endpoint_lists_fixture_voice — /profiles surfaces the seeded
      test-voice row (validates OMNIVOICE_DATA_DIR wiring → DB_PATH → init_db schema)
    * test_system_info_includes_data_dir — /system/info resolves data_dir
    * test_history_endpoint_empty — /history reaches DB and returns []
  Test isolation env vars (OMNIVOICE_MODEL=test, OMNIVOICE_DISABLE_FILE_LOG=1)
  set at module top BEFORE any backend import — pattern from tests/test_router_smoke.py.
  Fixture is copied to a per-session temp dir so the test never mutates the
  checked-in artifact (SQLite file-change counter + runtime subdirs like dub_jobs/
  would otherwise dirty `git status` after every run).
  Failure mode: if tests/fixtures/omnivoice_data/ is missing, pytest.fail at
  import time with the regenerate command.
- .gitignore — tighten the GATE-01 allow-list to ONLY the seed-produced files
  (README.md, omnivoice.db, voices/test-voice/profile.json, sample.wav).
  Prevents future runtime subdirs the backend may create under the fixture
  from being accidentally committed.

Verifies: `uv run pytest tests/smoke/ -q --tb=short` → 4 passed in 1.31 s
(target was < 30 s). `git status` clean after a test run.

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

* docs(triage): record post-planning GitHub state — PR #62, new issues, OOS deferrals

- GATE-06: mark #53 + #61 merged (2026-05-16); add #62 (Wave 1 quick wins) to gate set
- INST-01: note PR #62 implements setuptools pin (closes #58)
- INST-04: note PR #62 lands README docs for #56 workaround
- INST-12: new requirement for #65 Windows Triton/torch.compile OOM (filed post-planning)
- Out of Scope: defer #67/PR #68 (audio effects), #64 (custom model dir),
  PR #66 zh-CN (i18n milestone), #63 (empty-template bug)

PR #62 is the user's own Wave 1 work landed as a separate PR while
GSD planning ran in parallel. Merging it eliminates duplicate work
in Phase 1.

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

* ci(00-gates): add cross-platform smoke matrix (GATE-02)

- New smoke-matrix job on macos-14, windows-2022, ubuntu-22.04
- needs: test, fail-fast: false, timeout-minutes: 10
- Pinned actions: checkout@v4, setup-python@v5, setup-uv@v3 (cache enabled)
- Per-OS ffmpeg + libsndfile install (brew/choco/apt via awalsh128 cache)
- UV_HTTP_TIMEOUT=120, UV_HTTP_RETRIES=5 for restricted-network resilience
- Narrow scope: uv run pytest tests/smoke/ -q --tb=short
- Existing `test` and `tauri-cross-platform` jobs untouched

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

* ci: add workflow_dispatch to ci.yml so smoke-matrix can run on feature branches

* feat(00-gates): add --health-check CLI flag to backend entrypoint (GATE-03)

- argparse on __main__ block; --health-check boots uvicorn in a daemon
  thread and polls http://127.0.0.1:3900/health every 5s for up to 60s.
- Prints 'OK — /health responded 200 after Ns' and exits 0 on first 200.
- Prints 'FAIL — /health did not respond 200 within 60s' to stderr and
  exits 1 on timeout. Default invocation behavior unchanged.
- No new deps (stdlib argparse/threading/time/urllib.request/sys + uvicorn).
- Consumed by per-OS installer-smoke step in .github/workflows/release.yml.

Verified locally: exits 0 in 5s against tests/fixtures/omnivoice_data/.

* ci(00-gates): add per-OS installer smoke to release.yml (GATE-03)

Adds three matrix-leg-specific steps after 'Build + release (Tauri)',
each gated by runner.os with timeout-minutes: 5:

- macOS (macos-14): hdiutil attach DMG → locate bundled Python backend
  inside *.app/Contents (NOT the Tauri WebView shell — RESEARCH Pitfall
  #5: WebView hangs on headless runners) → invoke --health-check →
  hdiutil detach. Falls back to *.app/Contents/Resources and hard-fails
  with a directory listing if no backend binary found.

- Windows (windows-2022): msiexec /quiet install → find backend.exe
  under 'C:/Program Files/OmniVoice Studio' → invoke --health-check in
  background, wait, then taskkill //F //T //PID to cleanup orphaned
  PyInstaller child processes on port 3900 (RESEARCH Pitfall #2).

- Linux (ubuntu-22.04): --appimage-extract (no FUSE on GH runners),
  locate binary or AppRun, run under xvfb-run -a.

Bundle-only regressions (PyInstaller missing-module, Tauri sidecar
path mismatch) are invisible to ci.yml's in-process smoke matrix —
this step closes that gap before any release is published.

Verified: YAML parses; all three steps present; gating + timeout
correct; Pitfall #2/#5 mitigations preserved.

* ci(00-gates): publish SHA-256 checksums in release body + as asset (GATE-05)

- Add 'Compute SHA-256 checksums' step writing SHA256SUMS-<label>.txt
  per matrix leg using native shasum/sha256sum (Git Bash on Windows).
- Add 'Append checksums to release + attach SHA256SUMS file' step using
  softprops/action-gh-release@v2 with append_body: true so the hashes
  land in the release body alongside tauri-action's content (not
  replacing it) and the file is uploaded as a release asset for
  'shasum -c SHA256SUMS-<label>.txt' verification.
- Both steps gated by 'github.event_name == push && refs/tags/v*' so
  workflow_dispatch dry-runs do not attempt to attach to a non-existent
  release (per CONTEXT.md L70 + RESEARCH Pitfall #7 deferral of any
  aggregate cross-leg SHA256SUMS job).
- fail_on_unmatched_files: true to surface path-resolution errors loudly.

* docs(00-gates): document RC cadence + regression-fixture check in PR template (GATE-04)

* docs(setup): add HF token persistence guide for macOS/Windows/Linux (DOCS-05)

Covers two persistent paths:
- Method A — canonical ~/.cache/huggingface/token via huggingface-cli login
- Method B — shell env var (~/.zshrc / ~/.bashrc / Windows User scope)

Documents the v0.2.7 "session only" in-app behavior + notes that
Phase 1 AUTH-03 will make in-app pastes write to the canonical file.

Bundled with Phase 0 PR per user request. Strictly DOCS-05 scope —
zero code changes, no engine touches.

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

* spec(auth): redesign HF token resolution as 3-source cascade with fallback (AUTH-01..06)

Replaces the env_store.py file-based design with a SQLite-backed app
store + cascade resolver that checks app → env var → ~/.cache/huggingface/token
in priority order, with automatic fallback to next source on HTTP 401.

User-explicit design decision:
- App-stored token (SQLite settings table, AES-GCM encrypted) wins
- Env var ($HF_TOKEN) second
- Global huggingface-cli login file third
- All three sources visible in Settings → API Keys with "Active" badge
- Save action populates BOTH app store AND canonical HF file (defense in depth)

New requirement:
- AUTH-06 — on 401, auto-retry next source in cascade before erroring

Also: traceability count corrected (62 → 74 — undercount at planning +
INST-12 + AUTH-06 added post-planning). All 74 v1 reqs mapped.

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

* fix(auth): backend recognizes HF token from canonical file, not just env var

Two call sites were only checking $HF_TOKEN env var, missing the canonical
~/.cache/huggingface/token file written by `huggingface-cli login` (or the
app's future Save action):

- system.py `/system/info` `has_hf_token` flag — UI showed "No HF token"
  even when `huggingface-cli login` had populated the file.
- model_manager.get_diarization_pipeline — pyannote diarization silently
  returned None when only the canonical file was set. This is the bug
  behind issue #35 (speaker diarization setup failure).

Both fixes use the same pattern: env var > huggingface_hub.get_token()
(which reads the canonical file). Adds a local _has_hf_token() helper
to system.py with a comment marking it as prelude to the AUTH-01..06
cascade (Phase 1 token_resolver.py will layer SQLite app-store on top).

Closes #35 sub-issue (canonical token invisible to diarization).
Cross-cuts AUTH-02 + AUTH-06 design for Phase 1.

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

* feat(dictation): make pill-widget mode reachable from GUI + scripts (INST-13)

The dictation widget infrastructure shipped in PR #40 but was only reachable
via the undocumented --pill CLI flag. Adds three discovery paths:

1. Tray menu: "Switch to Dictation Widget" (studio mode) — saves
   launch_as_widget=true to config, relaunches with --pill, exits current.
   Mirrors the existing "Open Studio" path in pill-mode tray.

2. Persistent config: AppConfig.launch_as_widget (bool, default false). Read
   at startup via load_config_pre_app() (uses dirs-next, no AppHandle
   required). CLI --pill still takes precedence when explicitly passed.

3. Tauri commands: get_launch_as_widget / set_launch_as_widget for the
   Phase 2 Settings UI to bind a checkbox to.

4. Scripts: bun desktop-prod:pill / desktop-prod:run:pill — forward --pill
   to the bundled app launch. macOS uses `open -n --args` to spawn fresh
   instance with the flag.

Closes the GUI half of INST-13. Phase 2 closes the Settings UI half.

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

* fix(dictation): show widget unconditionally on pill-mode launch + visible Suspense fallback

Before: pill mode set up correctly but the widget window stayed hidden
until ⌘⇧Space was pressed. New users saw absolutely nothing on launch
(no main window, no dock icon, hidden widget) and assumed the app
failed. If global-shortcut Accessibility permission wasn't granted,
they had no path to discover the widget at all.

Two changes:

1. lib.rs: in pill_mode_setup, explicitly show + position + focus the
   widget window after hiding main. With per-call error logging so we
   can diagnose failures (and a clear error log if widget window
   wasn't created at all — points at tauri.conf.json regression).

2. main-app.jsx: Suspense fallback was `null`, which combined with
   widget's transparent+decorations:false config made any lazy-import
   delay or failure invisible. Now renders a dark pill saying
   "Loading dictation…" so even if CaptureWidget lazy-import stalls,
   the user sees the window exists.

Studio mode behavior unchanged — widget stays hidden until hotkey
or tray click triggers it (existing show() call in the shortcut/
menu handlers is preserved).

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

* fix(dictation): create widget window programmatically; Tauri 2 silently dropped config-array creation

Root cause: declaring the widget window in tauri.conf.json's app.windows[]
silently failed in Tauri 2 — get_webview_window("widget") returned None
even though the config was syntactically valid. Probable culprit was the
transparent + decorations:false + visible:false combo, but Tauri offered
no error message either at startup or via webview_windows() enumeration.

Diagnosed by adding webview_windows() enumeration logging at setup start
(only ["main"] ever appeared) and a programmatic WebviewWindowBuilder
fallback that surfaces real Result errors.

Fix:
- tauri.conf.json: widget entry now has `create: false` to make the
  config-vs-programmatic handoff explicit.
- lib.rs setup(): call WebviewWindowBuilder::new(app, "widget", ...).build()
  with the exact same surface attributes the config used to declare.
- capabilities/default.json: include "widget" in windows array so the new
  window inherits the same Tauri permissions as main.
- tauri.conf.json: remove the invalid `"url": "/?window=widget"` field —
  WebviewUrl::App takes a path only, query strings aren't supported.
  Both windows now load index.html.
- main-app.jsx: replace URL-query-based widget detection with
  getCurrentWindow().label === 'widget' via @tauri-apps/api/window. This
  is the Tauri 2-recommended pattern for multi-window apps and works
  regardless of URL routing.

Closes the immediate UX bug behind the dictation widget being invisible.
Builds cleanly + manually verified: pill widget visible on screen at
top-center after `bun desktop-prod:pill`.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:34:32 +05:30
Palash Debnath 545b39c912 feat: Scalar API docs, community health files, Quickstart cards (#41)
* feat: Scalar API docs, community health files, Quickstart cards, GHCR Docker

Backend:
- Replace Swagger UI with Scalar at /docs (scalar-fastapi)
- Add OpenAI-compatible /v1/audio endpoints (openai_compat router)
- Add TTS streaming endpoint (tts_stream router)
- Add voice marketplace router (marketplace)
- Update TTS backend registry

Frontend:
- Refine CaptureWidget, WaveformTimeline, App layout
- CSS polish and index.css updates

Community health:
- SECURITY.md — vulnerability reporting policy
- CODE_OF_CONDUCT.md — Contributor Covenant v2.1
- .github/FUNDING.yml — GitHub Sponsors
- .github/ISSUE_TEMPLATE/ — bug report + feature request
- .github/pull_request_template.md — PR checklist

README:
- Quickstart redesigned as 3-column progressive cards
- Docker section updated with GHCR pull instructions
- API Docs row added to service table

Infra:
- scalar-fastapi added to pyproject.toml + uv.lock
- research/ added to .gitignore

* refactor: clean up documentation and logging while enhancing desktop packaging dependencies and capture UI performance.

* fix: address CodeRabbit review — streaming, escaping, thresholds

Backend:
- marketplace: stream zip entries via ZipFile.open()/copyfileobj, add 100MB
  upload cap, fix raise-from exception chaining (OOM prevention)
- openai_compat: _encode_audio returns actual file ext so Content-Disposition
  matches real format; forward non-profile voices when DB row not found
- tts_stream: send 'start' frame after generation so sample_rate is real;
  forward non-profile voices on DB miss
- capture_ws: split MIN_BUFFER_BYTES into separate partial/final thresholds
  so short utterances (<2s) still get transcribed

Frontend (Tauri):
- lib.rs: tray 'dictate' now toggles start/stop based on widget visibility
- commands.rs: XML-escape exe path in LaunchAgent plist, shell-quote in
  .desktop Exec line to prevent injection from special-char paths
- CaptureWidget.css: fix Stylelint violations (empty lines, font-family quotes)
2026-05-04 10:57:37 +05:30
Palash Debnath 0ecbf136e7 refactor: codebase cleanup & root folder reorganization (#38)
refactor: codebase cleanup & root folder reorganization
2026-05-03 03:12:07 +05:30
debpalash f8b4673e1f fix(ui): Fix segment row layout collapse, memory bugs, enterprise page, and UI enhancements 2026-04-27 00:21:47 +05:30
debpalash fc76e79ff8 feat: setup wizard, donate page, CI fixes, performance optimizations, and style extraction
- Implement donate page and migrate API fetching to react-query hooks
- Add setup wizard for batch job management and voice clip editing
- Refactor setup router into package (wizard, models, download sub-modules)
- Fix 9 CI test failures from setup router refactor
- Fix cross-device link error in prefs.py atomic writes
- Fix event loop mismatch in export test fixtures
- Modernize README with architecture diagram and 13 app screenshots
- Defer per-segment disk writes in dub_generate for ~6s faster dubs
- Extract 45 inline styles from Launchpad, KeyboardCheatsheet, DubSegmentRow
- Add playwright dev dep and screenshot capture script
2026-04-26 16:47:00 +05:30
debpalashandClaude Opus 4.7 d1fd0e5fcb chore: release docs, pin python version, drop stale tarball
- Add docs/RELEASING.md, DESKTOP_RELEASE.md, desktop-build.md for
  release workflow and packaging steps
- Relocate next.md → docs/specs/studio-v1.md (scratch → formal spec)
- Pin Python version via .python-version
- Ignore research/ clones in .gitignore
- Remove stale omnivoice-studio-20260421-1834.tar.gz snapshot

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:48:45 +05:30
debpalash eb2e9988f6 chore: flatten project by moving all contents from submodule to root 2026-04-10 02:53:23 +05:30