d91beef0fd314250d8d9b94de86dfea019a8bd96
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9d133870e5 | fix(watermark): isolate lifecycle state between tests | ||
|
|
37c5df6f3a |
refactor(watermark): /simplify round — grace in one critical section, _env_float
Four-angle /simplify on the cumulative branch diff: - The idle-reaper grace flag now lives entirely inside _get_generator's lock: the prefetch claims it only when THAT call builds the model, and every other getter call consumes it. This deletes the duplicated call-site clears in embed/detect (detect no longer touches the generator's grace at all — it was clearing a flag for a model it never uses), and closes the lock-gap window where the prefetch's claim could land on an already-used model, which the old comment claimed was impossible. - Shared _env_float(name, default) for main.py's three inline float-env parsers (capture delay, watermark delay, MCP start timeout): one NaN/negative-rejecting implementation instead of three drifting copies; the older two lacked the isfinite guard entirely. - Test cleanups: dead isinstance-Future assert half removed, the fake-audioseal Event-wait simplified to sleep, the reaper-diversion guard simplified to a plain no-op lambda, stale setdefault sentence dropped from the conftest comment. Skipped with reason: merging the double will_mark() gate (they guard different invariants — pool creation vs model load, both tested) and hoisting the reaper guard to conftest (an autouse module-attr patch would break tests that verify release_idle_models directly). |
||
|
|
fbb258d2e2 |
fix(watermark): rebuild the pool after the shutdown drain + review round
The shutdown drain killed the module singleton with no replacement, so any process that keeps running after a lifespan shutdown — the CI suite does exactly this — dead-submitted on the next watermark op: "cannot schedule new futures after shutdown" (CI red; independently confirmed by Greptile P1, CodeRabbit Major, and the plugin code review at 95/100 confidence). shutdown_watermark_pool() now resets the singleton under its build lock before draining, so the next get_watermark_pool() hands out a live replacement. Regression test covers drained-pool-refuses + replacement-accepts. Same round, minor findings: the drain's except now logs with exc_info instead of a bare pass (GHAS CodeQL empty-except); the watermark delay knob rejects negative/non-finite overrides (CodeRabbit); conftest sets OMNIVOICE_PRELOAD_WATERMARK=0 unconditionally so a stray export from the runner shell cannot re-enable background warm-ups mid-suite (CodeRabbit). |
||
|
|
6837ba25ac |
fix(watermark): review follow-ups for #1577 — CI red + bot findings
Two CI failures, both understood: 1. test_shutdown_preload_race_1000 pins the production _cancel_and_await _tasks call site by regex; the new fifth handle broke the pattern. The guard now pins all FIVE handles (its property — every preload handle awaited under one generous bound — is unchanged). 2. test_prefetched_model_gets_one_extra_idle_window flaked only in the full suite: many tests boot the app lifespan, and any that exits without a lifespan shutdown leaves the deferred watermark-preload task pending — 35s later it fires mid-suite in another thread and re-stamps _last_used under whatever test is running. conftest now defaults OMNIVOICE_PRELOAD_WATERMARK=0 for the test session (a test can still opt in), and the grace test neutralizes will_mark so a leaked warm-up can't touch it. Bot findings: Greptile P1 + CodeRabbit — cancelling the preload task doesn't stop a watermark-pool thread already inside the ~42s cold import, and nothing drained that pool at shutdown (only the GPU pool was reset). Shutdown now drains the watermark pool's queue (shutdown(wait=False, cancel_futures=True)) — bounded abandon, same documented reality that Python can't kill a running thread. CodeRabbit Major: the warm-up reads its own delay knob (OMNIVOICE_PRELOAD_WATERMARK_DELAY, default 35s) instead of reusing the capture-ASR delay, so a capture env override no longer retimes it. CodeRabbit Minor: the _prefetched_unused claim/clear transitions now happen under _generator_lock, so the retention grace can't be granted to a model that has actually been used; the test fixture resets all lifecycle globals. Skipped with reason: gating prefetch on local-checkpoint presence — the warm-up downloads only what the first embed would download anyway; time-shifting that download is the feature, not a new network call. |
||
|
|
817e9a9c8b |
test: stop config-path leakage across test modules (#1269) (#1307)
* test: stop config-path leakage across test modules (#1269) Ten test modules share a fixture shape: monkeypatch OMNIVOICE_DATA_DIR to a tmp_path, then importlib.reload(core.config) (plus core.db, a router, main). monkeypatch restores the ENV VAR at teardown — and nothing reloads the modules back, so the path constants keep pointing at that test's tmp_path for the rest of the session. In a combined `pytest tests/ backend/tests/` run that produced three different answers to "where is the voices directory": OMNIVOICE_DATA_DIR .../omnivoice-test-data-vna0ywre (correct) core.config.VOICES_DIR .../test_fitted_srt_last_cue_withi0/… (leaked) profiles.VOICES_DIR .../test_clone_profile_save_saniti0/… (leaked) — which is why the personas import tests wrote a file to one directory and asserted it existed in another. Restores at MODULE teardown, and that boundary is the design. Function scope was wrong: tests/smoke/test_boot_smoke.py has a module-scoped fixture that deliberately aims core.config at a frozen fixture directory for the length of that file, and a per-test restore reset it between that module's own tests. Within a module a fixture cannot tell deliberate setup from a leak; across modules there is no ambiguity. Snapshot/restore of the constants rather than re-reloading: a reload would re-register FastAPI routes and rebuild module state as a side effect, while a setattr is inert. It also re-syncs modules that copied a value out of core.config — a reload fixture typically imports the router under test for the first time, so it has no earlier value to put back. 3 of the 4 failures are fixed. test_lifespan_shutdown_mid_load_is_clean_and_ clears_sentinel still fails in a combined run for an unrelated reason (its preload never reaches run_in_executor); #1269 stays open for that one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: import core.config when snapshotting, don't just probe sys.modules Greptile P1: a sys.modules-only probe returns {} when this module is the first to import core.config — and the empty-snapshot guard then skips restoration entirely, so the module most likely to reload config was the one least protected. Importing is cheap and idempotent, and tests/conftest.py has already pointed OMNIVOICE_DATA_DIR at a throwaway dir before any fixture runs, so the captured values are the right ones. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5afcdf787d |
fix(engines): warn before a long CPU synth burns the whole budget (#1302)
* fix(engines): warn before a long CPU synth burns the whole budget #1288 closed the under-provisioned-GPU gap but left the CPU one open, and I missed it: a CPU-only host is a BENIGN routing verdict, so routingNotice() correctly stays silent — yet #1299 and #1260 are exactly that shape, CPU hosts that hit the 300s budget on long text with no warning at all. "Nothing is misconfigured" and "this will finish in time" are different claims. Threshold is the backend's own definition of past-short: generate_timeout_for() gives the first 1200 characters the flat budget before extending it, so ordinary sentences on a CPU laptop stay quiet and only the shape that actually times out is flagged. Hardware caveats still take precedence — one toast, and it names the real reason rather than generic advice. 5 tests; engines.cpuLongText translated in all 21 locales. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(engines): don't tell CPU-tuned engines to switch to themselves Greptile P1. The advice names OmniVoice GGUF and Supertonic-3 as the CPU-tuned alternatives — shown to someone already running one of them, it is advice to switch to what they are using. Those two now get the same warning without the self-referential clause; the engine set matches the backend's own timeout message so the two can't disagree about who is CPU-tuned. Also documents the preflight in docs/performance.md (docs-sync rule): both warning shapes, why the threshold is 1200 characters (it is the figure the budget itself uses), that they are advisory and once-per-engine-per-session, and the CPU-tuned exception. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
83f943bead |
fix: bot-review harvest (16 findings) + deterministic style/locale CI + reviewer configs
Harvested and verified every CodeRabbit/Greptile finding from PRs #1175, #1189, #1192, #1195: 16 real ones fixed (fallback ASR preflight bypass, VRAM release on stream exit, typed 409 parity, uv env independence, path-privacy in errors, MCP clone_voice hardening, CaptureWidget WS guard, test hygiene), 4 refuted with evidence, rest documented as deliberate design or deferred. Deterministic CI replaces hand-enforcement: tests/test_changelog_style.py (quiet one-liner format) and tests/test_locale_parity.py (21-locale key/placeholder lockstep with a ratchet baseline) — the latter surfaced and fixes 151 already-broken locale strings. CodeRabbit/Greptile carry the house rules via .coderabbit.yaml + greptile.json; CLAUDE.md gains the harvest-before-merge and never-accept-as-is rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
63fd497caf |
feat: TTS-only first run, platform-curated ASR, guided OS permissions, parakeet-mlx
Only the TTS model (~2.4 GB) is required on first run; ASR models are per-platform curated picks (curated_on in models.yaml) installed on demand. Every transcription surface returns a typed asr_model_missing error with a one-click download CTA instead of silently pulling multi-GB Whisper weights. Settings -> Models is a grouped, platform-aware catalog. New guided permissions UX (wizard System Check + Settings -> Permissions + mic pre-flight) with native mic-state checks and OS settings deep-links. New parakeet-mlx engine brings Parakeet TDT v3 to Apple Silicon (language-gated capture preference so multilingual dictation never regresses). Docs: expressive-speech page, Flush/Unload + CPU-fallback triage, clone-length FAQ. Hardening: preflight fails open for custom model pins, ROCm curation no longer inherits NVIDIA picks, Windows mic probe reads the NonPackaged consent key, CaptureWidget setup race fixed, offline-cache CI simulation fixes so empty-cache runners stay green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9c81e3389d |
feat(network): automatic Hugging Face endpoint selection — probe, pick, remember (#1082)
* feat(network): automatic Hugging Face endpoint selection — probe, pick, remember Restricted-network first-runs (the #984 class: huggingface.co unreachable, user dead-ends before discovering the mirror setting) now self-heal by default, while explicit endpoint choices are never second-guessed. - New backend/services/endpoint_race.py: parallel HTTPS reachability + latency probes of huggingface.co and the hf-mirror.com community mirror (3s timeouts). Probes are the only signal — no geo-IP, no third-party calls. Reachable beats unreachable; with both reachable the official endpoint wins unless the mirror is decisively faster (anti-flap hysteresis). The pick is cached in prefs and re-raced only on first run, a network-classified download failure, staleness (>7 days), or an explicit "Test again". - Manual mode is sacred: HF_ENDPOINT env, an hf_endpoint pref, or any explicit Settings pick disables auto-switching entirely; OMNIVOICE_HF_ENDPOINT_MODE=manual is a hard opt-out. - Wiring: the wizard preflight races endpoints when nothing is configured (honest copy when the mirror wins; warn-not-block when nothing is reachable); Model Store installs and the model-cache auto-repair resolve their per-call endpoint= through the cached decision, and a network-classified failure re-races once per repo per process and retries on the new winner (same guard pattern as the cache-recovery ladder). - Settings → Models → Hugging Face mirror gains "Auto (recommended)": shows the current pick, measured latency, last-checked time, and a "Test again" button (POST /api/settings/hf-mirror/test). Existing explicit configs surface as the matching manual mode. Panel notes that hf_hub checksums every download regardless of endpoint. - Tests: policy/cache/failover matrices in tests/test_endpoint_race.py, preflight + settings + repair-failover integration with mocked probers, HFMirrorPanel mode tests, and a suite-wide conftest guard that pins the probers so no test can hit the real network. - Docs: downloading-models.md and install/troubleshooting.md describe the automatic default and both opt-outs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): Unreleased entry for automatic HF endpoint selection Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): endpoint-probe pin uses an isolated MonkeyPatch and clears the decision cache; dtype guard tolerates stubbed torch The autouse probe pin requested the shared monkeypatch fixture, hoisting its setup earlier for every test and reordering teardown against the fp16 guard — which then ran torch.get_default_dtype() on test_torch_compile_gate's SimpleNamespace stub. The pin now uses its own MonkeyPatch context and also clears the prefs-cached endpoint decision per test (one test's auto pick leaked into other tests' preflight labels on CI ordering). The dtype guard additionally skips non-module torch stubs outright. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): endpoint env vars can no longer leak out of the mirror-settings suite set_hf_mirror writes os.environ[HF_ENDPOINT] during the test, and monkeypatch.delenv(raising=False) on an absent var records nothing to undo — so the write leaked process-wide and flipped later suites' preflight network checks into the explicit-endpoint branch (the CI-order failures). Guaranteed save/restore autouse fixture at the source, plus defensive env shedding in the preflight suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3f62471bad |
test: pay down export-router test debt + kill two test-order pollution classes (#1081)
Four pieces of test debt, root-caused and hardened:
1. exports.py test coverage (was: zero dedicated tests): new
tests/test_exports_api.py (26 tests) covering /export, /export/record,
/export/history, /export/reveal — happy paths, traversal/containment
guards (incl. symlink escape), destination validation, error mapping,
and the mp4 watermark-overlay branch with its plain-copy fallback.
Two real bugs found and fixed in the router:
- _safe_destination checked isabs() on realpath()'s output, which is
always absolute — dead check; a relative destination silently exported
to a cwd-dependent location instead of the documented 400.
- _safe_source let "." / ".." through the basename guard (caught only
later by realpath containment as a confusing 404); now 400 up front.
2. CI-Linux fp16 default-dtype leak (test_prefers_vocals_over_mix,
test_final_dub_track_and_seg_wav_are_watermarked): not reproducible on
macOS — instrumenting torch.set_default_dtype across both tests records
zero non-fp32 sets locally. Both tests now carry an opt-in
torch_dtype_isolation fixture (save/restore, so the leak can never
spread), and the conftest guard is demoted to pure insurance. A cheap
permanent recorder wraps torch.set_default_dtype /
set_default_tensor_type once torch appears and captures the setter's
stack only on a non-fp32 set; both fixtures print that stack when they
fire, so the next CI occurrence names the exact culprit call chain.
3. Test-order pollution (both reported combos): root cause was
collection-time sys.modules stubbing in backend/tests — seven modules
installed bare ModuleType stubs for core.config (and test_capture_ws.py
for services.model_manager/asr_backend/ffmpeg_utils, now all lazily
imported by the router anyway). pytest imports test modules during
collection, so the stubs leaked process-wide before any test ran:
- combo (a): monkeypatch.setattr("core.config.OUTPUTS_DIR", ...) in
test_longform_e2e died with AttributeError (core never gets a .config
attribute when the import is satisfied straight from sys.modules).
- combo (b): test_router_smoke's `from main import app` died with
ImportError: cannot import name 'find_ffmpeg' (unknown location).
Fix at source: new backend/tests/conftest.py sets a hermetic
OMNIVOICE_DATA_DIR (mirroring tests/conftest.py, #878) and the real
core.config is imported everywhere — zero sys.modules surgery. New
backend/tests/test_no_module_stubs.py guards the whole class (verified
fail-before/pass-after against the old stub). Stale rationale comments
in pyproject.toml and ci.yml updated to match.
4. batched_tts.py TODO(#312): investigated, comment corrected only —
#312 is closed (the live routes are engine-aware); this module has zero
call sites and stays an unintegrated experiment. See PR notes.
Full tests/ suite: 2796 passed. backend/tests standalone: 130 passed.
Both pollution combos re-run green in the reported orderings.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
0076d0067e |
test(ci): root-cause and neutralize the flaky trio — a leaked torch fp16 default dtype (#1021)
test_effects_chain / test_generation_audio_guard / test_persona_bundle failed intermittently on CI (never locally) with identical signatures across three unrelated PRs today (#1002, #1019, #1016) — costing a full CI cycle per occurrence and repeatedly muddying merge decisions. Root cause, confirmed by local reproduction: a leaked torch.set_default_dtype(torch.float16) from some earlier test in the CI-Linux ordering. The smoking gun was test_generation_audio_guard's observed 0.0999755859375 — exactly float16(0.1), i.e. torch.tensor([0.1, …]) built under a leaked fp16 default. Reproducing with a simulated polluter locally produced the trio's exact failures: Pedalboard refuses fp16 audio outright ("only supports 32-bit and 64-bit floating point") and silently returns unmodified audio for every preset, so test_effects_chain's preset outputs compare identical; and the fp16 tensor value breaks the sanitize approx-check. Fix: an autouse conftest guard (same philosophy as the existing LLM-state isolation guard, #878) that checks torch's default dtype after every test, resets any leak to float32, and emits a UserWarning naming the offending test's nodeid — so the actual CI-only polluter identifies itself in the next CI log instead of being chased blind. Regression test: a deliberate-leak pair proving reset-between-tests. Fail-before/pass-after verified: with the guard stashed, a simulated polluter + the trio reproduced 2/3 failures locally with the exact CI signatures; with the guard active, 73/73 pass and the warning names the polluter. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3bb401f4e5 |
test: make LLM-provider state leaks between tests impossible (#878) (#894)
Root cause: LLM provider selection reads three process-global surfaces — env vars (LLM_DEFAULT_PROVIDER, per-provider *_API_KEY/*_BASE_URL, TRANSLATE_*), the SQLite settings store (llm.active_provider & co.), and prefs.json (llm_backend). Importing `main` (TestClient fixtures do) dotenv-loads the developer's .env and ~/.config/omnivoice/env straight into os.environ, and several tests/endpoints mutate these surfaces without teardown — so whichever test imported the app first flipped what later tests' active_backend_id()/active_provider_id() resolved to (order-dependent failures in test_engines.py, test_llm_endpoint_settings.py, test_llm_providers.py). Fix the class, not the instances: - tests/conftest.py: redirect OMNIVOICE_DATA_DIR to a per-session tmp dir and OMNIVOICE_ENV_FILE into it (before collection freezes core.config.DATA_DIR), so tests never read or write the developer's real app state and local runs behave like clean CI. - tests/conftest.py: autouse `_isolate_llm_provider_state` fixture snapshots env (derived from llm_providers._PROVIDERS, so new providers are guarded automatically), llm.* / secret.llm_key.* settings rows, and the prefs llm_backend/env.TRANSLATE* keys before every test and restores them exactly afterwards. - shared `clean_llm_env` fixture clears the FULL provider env surface; the four LLM test modules' hand-picked partial delenv lists (which left e.g. LLM_DEFAULT_PROVIDER / OPENROUTER_API_KEY standing) now use it. - tests/test_llm_state_isolation.py: deterministic fail-before/pass-after regression pair — pollutes all three surfaces without cleanup, then asserts the guard restored them. Verified: the issue's two-test repro passes; the five LLM-related test files pass in order; full suite green (2046 passed, 20 skipped, 10 xfailed, 4 xpassed). Fixes #878 Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
93aa66ab0a |
Phase 3 Plan 03-01: Supertonic-3 engine on SubprocessBackend (#101)
* Phase 3 Plan 03-01: Supertonic-3 engine on SubprocessBackend
Adds Supertonic-3 as a 7th opt-in TTS engine on the Phase 2
SubprocessBackend primitive. Closes TTS-01..06 (REQUIREMENTS.md):
* TTS-01 — _REGISTRY["supertonic3"] resolves to Supertonic3Backend,
a SubprocessBackend subclass.
* TTS-02 — `supertonic==1.3.1` lives under [project.optional-dependencies];
default `uv sync --no-dev` does NOT install it. Exactly one
`onnxruntime` row in `uv pip list` after `--extra supertonic`.
* TTS-03 — Model revision pinned by 40-char commit SHA
(724fb5abbf5502583fb520898d45929e62f02c0b — the "Initial
Supertonic 3 release" SHA, same as the SDK's own pin).
Resolver script for intentional bumps:
scripts/resolve_supertonic3_sha.py.
* TTS-04 — Honest CPU-only reporting. `is_available()` message says
"ready (CPU-only via onnxruntime)" and never mentions
"cuda" or "mps". `gpu_compat = ("cpu",)`.
* TTS-05 — License gate via settings_store helpers
(get/set_license_accepted) + Loopback-only
/api/settings/license endpoint + SupertonicLicenseDialog
frontend modal showing MIT (code) and OpenRAIL-M (model).
Wired into EngineCompatibilityMatrix as an "Accept license"
button on rows whose `reason` mentions "license not
accepted".
* TTS-06 — 3 langs (en/ja/ru) × 3 sec smoke test in
tests/test_supertonic3.py::test_smoke_3langs_3sec
(OMNIVOICE_SMOKE-gated; asserts no onnxruntime-gpu row
post-synthesize).
Package legitimacy gate (Task 1 in plan): supertonic on PyPI verified
to be published by Supertone Inc. (ato@supertone.ai), repo
github.com/supertone-inc/supertonic, wheel is pure-Python with no
postinstall scripts. Same publisher ships supertonic-js on npm under
the same maintainer email.
Test results:
* tests/test_supertonic3.py — 10 passed, 3 skipped (network-gated).
* tests/smoke/ — 4 passed.
* tests/ (full, --ignore=tests/manual) — 412 passed, 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(tests): uv sync --all-extras so optional-engine tests can import their package
Phase 3 added `supertonic` as an optional dependency. The CI Tests job
runs `uv sync` (no extras), so `test_cpu_only_honest` and `test_license_gate`
in tests/test_supertonic3.py hit the "supertonic package not installed"
fallback instead of the real import path, and fail.
Bare `uv sync` is the right default for users (engines are opt-in), but
the test environment should exercise the full surface. `--all-extras`
keeps the smoke job lean (still bare `uv sync`) while letting Tests
verify the integrated behavior of every optional engine.
Future-proofs against the same failure mode in Phase 4 (GGUF) and any
later optional engines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
67328d04fe |
refactor: split backend into api/core/services/schemas, harden security + fd pressure, add searchable language picker, fix segment fragmentation
Backend:
- Split monolithic main.py into backend/{api/routers,core,schemas,services}
- core/db.py: allowlist-gated migrations, db_conn context manager (kills SQL injection on ALTER)
- core/tasks.py: lock-guarded listener add/remove/push, snapshot-before-iterate
- services/ffmpeg_utils.py: run_ffmpeg helper with concurrency semaphore, EAGAIN retry, guaranteed reap
- services/segmentation.py: Bengali/CJK/Arabic punctuation, ultra-short tier, stitch_adjacent_shorts,
bounded-loop merge; public clean_up_segments API
- services/model_manager.py: robust lock.locked() handling
- api/routers/dub_core.py: job_id traversal guard, thread-safe _active_procs, timeouts on ffmpeg/demucs,
POST /dub/cleanup-segments endpoint
- api/routers/dub_export.py: guarded SSE listener remove, ffmpeg timeouts via run_ffmpeg
- api/routers/exports.py: destination_path validation, safe source resolver, subprocess list-form
- api/routers/generation.py: contextlib.suppress on tempfile cleanup, db_conn usage, safe output-path helper
- api/routers/system.py: try/finally tmp cleanup, subprocess timeouts
- schemas/requests.py: TranslateSegment.id int->str to match hex segment IDs
- main.py: threading.Lock around crash log writes
Frontend:
- components/SearchableSelect.jsx: popover combobox with search, keyboard nav, popular+recent pins, 200-item cap
- App.jsx: wire SearchableSelect for dub language / ISO code / voice-gen language; Clean Up segments button;
fix blob URL leak (object-shaped prev in setter, unmount cleanup via ref)
- components/WaveformTimeline.jsx: explicit <video> detach instead of innerHTML='' to release decoder
- index.css: ss-* combobox styles matching Gruvbox theme
Tests:
- tests/test_segmentation.py (26 cases), test_dub_transcribe.py, test_dub_export_unique.py, conftest.py
Chore:
- .gitignore: exclude omnivoice.zip, /research/ reference clones
- Remove tracked stray root test scripts + crash_log.txt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|