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

Adds a separately identified per-user MSI and updater channel, non-administrator install/uninstall verification, and fail-closed WebView2 handling for current-user installs.
2026-09-01 22:03:33 +05:30
Palash Debnath e1101255bd fix: make WebView2 MSI bootstrap controllable 2026-08-30 21:25:12 +05:30
Palash DebnathandClaude Fable 5 420bc73e78 fix(release): harden the AppImage repair — pinned tooling, final-writer manifest (#1545)
* fix(release): harden the AppImage repair step

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

Tooling fixes this turned up:

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

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

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

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

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

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

Review findings on #1517:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

CodeRabbit, #1517:

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

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 23:29:19 +00:00
debpalash 64973d1829 fix(release): validate wrapped AppImage launcher 2026-08-12 11:24:01 +00:00
debpalash 9f2c8ac0a4 fix(pockettts): close license review findings 2026-08-10 09:24:37 +00:00
debpalash fb78d87ad9 test(appimage): verify packaged WebKit marker 2026-08-10 04:32:08 +00:00
debpalash 5be4a26903 fix(appimage): ship the compatibility launcher 2026-08-09 23:12:04 +00:00
Palash Debnath 5cab8e0149 feat: rename the product to VoiceStudio (previously OmniVoice-Studio)
Renames what users see. The app, the installers, the window title, the
docs and all 21 locales now say VoiceStudio, with "(previously
OmniVoice-Studio)" noted near the title of each doc surface so people
recognise it.

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

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

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

Linux .deb users install a new package name and should apt remove
omnivoice-studio; that note is in the changelog.
2026-08-07 01:30:58 +05:30
Palash Debnath 9877e7c218 fix(ci): bind the preview manifest to its run, not to sibling upload times (#1387)
The nightly preview build had been refusing to publish its own healthy manifest since 2026-08-05 — all four matrix legs green, but the macOS bundles uploaded a few minutes ahead of the slowest versioned artifact, and the freshness check compared the version-less darwin tarballs against their siblings with two minutes of slack. Legs finishing minutes apart is normal, so the comparison itself was wrong, and Preview-channel users quietly stopped getting builds.

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

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

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

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

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

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

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

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

Bot review findings on this branch, all fixed here:

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Pinkers01 <pinky.bouw@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 17:41:45 +05:30
Palash DebnathandClaude Opus 5 602ea6f53e ci(release): stop the macOS preview updater bundle colliding with itself (#1281)
* fix(engines): show the under-provisioned-VRAM warning instead of discarding it

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:20:30 +05:30
mergetestandClaude Fable 5 f609f57fbc chore(release): codify all deployment channels as release rules; preview always builds from main
A release now has an explicit channel checklist (docs/RELEASING.md §5b):
GH Release + stable updater manifest, preview updater channel, GHCR +
Docker Hub in both CUDA and ROCm flavors, and the Docker Hub overview
sync (whose continue-on-error step must be verified by step log — it
403s silently on tokens without description-edit scope).

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 16:12:31 +05:30
2d9ebe350a feat(analytics): wire posthog-js — consent-gated, autocapture OFF (#1123)
* feat(analytics): wire posthog-js — consent-gated, autocapture OFF

The owner supplied the standard snippet:

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

Refs #1089

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

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

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

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

Guard green.

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

---------

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

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

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

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

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

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

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

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

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

This reverts commit 7489bef085.

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

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

---------

Co-authored-by: mergetest <test@local>
2026-07-01 20:18:15 +05:30
5a28c04c44 chore(version): make frontend/package.json the single source of truth (#503)
Five hand-maintained version literals (pyproject, Cargo.toml, tauri.conf.json,
package.json, version.py) drifting is what shipped a 0.3.6 build calling itself
0.3.5 (package.json lagged; the frozen backend's literal lagged). Collapse to
one canonical source.

- frontend/package.json is canonical: vite already injects __APP_VERSION__ from
  it (first-run setup footer + bug reports).
- tauri.conf.json now reads its bundle version from it ("version":
  "../package.json", a supported Tauri v2 feature) — the MSI/dmg/updater version
  can no longer drift from the UI. Removes the most error-prone literal.
- Cargo.toml + pyproject.toml + version.py's _FALLBACK_VERSION remain as
  toolchain-required CI-guarded mirrors, bumped in lockstep from the canonical.
- release.yml: the preview-stamp and version-bump jobs now read/write
  package.json (the canonical) and no longer touch the derived tauri.conf.json.
- tests/test_app_version.py: new test_tauri_version_derives_from_package_json
  guards the path; the lockstep test now checks the mirrors against the
  canonical package.json.
- CLAUDE.md versioning rule updated to document the single-source model.

6 version tests pass.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:50:39 +05:30
0337e15a2e fix(version): frozen backend reports real version, not the 0.3.5 fallback (#501)
The desktop app's About panel, /health, /system/info, diagnostics, bug reports,
and exported persona/marketplace bundle metadata all read core.version.APP_VERSION.
In a synced env that resolves from package metadata (correct), so CI and the
lockstep test were green — but the PyInstaller-frozen backend has no omnivoice
.dist-info, hits PackageNotFoundError, and fell back to a hardcoded
APP_VERSION = "0.3.5". The version-bump job never touched that literal, so every
0.3.x desktop build has been reporting 0.3.5 regardless of its real version.

Fix (belt and suspenders, so it can't recur):
- backend.spec: copy_metadata('omnivoice') so importlib.metadata resolves in the
  frozen build — the primary path now works there too.
- backend/core/version.py: resolution chain is metadata → pyproject (walked up,
  correct for raw source checkouts) → a named _FALLBACK_VERSION literal as last
  resort (no longer the only fallback).
- tests/test_app_version.py: _FALLBACK_VERSION joins the lockstep (now FIVE
  sources); + a test that the fallback resolves to pyproject, + a test that
  backend.spec copies the metadata (so the frozen path can't silently regress).
- release.yml version-bump: also bumps _FALLBACK_VERSION so the lockstep guard
  never reddens main after a release.

Already-shipped binaries can't be fixed, but every build from here (tonight's
preview, the next stable) reports its real version. 5 version tests pass.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:05:11 +05:30
e58106d552 fix(updater): preview channel builds nightly from main + prerelease/parity guards (#500)
The Preview update channel was effectively dead: its only build trigger was a
manual workflow_dispatch, so "preview = main" was never enforced — the live
preview manifest was stuck at 0.3.5-41 (June 7) while main moved to 0.3.7.
It also shipped two latent hazards: the `preview` GitHub release had drifted to
isPrerelease=false (a non-prerelease `preview` is eligible to become GitHub's
"Latest" — the exact URL the *stable* updater reads, so it could hijack the
Stable channel), and its updater manifest dropped darwin-x86_64 (Intel-Mac
preview users silently got no updates — a cross-platform-parity breach).

Changes (release.yml):
- Add a nightly `schedule` (07:00 UTC) that rebuilds the rolling `preview`
  prerelease from main. A new `preview-gate` job no-ops the 4-platform matrix
  on nights when main didn't move, so idle days cost only a ~30s gate job.
- Centralize the preview-vs-stable decision in `preview-gate.outputs.is_preview`
  (schedule OR workflow_dispatch+publish_preview), consumed by the stamp step,
  tauri-action, and preview-notes — replacing the repeated inline conditions.
- Harden the prerelease flag: preview-notes' `gh release edit` now re-asserts
  `--prerelease` every run, and a new post-publish step fails the run if the
  preview release isn't a prerelease or its manifest is missing any platform
  stable ships (catches the Intel-Mac regression in CI).

Docs (docs-sync): update docs/update-channels.md — previews are no longer
"manual / no scheduled spend"; they build nightly from main (+ on demand).

The live `preview` release was re-flagged prerelease out-of-band to close the
hazard immediately; this makes it recurrence-proof.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:04:27 +05:30
6044765f09 chore(version): bring frontend/package.json into the version lockstep (0.3.6) (#497)
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>
2026-06-16 17:59:44 +05:30
101cf2a6e7 ci(release): reinstate macOS Intel (x86_64) build target on macos-15-intel (#342)
Intel MacBook users had no installable artifact: the release matrix only
built aarch64-apple-darwin, and Rosetta 2 cannot run arm64 apps on Intel
(it only translates the other direction) — the rationale in the old
"Intel dropped" comment was backwards. Refs #279.

- Add a native `macos-15-intel` matrix leg (GitHub's designated x86_64
  migration target after macos-13 retired Dec 2025; standard image,
  supported through Aug 2027) building --target x86_64-apple-darwin
  with app,dmg,updater bundles.
- Existing per-TRIPLE steps already carry x86_64-apple-darwin cases
  (uv sidecar tar.gz, evermeet.cx ffmpeg/ffprobe — x86_64 Mach-O,
  natively correct on Intel), so the leg flows through the same
  Bundle/Build/Smoke/Verify steps untouched.
- The PR #290 signing path applies automatically: ad-hoc seal from
  tauri.conf.json signingIdentity "-", opt-in APPLE_* stable signing,
  and scripts/verify-macos-signing.sh both gated on runner.os == macOS.
- tauri-action includeUpdaterJson merges the new darwin-x86_64 platform
  key into latest.json alongside darwin-aarch64, so Intel installs
  auto-update on both Stable and Preview channels.
- docs/install/macos.md: table telling users which DMG (aarch64 vs x64)
  matches their Mac, and the from-source fallback for old releases.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:58:28 +05:30
Palash DebnathandClaude Opus 4.8 948bc76543 macOS: ad-hoc sign so users open without Terminal + signing/notarization verification (#290)
* chore(release): add macOS signing/Gatekeeper/notarization verification

Codify and enforce the macOS build-signing requirements. The release
pipeline built bundles and had opt-in Apple signing, but never verified
codesign/spctl/notarization — unsigned or broken bundles could ship silently.

- scripts/verify-macos-signing.sh: runs codesign --verify --deep --strict,
  spctl Gatekeeper assessment, per-nested-Mach-O signature check, stapler
  validate, and (opt-in) notarytool history. Report-only by default (unsigned
  dev/preview is expected); --require-signed fails on any unsigned/un-notarized
  component so a broken release stops instead of publishing an unsigned artifact.
- scripts/macos-dev-unquarantine.sh: local-dev-only quarantine stripper, with a
  loud "never a substitute for notarization" warning.
- release.yml: new "Verify macOS signing" step on the macOS leg — report-only on
  unsigned paths, STRICT on the opt-in signed stable path (same condition as
  "Configure Apple signing"), so signing/notarization failures fail the job.
- docs/macos-signing-verification.md: the canonical 10-point requirements +
  how-to-verify checklist, cross-linked to docs/install/macos.md and DESKTOP_RELEASE.md.

Verified locally: report-only PASS (exit 0) and --require-signed FAIL (exit 1)
against the real unsigned debug .app; release.yml parses as valid YAML.

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

* feat(macos): ad-hoc sign bundle so users open it without Terminal (no Apple ID)

The "app is damaged and can't be opened" error is caused by a broken/incomplete
code-signature seal (codesign --verify failed: "code has no resources but
signature indicates they must be present") on the quarantined download — there
is no GUI bypass for that variant on modern macOS, forcing users to run `xattr`.

Give the bundle a VALID ad-hoc signature at build time (free, no Apple Developer
account) via tauri.conf.json bundle.macOS.signingIdentity = "-". Verified through
a real `tauri build`: the produced .app is now flags=adhoc,runtime and passes
codesign --verify --deep --strict. A valid seal flips the Gatekeeper prompt from
the un-bypassable "damaged" to the GUI-bypassable "unidentified developer", which
users clear with right-click → Open / Settings → "Open Anyway" — no Terminal.

Still not notarized (that needs the paid Apple ID), so there's a one-time
confirmation rather than a clean double-click. The opt-in Developer-ID path is
unchanged: APPLE_SIGNING_IDENTITY (env) overrides the "-" default on the signed
stable release.

- tauri.conf.json: signingIdentity "-" (ad-hoc default).
- verify-macos-signing.sh: detect ad-hoc tier; report the no-Terminal GUI path
  in report-only, still FAIL it under --require-signed (production must notarize).
- docs/install/macos.md: lead the Gatekeeper section with right-click → Open;
  keep xattr as fallback for the harsher "damaged"/corrupted-download case.
- docs/macos-signing-verification.md: signing-tiers table + ad-hoc default note.
- release.yml: comment the ad-hoc default + env override on the signed path.

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-06-11 13:26:04 +05:30
Palash DebnathandClaude Fable 5 a949b2c78a chore(version): main is always latest release + 1 — rule, bump to 0.3.6, Docker retag, auto-bump job (#338)
Versioning hard rule (owner-set 2026-06-11), codified in CLAUDE.md:
- main's three version sources (tauri.conf.json, Cargo.toml,
  pyproject.toml) always carry last release + 1 patch; bumped 0.3.5 ->
  0.3.6 now.
- Preview builds stamp BASE-N which now sorts ABOVE the last stable
  (0.3.6-N > 0.3.5) — the updater ordering becomes natural and the
  Windows MSI ProductVersion wrinkle disappears.
- Docker: :latest = rolling main preview; :stable + :X.Y.Z + :X.Y =
  tagged releases. workflow_dispatch still only emits throwaway :sha-.
- release.yml gains a version-bump job: on every stable v* tag it
  bumps main to the next patch automatically.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:16:03 +05:30
Palash DebnathandClaude Opus 4.8 e49a0163ea fix(release): MSI-legal preview version stamp — numeric pre-release identifier (#293)
The Windows preview build dies in WiX with 'optional pre-release
identifier in app version must be numeric-only and cannot be greater
than 65535 for msi target' because the stamp was BASE-preview.N. Drop
the word: BASE-N is still a valid semver prerelease (sorts below the
stable BASE for the updater channel), unique per run, and MSI-legal.

Failed run: 27096586578 (Windows x64; macOS + Linux built fine but the
publish job was skipped).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:07:43 +05:30
Palash DebnathandClaude Opus 4.8 35b62ad0c0 fix(ci): make SHA-256 checksum step bash-3.2 safe (macOS runner) (#265)
The "Compute SHA-256 checksums" step used `mapfile -t` (a bash 4+ builtin) but
macOS GitHub runners execute `shell: bash` as /bin/bash 3.2, which has no
`mapfile`. The step exited 127 ("mapfile: command not found") on the macOS leg,
so `SHA256SUMS-macOS Apple Silicon.txt` was never produced/uploaded for v0.3.1
and v0.3.2 (the binaries themselves shipped fine; only the macOS checksum file
was missing and had to be regenerated by hand each time).

Replace `mapfile` with a portable `while IFS= read -r … done < <(find … | sort)`
loop (works on bash 3.2). Verified on bash 3.2.57: builds the array correctly,
handles spaces in bundle filenames. Linux/Windows legs are unaffected.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 06:56:42 +05:30
Palash DebnathandClaude Opus 4.8 94f2363fa2 feat(release): version preview builds (0.3.0-preview.N) + rollback spec (#226)
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>
2026-06-01 13:25:42 +05:30
Palash Debnath 5320b57de5 fix(release): macOS smoke mount path preserves space in volume name (#221) 2026-06-01 10:16:10 +05:30
Palash Debnath fa9cda3381 fix(release): macOS signing env must be ABSENT not empty (unblocks mac build) (#220) 2026-06-01 10:01:26 +05:30
Palash Debnath 6aa581c5f1 fix(release): resolve AppImage path before cd in Linux smoke (exit 127) (#218) 2026-06-01 09:38:46 +05:30
Palash DebnathandClaude Opus 4.8 1ece49a080 fix(release): make macOS signing opt-in so a bad cert can't break builds (#217)
The APPLE_CERTIFICATE secret is currently set-but-invalid, so tauri-action's
'security import' fails and kills the whole macOS build — on stable v* releases
too, not just preview. Make Developer-ID signing OPT-IN: pass the Apple creds
only on a v* tag push AND when the repo variable MACOS_SIGNING_ENABLED == 'true'.
Otherwise pass empty -> the build stays unsigned and succeeds (users clear
quarantine via xattr -cr, as documented). Preview is always unsigned.

To re-enable signed stable releases: fix the signing secrets, then set
MACOS_SIGNING_ENABLED=true (Settings -> Secrets and variables -> Actions ->
Variables). No code change needed to flip it.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 09:24:01 +05:30
Palash DebnathandClaude Opus 4.8 891e819b4e fix(release): unblock all-platform preview/release builds + auto-generated notes (#215)
The first preview build surfaced four real release-pipeline issues (all of which
also affect a stable v* release):

- macOS: build died at codesign — `security import: failed to import keychain
  certificate` (the APPLE_CERTIFICATE secret is set but invalid). Preview now
  force-skips Apple signing (passes empty creds) so it can't fail on a bad/absent
  cert; stable v* tags still receive the secrets, so signing engages once the
  cert is fixed.
- Linux: .deb bundling fails with "Failed to create control scripts: No such
  file or directory" (no custom deb config of ours). Drop .deb, ship AppImage
  only — the universal Linux format and the Linux auto-update target.
- Installer smoke (all 3 OSes): the steps hunted for a frozen backend binary to
  boot with --health-check, but the thin uv-venv installer ships no such binary
  (the venv builds on first launch). Rewrite to structural verification —
  assert the bundle carries the shell binary + bundled uv sidecar + backend
  source resources (pyproject.toml + backend/main.py).

Also: a new preview-notes job regenerates the rolling preview release body with
GitHub's auto-generated notes (What's Changed by PR + New Contributors + Full
Changelog) plus a Contributors avatar strip built from the PR authors — instead
of the bare "Auto-generated release for main…" fallback. Runs once after the
matrix, preview-only; stable keeps its CHANGELOG section + appended checksums.

Stable v* tag-push behavior is otherwise unchanged. YAML validated.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 08:53:28 +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 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 Debnath 651e63b7e9 P0 wave-1: security + correctness + Phase 2 foundation (#88)
P0 security + correctness fixes plus Phase 2 foundation work. 7 atomic commits, all CI green (Smoke + Tauri shell on macOS/Win/Linux + Tests).

Code commits:
- 92f716e: P0 security — loopback guard on /ws/transcribe before accept()
- fb52140: P0 dub — atomic WAV writes (closes #48 partial; Phase 2 plan 02-02 covers remaining sites)
- 9545640: P0 supply-chain — pin BtbN ffmpeg URL via FFMPEG_BTBN_VERSION
- e414665: P0 security — remove torch.load monkey-patch in asr_backend
- 6b49290: docs — Phase 4 plan <action> blocks on checkpoint tasks
- e764fdb: Phase 2 prep — TTSBackend.unload() foundation
- 71c10dc: test — fix capture_ws TestClient host for loopback guard

243 pytest passing, 0 failures. All 18 phase plans now validate.
2026-05-19 21:11:25 +05:30
Palash Debnath e4dbf4c8c0 P0: release.yml typecheck + bind audit + loopback middleware (#84)
Three P0 fixes bundled — foundation cleanup before v0.3.0 phase work. Closes release.yml drift (PR #51's tabs broke v0.3.0 tag releases), production bind exposure (Critic F1), and 9-endpoint LAN gap on /system/* (Critic F2+F3). 5 new tests; 243 full pass.
2026-05-18 22:00:59 +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
debpalash 41c23f6b3a feat: enhance ASR performance and reliability with binary bundling, model warmup, sub-stage progress tracking, and optimized polling. 2026-04-30 07:46:35 +05:30
debpalash c8d1858420 refactor: bundle uv binary per-platform as Tauri sidecar and remove redundant ffmpeg bootstrap download 2026-04-29 20:34:12 +05:30
debpalashandClaude Opus 4.7 d6b1dc1b49 fix(0.2.6): WS first-chunk drop, mic permissions, release-body from CHANGELOG
WS dictation pipeline was producing exit-183 from ffmpeg on every
partial because MediaRecorder.start(250) ran before the WebSocket
handshake finished — the first chunk (WebM EBML header) was queued
only into chunksRef and never pushed to the WS, so concatenated
chunks 1..N decoded as malformed WebM. Fix:

- Construct the WebSocket BEFORE starting the recorder so wsRef is
  set when the first ondataavailable fires.
- ondataavailable now queues every chunk through wsPendingRef when
  the socket isn't OPEN; ws.onopen drains the queue.
- ws.onmessage('error'): fire HTTP fallback immediately instead of
  waiting the full fallback-timeout window.
- ws.onclose without prior `final`: same — kick the HTTP path now
  if the recorder has already stopped.

Mic permissions:
- New frontend/src-tauri/Info.plist with NSMicrophoneUsageDescription
  + NSCameraUsageDescription. Tauri 2 auto-merges the file at bundle
  time (path is the same dir as tauri.conf.json — schema documents
  this fallback). Without it, getUserMedia silently fails on macOS
  10.14+ TCC.
- Mic-denial toast now includes platform-specific recovery (Settings
  paths for macOS/Windows, audio-group check for Linux).

CI / release notes:
- release.yml extracts the matching `## [X.Y.Z]` section from
  CHANGELOG.md and feeds it into tauri-action's releaseBody, so
  v0.2.6+ tag pushes produce real release notes instead of the
  placeholder "Auto-generated release. See commit log for changes."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:11:26 +05:30
Palash DebnathandClaude Opus 4.7 8c27ba40dc feat(release): add Linux AppImage bundle (#23)
AppImage was dropped earlier when linuxdeploy's AppImage runtime
couldn't FUSE-mount on GH Actions runners. Now viable again because:

1. `APPIMAGE_EXTRACT_AND_RUN=1` bypasses FUSE (extract-and-run).
2. The thin uv-venv installer is ~10 MB (vs the prior ~2 GB PyInstaller
   payload that tripped linuxdeploy's internal size limits).

Matrix `bundles` for Linux: `deb,updater` → `deb,appimage,updater`.
tauri.conf.json `targets` also updated so dev builds can produce
AppImages locally.

Covers universal Linux — runs on any glibc-2.31+ host without a
package manager.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:47:43 +05:30
Palash DebnathandClaude Opus 4.7 5a754b461e chore(release): drop macOS Intel from matrix (#21)
Apple shipped the last Intel Mac in June 2023 and Rosetta 2 runs the
ARM build natively at 85-100% of native speed. macos-13 runner backlog
was also blocking every v0.2.0 retag for ~10 min waiting on a hosted
Intel runner — measurable pain for no measurable user reach.

If we ever need Intel builds back, the matrix entry is one block of
five YAML lines.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:06:41 +05:30
Palash DebnathandClaude Opus 4.7 90ef02b2e4 chore: unique ports (3900/3901) + broader CI caches (#20)
Two unrelated tweaks grouped into one PR to keep churn low.

## Ports

Backend 8000 → 3900, Vite dev 5173 → 3901, 3902 reserved for future
IPC. Port 8000 conflicts with Django/Rails/Jupyter/Airflow on most
dev machines; the uncommon 3900 range dodges that. Touched:

- frontend/src-tauri/src/lib.rs (BACKEND_PORT)
- frontend/src-tauri/tauri.conf.json (devUrl)
- frontend/vite.config.js (server.port)
- frontend/src/api/client.ts (hardcoded API base)
- frontend/src/App.jsx (PREVIEW_API fallback)
- backend/main.py (CORS allowlist + uvicorn.run default)

Rust sidecar launcher and FastAPI uvicorn port stay in sync via the
`BACKEND_PORT` constant + explicit port=3900.

## CI caches

Build time shaves across ci.yml and release.yml:

- `astral-sh/setup-uv@v3` → `enable-cache: true` keyed on uv.lock
  (~45 s saved per run after uv.lock stabilises)
- `awalsh128/cache-apt-pkgs-action` for ffmpeg (~25 s saved)
- `actions/cache@v4` on `~/.bun/install/cache` keyed on bun.lock
  (~15 s saved; applied to both test gate and build matrix)

Expected warm test job: ~45-60 s (was ~2-3 min).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 06:52:30 +05:30