| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
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> | 4 个月前 | |
chore: set version to 0.3.0 across all sources (+ drop v0.4 references) (#145) * chore: drop stray v0.4 references — everything ships on the v0.3.0 line Per the project's versioning rule (no v0.4, no unprompted version chatter): - backend/main.py + marketplace.py: the app reported version "0.4.0" (ahead of even pyproject's 0.2.7 and referencing a forbidden version). Aligned to "0.2.7" to match pyproject.toml / tauri.conf.json — a consistency fix, not a bump. - errorDocsMap.ts / indextts/bootstrap.py / _secret_key.py: reworded "v0.4" deferral comments to version-agnostic "deferred / later hardening pass". - docs/install/troubleshooting.md: the "tracked for v0.4" notarization line now matches macos.md (signing is wired; activates on the Apple cert secrets). Note: historical planning records under .planning/ still contain "defer to v0.4" notes; left as-is (a record of superseded decisions) — CLAUDE.md + the constitution are the live source of truth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: set version to 0.3.0 across all sources (current dev line) The current/upcoming version is v0.3.0 (0.2.7 is the prior stable). Bump every version source so the codebase consistently reports 0.3.0 — the in-code dev version; the git *tag* still happens later per the release cadence. - pyproject.toml, frontend/src-tauri/Cargo.toml, tauri.conf.json, frontend/package.json: 0.2.7 → 0.3.0 - backend/main.py (FastAPI) + marketplace.py export metadata → 0.3.0 (these had drifted to a phantom "0.4.0") - CHANGELOG.md: "[0.2.7] — Unreleased" → "[0.3.0] — Unreleased" - uv.lock + Cargo.lock reconciled (1-line each) so `--frozen` installs hold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(version): read app version from package metadata (no more drift) Greptile (#145): the FastAPI version + marketplace bundle metadata were bare string literals — they'd go stale-wrong again at the next bump (the exact class of bug this PR fixes; that's how "0.4.0" happened). Read once from importlib.metadata.version("omnivoice") via core.version.APP_VERSION, with a "0.3.0" fallback only for a non-installed source checkout. pyproject.toml is now the single source of truth for the runtime version. Tests: tests/test_app_version.py (semver + equals installed metadata). 2 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 3 个月前 | |
feat(dub): realtime dub preview (#1769) Opt-in live preview for dub segments: edits debounce into a streamed /ws/tts synthesis played through the chunk player, with cancellation preserved through buffered playback. Maintainer fixes: /ws/tts added to the backend ticket allowlist (feature was dead off-loopback), handshake failures surface a toast, loopback-only plaintext refusal reverted to keep the documented remote-GPU setup working, PCM16 decode hardened. Thanks @mvanhorn! | 15 天前 | |
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. | 1 个月前 | |
test(cloning): verify catalogue CT2 reuse without a second ASR pipeline | 23 小时前 | |
fix(dub): preserve complete speech and reject silent partial output | 4 天前 | |
fix: close lifecycle and diagnostic review regressions | 2 天前 | |
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. | 1 个月前 | |
feat(electron): add full VoiceStudio desktop app | 4 天前 | |
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. | 1 个月前 | |
fix(generate): never send a chunk with nothing to say (#1330) The chunk splitter could end a chunk on a fragment with no speakable character in it — a lone ".", "—", or "?" left behind by a boundary landing just past the last word. The engine renders that to nothing, so the pass was pure waste and, on a slow CPU, a visible stall. The splitter now folds an unspeakable fragment back into its neighbour. Where the fold would push the chunk past max_chars it moves a word across instead of overflowing, and it never borrows a word that is itself unspeakable (which would just recreate the dead chunk). Measured on realistic prose: zero dead chunks, zero over-limit chunks. Adds tests/test_no_unspeakable_chunks_1330.py — fails before, passes after, and includes a randomised probe asserting any residual overflow is punctuation-only and bounded. | 1 个月前 | |
feat(electron): add full VoiceStudio desktop app | 4 天前 | |
feat(electron): add full VoiceStudio desktop app | 4 天前 | |
feat(electron): add full VoiceStudio desktop app | 4 天前 | |
fix(security): stabilize streamed failure responses | 1 个月前 | |
fix(dub): preserve original sound outside dialogue intervals | 3 天前 | |
feat(electron): add full VoiceStudio desktop app | 4 天前 | |
fix: close lifecycle and diagnostic review regressions | 2 天前 | |
feat(dub): second-pass ASR QC — flag lines whose dub drifts from target (Wave 3.3) (#370) After a dub is generated, re-recognize the synthetic audio and compare what the ASR heard against what we asked the TTS to say. Lines that drift are flagged for the user to re-listen / re-dub — turning subtitle timing and pronunciation from trusted math into measured truth, and doubling as an automatic dub-quality check. Design delta from pyvideotrans (which lets recognized text REPLACE the subtitles wholesale): we keep the generated text authoritative and use the second pass only for MEASUREMENT — a per-line drift score + measured start/end that feed the incremental re-dub loop, never silently overwriting the translation. - services/dub_qc.py (pure, tested): word_error_rate (normalized token edit distance, case/punct-insensitive, script-agnostic) + score_dub (matches recognized segments to dub segments by time overlap, concatenates the hypothesis, scores drift, derives measured bounds). - POST /dub/qc/{job_id}: runs the active ASR backend on the dubbed track in the GPU pool, annotates each segment with qc_drift/qc_flagged/ qc_recognized/qc_measured_start-end (non-destructive — content untouched), persists, emits a qc_done job event. Opt-in, never fatal. - Frontend: dubQc() API fn + a red 'Verify' badge on flagged segment rows (en.json keys; other locales fall back). 12 pure scoring tests (identical/substitution/empty/no-overlap/multi-segment matching/measured-timing); endpoint validated in CI. Spec 5 / parity program Wave 3.3. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 3 个月前 | |
feat(dub): predict segment fit before synthesis — tight/impossible badges + opt-in shorter rewrites (#1051) New pure planning layer (services/duration_planner.py) runs after translation, before TTS: estimates each translated line's natural speech duration (self- calibrating from the job's already-synthesized segments, static per-language rates as cold-start fallback) and classifies it fits/tight/impossible against slot + capped gap borrow, with thresholds derived from fit_planner's own caps so "impossible" means "would be trimmed". Verdicts ride the /dub/translate response and badge the segment table; an opt-in (default OFF) LLM pass attaches one-click shorter-rewrite suggestions for impossible lines. Never blocks generation — informs before GPU time is burned. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
fix(catalogue): repair the guidance the Weights-list rename broke The rename to "the engine's Weights list in Model Catalogue" left several messages without a verb, and pointed others at the wrong place: - The offline and create-voice messages say what to do again. - pyannote has no owning engine, so diarization points at Other weights. - The Hugging Face mirror moved to Settings → Network, and voice previews moved to Settings → Storage. - Unloading and switching engines happen in the engine list, not a Weights list. - A bad saved path points at Settings → Storage or the env file. - Docstrings that read "the the" are fixed. The dub stream-drop fallback goes through i18n in all 21 locales. A Dictation pick on a row that is already downloading no longer starts a second install: the row's radio is disabled while it works, and useModelDownloads refuses a second mutation for a repo already in flight. The Supertonic-3 license test checks for the Accept wording. | 8 天前 | |
fix(audiocpp): require explicit model install | 11 天前 | |
fix: integrate current main and finish review requirements for #2085 | 2 天前 | |
fix(audiocpp): harden GPU runtime routing | 10 天前 | |
fix(memory): release the model before emptying the cache, not after The shared voice model's unload emptied the allocator caches and *then* dropped the reference. That frees nothing: the weights are still reachable when gc.collect() runs, empty_cache() only returns blocks the allocator already considered free, and the reference drops a moment later into a cache nothing will flush again. The unload logs success, the engine leaves the registry, and nvidia-smi does not move. Six modules open-coded the same two lines. Exactly one had them inverted -- OmniVoiceBackend.unload, which is the path the engine-registry idle sweep reaches, which is the sweep a headless worker node runs. So every unload a user could trigger from the UI worked, and the one that runs unattended on a machine lending its GPU held 3.6 GB indefinitely. Found on hardware: the sweep fired on schedule, logged "Released 1 idle engine(s)", and VRAM stayed flat at 3656 MiB for the next two minutes. Replace all six with model_manager.unload_shared_model(), which clears the reference, drops the clone-prompt side cache, then frees -- in that order, in one place. Two callers gain the side-cache drop they were missing (/system/flush-memory and the shutdown path), which is the same defect one step down: an unload that kept the encoded reference tensors belonging to the model it had just released. A source guard asserts nothing outside model_manager assigns the shared reference, so the next caller cannot reintroduce the ordering. It caught the sixth site while being written. Also give the AudioSeal watermark models the bargain every other model in the app already makes: they loaded on the first embed and stayed resident for the life of the process. CPU-resident, so this is system RAM rather than VRAM, and the machines that notice are the ones running batches. The error text on a failing unload changes with the ordering. "Could not be unloaded, retry after the current generation finishes" was accurate when the cache flush ran first and aborted before the release; now the release has already happened and only the flush can fail, so it says that instead of sending the user to repeat work that is done. | 1 个月前 | |
fix(audiocpp): harden async and worker routing | 10 天前 | |
fix(media): require successful tool version probes | 2 天前 | |
feat(dub): underrun fill — short dubbed lines are slowed toward their slot instead of leaving dead air (#1137) * feat(dub): underrun fill — short dubbed lines are slowed toward their slot instead of leaving dead air The dub pipeline has always handled audio that is too LONG for its slot (atempo compression, Smart Fit's audio/video split, trims). Audio that is too SHORT was start-aligned and abandoned — and that is the common case, not the corner: translations routinely speak faster than the source delivery. Measured on a real 4-segment dub, 8.8 of 18.7 seconds of original speech time had no dubbed voice. What fills those holes is the separated bed's under-speech residue (37% of the original energy, measured), so the user hears them as BOTH "little silences" AND "the music is numbed" — and sees them as lip-sync failure, since the mouth keeps moving after the dub stopped. The fill: when a line's natural duration covers less than UNDERRUN_TOLERANCE (95%) of its slot, slow it toward the slot with the same pitch-preserving atempo pipe the compression path uses, bounded at min_audio_rate (default 0.85x — comfortably natural; atempo handles <1 natively). Wired into both fitting strategies: - fit_planner._fit_one: need < 1 now resolves to audio_rate=max(need, floor), status "audio_slowed" — planner stays a pure function; golden fixtures regenerated per their own instructions (10 substantive lines: five underrun segments across four scenarios flip to audio_slowed@0.85). - dub_generate smart_fit branch: applies the rate in both directions (the target formula was already direction-agnostic). - dub_generate strict_slot branch: mirror of its compression arm. - stretch_video and concise strategies deliberately untouched (natural-rate by design / never-intervene by design). OMNIVOICE_UNDERRUN_MIN_RATE overrides the floor (1.0 disables; clamped to atempo's sane range). The per-segment fit badge shows "slowed N.NNx" with a tooltip, translated in all 21 locales. Tests: planner contracts (fill bounded by floor, tolerance zone untouched, disable switch, empty-audio guard), the flipped unit/golden/integration expectations updated with the rationale, and the existing smart_fit integration test now exercises the fill through the real mix loop (its seg0 comes out audio_slowed@0.85 end to end). Full suite: 2987 backend + 1236 frontend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(dub): strict-slot slow-downs report themselves honestly (review); ru pitch wording Review round on #1137: - Greptile P1 "slowdown reports fits" — REAL: the strict_slot underrun fill fell through to the unconditional {"status": "fits"} entry, so a slowed segment's badge hid the applied rate (and compression_applied mislabeled it). The branch now emits {"status": "audio_slowed", "audio_rate": …} like the smart_fit path — same honesty contract everywhere. - Greptile P1 "padded audio hides underruns" — REFUTED with evidence: nothing pads strict-slot audio before the check (_load_entry_wav returns the natural-length WAV; only error/silence slots are slot-sized, and those are synthetic silence by design). On-disk segment WAVs measure both shorter and longer than their slots, which pre-padding would make impossible. - CodeRabbit: Russian tooltip now says "высота тона сохранена" (pitch), not "высота сохранена" (height). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: mergetest <nizam4103@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> | 2 个月前 | |
feat(dub): regenerate subtitle timeline on the fitted timeline (Wave 3.1) (#371) Smart Fit Phase A (planner) + the export-side video retime + audio stretch already shipped (#347 + dub_export stretch filter). The last piece of Spec 1 was the subtitle timeline: under stretch_video the dubbed audio plays at FITTED positions, but the standalone SRT/VTT export still used the original segment times — so external subtitles drifted against the dubbed video. - services/fitted_subtitles.py (pure, tested): map_time_to_fitted() + fitted_cues() remap original cue times onto the same per-chunk {orig→new, stretch_ratio} plan the video stretch uses, with a monotonicity guard. - dub_export SRT + VTT endpoints: when a job used stretch_video, cues are regenerated from the plan (subtitles track actual dub placement); no plan → original times, unchanged. New optional ?lang= selects the track. 7 pure tests (chunk-bound mapping, linear interpolation, unit-rate tail, fitted cues, monotonicity, empty-plan identity). Spec 1 (remaining) / parity program Wave 3.1. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 3 个月前 | |
feat(workers): make a remote GPU actually run a task, end to end Selecting a remote worker repainted a badge and nothing else. The cause was not subtle: `scheduler.submit` had no production caller, and `routing.decide()` was read only by the status endpoint that paints the header. Remote execution was a complete, tested pipeline with no producer at its head. This adds the producer and fixes the defects that made the pipeline unable to carry a real job: - Nothing routed to the scheduler. Adds `POST /workers/tasks` (loopback-gated, **development-only** until the gateway lands) and `Scheduler.wait`, backed by per-task futures rather than the unregisterable `on_change` listener list. - Every task over two minutes died. No worker ever sent `TaskProgress`, so the 120s progress lease expired mid-render — including during the cold model load, which happens after `TaskStarted`. Workers now report progress and emit a keepalive, bounded by the phase's absolute budget so it renews the lease without deleting the only enforced bound in the system. - The executor rebuilt its engine per task (`return cls()`), so every job paid a cold load. Engines now share one instance cache with the router, resolved by the assignment's engine — never `get_active_tts_backend()`, which returns the worker machine's own Settings preference and would silently run the wrong engine. - One lease expiry took a worker offline permanently: parked slots were never reclaimed. Parks now expire on a TTL, and are deliberately NOT reconciled against the worker's own load report — at a ceiling of one the only task such a worker can report is the wedged one, so "busy" would drop the park and the next idle heartbeat would hand out a slot with a live GPU thread (#730/#1190). - A worker that dropped and reconnected mid-render had every liveness frame discarded: task frames were fenced on the live session epoch, which bumps on every reconnect, while the worker echoes the ref stamped at dispatch. The control plane then expired a task whose GPU was still rendering, and swallowed the failure report when it went wrong. Fenced per attempt instead. - A result from one worker could commit another's task, after which the owner's real delivery arrived as a duplicate and its audio was discarded. "Unknown attempt" and "another worker's attempt" are no longer the same answer. - An oversized result was a poison pill, re-sent identically on every reconnect and permanently disconnecting the worker. It is now a terminal `RESULT_TOO_LARGE`, which is also classified — it was falling through to TRANSIENT and retrying a re-render that could never fit. - `_store_inline` joined the artifact directory with worker-supplied ids, and `os.path.join` discards its prefix on an absolute component. Paths are now minted control-plane-side and resolved through `core.path_security`. - Remote synthesis bypassed `mark_synthetic`, and the guard that exists to catch exactly that walked only `backend/api` and `backend/services` — so it stayed green while a fourth unmarked producer shipped. Marking moved to the worker's tensor stage; the guard now walks `backend/worker` too. Also adds pre-rendered voice previews (`services/gallery.py`), so browsing the gallery no longer needs a GPU or a downloaded model. The manifest is verified against the updater's release key already baked into the binary; a fresh install hears voices without downloading 2.4GB first, and everything falls back to local rendering when the gallery is unreachable. Verified on hardware, not just in CI: 1728 characters submitted to an RTX 4090 returned 105.94s of 24kHz audio in 23.9s, committed and served from the artifact store. Not yet done, and deliberately not claimed: the keepalive fix cannot be exercised end-to-end on fast hardware, because any job long enough to reach the 120s lease produces audio past the 8MiB inline cap. Chunked `UploadResult` has to land first. Pinning to the worker the user chose is also still absent, so "Remote" reaches a remote GPU but not necessarily the one on the badge. | 1 个月前 | |
feat(electron): add full VoiceStudio desktop app | 4 天前 | |
chore(lint): remove unused imports + variables (ruff F401/F841) (#210) Autofixes the genuine lint behind the CodeQL py/unused-import and py/unused-local-variable note-level alerts — actually removing the dead code rather than dismissing it. 68 safe fixes via 'ruff check --select F401,F841 --fix' across 29 backend files (dead stdlib/symbol imports like io/sys/json/torch/typing.Optional and unused locals). Only ruff's safe fixes applied — the 9 'unsafe' fixes and the audio_dsp numpy availability import were left untouched. Not touched: empty-except (needs per-site judgement, not autofixable); frontend js/unused-local-variable (eslint no-unused-vars has no autofix); the loopback-low-risk path/log/stack-trace alerts (real, left visible). Verified: full tests/ suite unchanged at 601 passed (the 2 test_supertonic3 failures are pre-existing on main, local .venv state, green in CI). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 3 个月前 | |
fix: repair the resolved Hugging Face cache | 1 个月前 | |
feat(electron): add full VoiceStudio desktop app | 4 天前 | |
feat(dub): Voice match toggle — per-line prosody vs one consistent reference per speaker (#1147) * feat(dub): Voice match toggle — per-line prosody vs one consistent reference per speaker Owner report: "still 4 segments different in voice as they are 4 times done from each segment?" — Wave 3.2 clones each dub line from a reference cut from its OWN source audio (great prosody match), but the voice IDENTITY drifts line to line, and heuristic-diarized jobs have no pooled speaker clones to anchor it. The precedence was hardcoded; now it's a per-dub-job setting. DubRequest.voice_match: - "per_line" (DEFAULT, unchanged): segment clip preferred, speaker clone fallback — byte-identical to the previous behaviour. - "consistent": ONE reference per speaker for the whole dub. `auto:` bindings use the pooled speaker clone; when none exists (heuristic diarization skips extraction entirely — the key case) a deterministic pick among that speaker's segment clips (longest ≥3 s, tie-break lowest segment id) is reused for every line. Server-default self `auto-seg:` bindings join the pick (they're what prepare stamps on heuristic jobs — the Voice dropdown can't even render them, so no user choice is overridden); explicit CROSS auto-seg bindings still honour their clip. The shared pick is multi-use, so it stays warm in the clone-prompt cache (#1132 cache_ref semantics) at both the main generate and the OOM-retry call site. voice_match is part of the segment fingerprint when non-default (mixed in like track_lang, so all stored hashes keep their values): flipping the toggle marks segments stale instead of letting "Regen changed" splice mixed-identity voices (#281 class). The client sends the mode on both /tools/incremental recompute paths. UI: a compact Voice-match Segmented control next to the Timing picker in the dub panel, persisted in the prefs slice; labels + tooltips in all 21 locales. Tests: resolution through the real dub_generate path for both modes (incl. the 4-segment heuristic job unifying on one ref — fail-before/pass-after), pick determinism + tie-breaks, schema validation, fingerprint semantics, and frontend store→request wiring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(changelog): Voice match toggle entry under Unreleased (#1147) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: mergetest <nizam4103@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> | 2 个月前 | |
fix: close lifecycle and diagnostic review regressions | 2 天前 | |
feat(dub): karaoke word-highlight caption burn-in (#1764) Adds opt-in word-timed ASS karaoke captions while preserving the existing line-caption default.\n\nCo-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com> | 17 天前 | |
feat(engines): open the door between the LLM family and its providers (#1538) * feat(engines): open the door between the LLM family and its providers The openai-compat family entry and the LLM Providers panel are one system — llm_backend resolves every call through the active provider — but the UI presented them as unrelated (council coherence finding). Now: - the catalogue's openai-compat row carries a 'Provider · model' hint naming the endpoint that actually answers (decorative: a provider registry hiccup degrades to no hint, never a failed listing) - the row offers 'Configure providers' straight into Settings → LLM Providers; the panel gains the backlink into catalogue → LLM family - three new strings in all 21 locales, matching each file's provider terminology Also: bugReport's encoded-ceiling test is hermetic now — it was the one test in its file trusting ambient fetch, and hung on any machine where a local backend holds the port without answering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(i18n): the catalogue note names the ACTIVE provider, not the edited one CodeRabbit on #1538: the panel can be editing a provider that is not active, and 'this provider answers…' then points at the wrong one. The note now says the provider MARKED ACTIVE answers, which is true under any selection — no state-dependent copy needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
test(llm): complete OrcaRouter provider contract | 1 个月前 | |
fix(net): SOCKS-proxy users can synthesize again — ship socksio, cache-first model resolution (#959) (#966) * fix(net): SOCKS-proxy users can synthesize again — ship socksio, cache-first model resolution, degrade LLM clients (#959) Under ALL_PROXY/HTTPS_PROXY=socks5:// without socksio installed, httpx raises ImportError AT CLIENT CONSTRUCTION ("Using SOCKS proxy, but the 'socksio' package is not installed"). huggingface_hub's get_session() builds exactly that client inside snapshot_download, so POST /generate 500'd with the bare message even for a fully installed model, and preload_model's model_info probe hit the same error and silently skipped warm-up. Latent since v0.3.5 — #947's fresh-process engine spawning unmasked it in v0.3.10 by handing the user's proxy env directly to a clean backend process. Three layers, so the class (any session-construction failure) is dead, not just the reported instance: * Ship SOCKS support: socksio>=1.0 in [project] dependencies (pure Python, MIT, zero transitive deps) AND in backend.spec hiddenimports — httpx imports it lazily in try/except, so PyInstaller's tracer misses it and the frozen installers would stay broken without the explicit entry. uv.lock regenerated; `uv lock --check` and `uv sync --frozen` (the Docker/release bootstrap semantics) verified. * Cache-first model resolution: from_pretrained's snapshot resolution extracted into _resolve_snapshot_dir() — local dir, else snapshot_download(local_files_only=True) (a complete cache resolves with NO HTTP session constructed), else the original network path. preload_model's failed network probe now falls back to a cache-only check and warms up anyway instead of silently skipping (honest log either way). * Class guards: resolve_skill_client wraps OpenAI() construction — env-shaped construction failures degrade to the existing "LLM unavailable" contract instead of 500ing the calling feature; and core.failure learns SOCKS_PROXY_SUPPORT_MISSING with an actionable hint, appended on the raw-string surfaces (global 500 handler, model-install SSE) via the new append_hint(). Fail-before/pass-after verified by reverting the fix: 11 of the 12 new tests fail pre-fix (the remaining one is the unchanged network-fallback contract). 165 tests green across the touched suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add SOCKS-proxy resilience under [Unreleased] (#966) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
feat(privacy): Settings → Usage — local-only insights instead of cloud analytics (#1114) * feat(privacy): Settings → Usage — local-only insights, the answer to cloud analytics A PostHog integration was proposed and rejected (PR #1110, closed): sending usage events to a third-party endpoint would break the one promise this product is built on — nothing leaves your machine — and local-first is the reason people choose it over ElevenLabs. But the question analytics was meant to answer ("how am I using this?") is a fair one, so answer it locally. services/local_stats.py aggregates the history the app has ALREADY written to the user's own SQLite DB: takes, audio produced, compute time, starred, active days, voices/dubs/projects/exports, and distributions by mode and language. GET /stats/usage serves it over loopback; Settings → Usage renders it. The three properties that stop this becoming telemetry by accident: - READ-ONLY. No new table, column, or event stream. Delete the feature and not one byte of stored data changes. - NO CONTENT. Counts and totals only — the `text` column of a take is never read and never returned; no paths, no ids, no person. Pinned by a test that asserts the payload contains no take text, no /Users/ path, no row id. - NO NETWORK. There is no client, no endpoint, no token. It has no way to send anything anywhere. The panel states the guarantee in the UI, because a privacy promise the user can't see isn't worth much. Route added to the API-surface snapshot (the inventory guard caught it, as designed — one line: GET /stats/usage). 4 backend tests (aggregation / never-leaks-content / empty install / missing table degrades to 0) + 4 frontend tests. Backend suite 2924 passed; lint, format, typecheck clean. Closes the analytics question opened by #1110. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(settings): use the real --chrome-fg-dim token in UsageTab (css-token guard) cssTokens.test.js is a frontend guard that every var(--…) a component references actually exists — an undefined custom property with no fallback is an invalid declaration, so the style silently does nothing. UsageTab referenced --chrome-fg-subtle, which doesn't exist; the dim sub-label token is --chrome-fg-dim (what the other settings panels use). My miss: I ran the full BACKEND suite but only the two new frontend test files, so this guard never ran locally. Full frontend suite now green (1211 passed). 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> | 2 个月前 | |
fix: detect wide EPUB encoding after XML whitespace | 1 天前 | |
feat(longform): canonical Python parser + golden corpus (#27 slice A) (#465) The longform marker dialect (# heading / [voice:] / [pause] / SSML-lite) was parsed by three independent code paths that already disagreed (client vs server on [pause] units, [voice:] empty, H1-only chapters). This lands the single canonical Python parser; the JS port + cross-impl test follow in slice B. - New backend/services/longform_parser.py — parse_script_to_spans(text, *, default_voice, default_speed) + _parse_chapter_body (the reusable voice→pause →SSML layering the JS twin mirrors). Moves the H1/voice regexes verbatim from audiobook.py (already CodeQL-cleared), reuses parse_pause_markers + ssml_lite unchanged. Coerces None→"" and normalizes CRLF/CR→LF at entry (cross-platform parity so Windows-authored scripts never carry a stray \r). Adds default_speed plumbing (inline SSML speed overrides the per-line default). - audiobook.py: parse_audiobook_script is now a thin wrapper that wraps the canonical span dicts in Span/Chapter/AudiobookPlan — public return type and .to_dict() shape unchanged, all four router call sites untouched. Deleted _parse_spans / _HEADING_RE / _VOICE_RE and the now-dead `import re` + parse_pause_markers import. - tests/fixtures/longform_parser_cases.json — 78-case golden corpus (≥40 required) covering §A–I: H1-only chapters (H2–H6 + `# ` no-title → body), the full pause dialect incl. the NO-MATCH boundary, banker's-rounding ties ([pause 0.5]→0, [pause 1.5]→2), [voice:] empty→default, [voice:[nested]] literal, SSML nesting/spell/unknown-tag, speed override, CRLF, combined precedence. Generated from actual parser output (the truth the JS port must match). - tests/test_longform_parser.py — parametrized over the corpus + None-input + ReDoS-linearity (5000× repeats < 1 s). 130 passed (corpus + test_audiobook + test_pause_markers + test_ssml_lite all green); CJK guard green. | 3 个月前 | |
chore(audiobook): correct issue refs to #1208 + changelog entries | 1 个月前 | |
feat(audiobook): durable crash-resume for interrupted longform renders (#470) * feat(audiobook): durable crash-resume for interrupted longform renders Chapter WAVs were already content-addressed (a re-run reused finished chapters), but resume only worked if the user could re-submit the EXACT script — impossible for Stories, whose plan is compiled from cast+lines. This persists the plan itself so an interrupted render is resumable without the original input. - New services/longform_resume.py (pure file/JSON): on render start, write a resume.json manifest (compiled plan + render params + title) into the job work dir, atomically; clear it on successful completion. read/has/clear/build helpers, schema-versioned (a foreign/corrupt manifest is ignored, never resumed). - _render_longform_sse: accepts an optional job_id + resume flag (resume reuses the original job row + cached chapters instead of creating a new one); writes the manifest at start, clears it on done. Both front doors (/audiobook, /longform/render) unchanged for callers. - GET /audiobook/jobs — lists interrupted renders (running/failed longform jobs that still have a manifest; a job left "running" across an app restart is interrupted by definition), with title + total/done chapter counts for the UI. - POST /audiobook/resume/{job_id} — rebuilds the plan from the manifest and replays _render_longform_sse under the original job_id; the content-addressed cache makes finished chapters instant, so only the unrendered ones synthesize. 404 on unknown id / missing manifest. Resume durability is best-effort — a manifest failure never blocks the render. The resume UI affordance is a follow-up (the endpoints are ready for it). Tests: tests/test_longform_resume.py (7, pure manifest round-trip / version & corrupt rejection / atomic write — monkeypatches OUTPUTS_DIR, no global core.config stub so the shared tests/ session isn't polluted) + backend/tests/test_audiobook_resume_api.py (6, config-stub: jobs-list with progress, failed-included, done/manifestless/non-longform excluded, resume 404s). 13 passed. CJK green. Stale module docstring updated. * fix(audiobook): confine resume paths — py/path-injection (CodeQL) + quality The default-setup CodeQL (security-and-quality suite) flagged the crash-resume work: longform_resume built filesystem paths from job_id, which on the POST /audiobook/resume/{job_id} endpoint is a request-supplied path param → py/path-injection (10 high-severity sinks: open/replace/remove/makedirs/isfile). - longform_resume.work_dir now confines like profiles._voices_path: reject an unknown job_type or an id that isn't a bare safe token (^[A-Za-z0-9_-]{1,64}$), then realpath + startswith(OUTPUTS_DIR + os.sep) — a crafted id (`../`, NUL, separators) can never escape OUTPUTS_DIR. Returns None on violation; all callers (manifest_path/read/write/clear/has) degrade gracefully. - The resume endpoint also gates the path-param id up front (404 on a bad token) — barrier at the source as well as the sink. Also cleared the quality alerts the same diff introduced: - py/repeated-import: the 4 inline `from services import longform_resume` calls collapse to one module-top import (it's pure, no torch). - py/empty-except: the best-effort manifest blocks now logger.debug instead of a bare `pass`. 13 resume tests still pass; all job ids in tests are safe tokens. * fix(audiobook): sanitize resume job_id at the source (path + log injection) The first CodeQL pass wasn't enough: resume made job_id request-controlled, so it tainted not just the manifest paths but the EXISTING work-dir join and the progress log lines too (py/path-injection + py/log-injection, ~14 alerts). Fix at the source so the whole dataflow is clean: - _render_longform_sse strips job_id to a safe token (`re.sub` removing anything but [A-Za-z0-9_-], capped 64) right after it's resolved — no path separator, no CR/LF can survive, whether the id came from the resume path param or a fresh uuid. - The work dir now routes through longform_resume.work_dir, which adds the proven os.path.basename(seg)==seg barrier (the shape CodeQL accepts in _voices_path) on top of the realpath+startswith confinement — so the join and every path derived from it (meta/concat/out) is sanitized. - The best-effort manifest-write log no longer interpolates the raw exception (uses exc_info); clear_manifest's OSError handler returns instead of bare pass (py/empty-except). 13 resume tests still pass. * fix(audiobook): launder resume job_id via trusted FS scan (CodeQL path/log-injection) The custom realpath/regex barriers weren't in CodeQL's recognized sanitizer set, so the request-supplied resume job_id kept tainting the work-dir/manifest paths and the progress logs. Switch to the pattern CodeQL does accept — launder the id through a trusted filesystem enumeration: - longform_resume.scan_resumable() lists resumable jobs by scanning OUTPUTS_DIR for <type>_<id>/resume.json; every id it returns is sourced from os.listdir (never request input). - POST /audiobook/resume/{job_id} now only resumes an id that scan_resumable() reports (membership match), and uses the (job_type, job_id) pair FROM that trusted list for everything downstream — so nothing request-controlled reaches a filesystem path or a log line. - GET /audiobook/jobs lists from scan_resumable() too (filesystem-sourced ids). work_dir keeps the realpath+startswith+basename confinement as genuine defense; the render path's job_id is now always either a fresh uuid or a laundered id. 13 resume tests still pass. * fix(audiobook): exact-match allowlist on the work-dir name (CodeQL path-injection) The remaining 4 path-injection alerts were inside work_dir: I validated job_id with an anchored regex but then joined a DIFFERENT f-string (`{job_type}_{job_id}`), so CodeQL didn't carry the sanitization to the joined value. Mirror the pattern the repo's _safe_cover_path uses (which CodeQL accepts): validate the WHOLE joined component against an exact-match allowlist regex (_SAFE_SEG_RE), then confine with os.path.commonpath containment (the recognized barrier) instead of startswith. 13 resume tests still pass. * fix(audiobook): basename-sanitize the work-dir name for CodeQL path-injection The exact-match regex alone wasn't credited; route the joined value through os.path.basename() first — the sanitizer CodeQL recognizes (mirrors _safe_cover_path) — then the regex + commonpath. Functionally identical (no separator in the name) but clears the 4 remaining alerts. 13 tests pass. * fix(audiobook): allow-list membership guard launders resume job_id (CodeQL) The next(... if pair[1]==job_id) comparison-select didn't sanitize for CodeQL. Build a dict of resumable ids from the trusted scan and gate with 'if job_id not in resumable' — the membership barrier CodeQL recognizes — then use job_id directly downstream. 13 tests pass. * fix(audiobook): eliminate request→path flow in resume (definitive CodeQL fix) Five rounds of recognized path-injection barriers (regex, basename, exact-match, commonpath, membership-guard) still left CodeQL flagging the resume job_id → work-dir/manifest/log flow. Remove the flow entirely instead of guarding it: - scan_resumable() now returns {job_type, job_id, manifest_path} where manifest_path is built from the os.listdir dir name (trusted), plus load_manifest_file(path) / discard_manifest_file(path) that operate on those trusted paths. The request job_id is used ONLY to *select* a scan entry, never to build a path. - POST /audiobook/resume/{job_id} reads the manifest via the trusted scan path and renders under a FRESH server uuid (job_id=None). The chapter cache is content-addressed (keyed by chapter content, not the job id), so finished chapters still hit instantly — resume works, but the request's id never names a work dir, output file, or log line. - The interrupted job's manifest is discarded (trusted path) once the fresh-id resume kicks off, so it stops showing as resumable. Net: no request-controlled value reaches any file operation or log on the render path (job_id there is always a server uuid). work_dir keeps its confinement barriers as defence-in-depth. 13 resume tests pass. | 3 个月前 | |
feat(longform): two-pass loudnorm measure orchestrator + wiring (#28 slice 2) (#455) * feat(longform): two-pass loudnorm measure orchestrator + wiring (#28 slice 2) Completes accurate ACX/podcast mastering end-to-end (builds on the pure builders from #28 slice 1). - `services/loudness.py` — `measure_loudness(ffmpeg, concat, preset, *, job_id)`: runs ffmpeg's measure pass, parses the loudnorm JSON → MeasuredLoudness. **Never raises** — skip / non-zero rc / rc None / asyncio.TimeoutError / spawn OSError / empty or unparseable stderr / silent program all WARN + return None → single-pass fallback (a slow/broken measure degrades the master, never aborts the render). Logs rc + a static message only, never the raw stderr (path-safe / local-first). UTF-8 decode with replacement (Windows-cp safe). - `_render_longform_sse` (audiobook.py): between the concat write and the mux, when `loudness` is a known preset (acx/podcast; same `.lower()`/no-strip gate as the builders) → emit a `mastering` event, measure, and pass `measured` into `build_render_cmd` (two-pass apply; `None` → single-pass). `done` gains a `loudness` block {preset, target_i, target_tp, two_pass, measured_i} ONLY for a requested preset — off/None paths keep the byte-identical legacy `done` shape. Both front doors (/audiobook + /longform/render) get it via the shared generator. Chapter cache key is deliberately untouched (loudness-agnostic → acx/off reuse the same cached WAVs; no re-render, no cache-layout break). Tests: `test_loudness.py` (14 — happy fixture, skip-without-spawn for off/ unknown/whitespace/None, non-zero/None rc, timeout-not-propagated, OSError, empty/unparseable stderr, non-UTF-8 stderr, job_id+argv forwarding) + 2 e2e cases (mastering event + done.loudness present for acx; absent for off). Orch tests run locally (stubbed run_ffmpeg, no torch); e2e on CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(loudness): lazy-import run_ffmpeg so the measure stub survives sys.modules purges test_loudness monkeypatched services.loudness.run_ffmpeg, but the route-shape fresh_app fixture purges services.* from sys.modules, so under the full-suite ordering the patch missed the re-imported module → real ffmpeg ran → 3 failures. Lazy-import run_ffmpeg inside measure_loudness and patch it at its source (services.ffmpeg_utils.run_ffmpeg) so the stub is always picked up at call time. Verified by running the purging suite + test_loudness together (31 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 3 个月前 | |
feat(mcp): MCP server v1 — mount on /mcp, per-agent voice binding, stdio shim (Wave 2.2) (#368) * feat(mcp): MCP server v1 — mount on /mcp, per-agent voice binding, stdio shim (Wave 2.2) The FastMCP server (previously dead code, never mounted) is now mounted on the main FastAPI app at /mcp via Streamable HTTP, with its session manager composed into the app lifespan through an AsyncExitStack (best-effort: a missing mcp package or OMNIVOICE_MCP_DISABLE=1 never breaks startup). streamable_http_path set to '/' so the sub-mount lands at /mcp, not /mcp/mcp. Adds the 'mcp' dependency (1.27.x). Per-agent voice binding (Spec 2 headline): each MCP client sends an X-OmniVoice-Client-Id header; generate_speech resolves the voice as explicit arg > the client's binding > global default > app default. New mcp_client_bindings table (alembic 0004 + _BASE_SCHEMA, additive/idempotent), services/mcp_bindings.py (CRUD + resolve_voice + best-effort last_seen), and a loopback-gated REST router (/api/mcp/bindings) the Settings panel drives. New transcribe tool (base64 audio in, 200 MB cap). Stdio shim (backend/mcp_shim, httpx-only, ported from voicebox MIT) proxies stdio clients to the mounted endpoint and forwards OMNIVOICE_CLIENT_ID as the binding header. Settings → Sharing gains an MCP bindings panel. Docs: docs/mcp.md (both connection modes + binding REST) and docs/mcp.json updated to the shim form. Tests: bindings service + resolution precedence + migration up/down (pure, run locally); REST CRUD + mount-not-404 + disable-flag (main-importing, validated in CI). MCP build + mount + initialize handshake verified out-of-band (no torch). Spec: docs/competitive-analysis.md Spec 2 / parity program Wave 2.2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(mcp): assert /mcp mount via app.routes, not a lifespan client The two main-importing mount tests ran the app lifespan, which now starts the FastMCP session manager and binds asyncio queues to the test loop — contaminating later lifespan-running tests ('bound to a different event loop'). The mount happens at import time, so inspecting app.routes for the /mcp Mount is the correct loop-free assertion. Same fix shape as the Wave 0.2 consent tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(mcp): stop reload-main poisoning across the MCP test files Root cause of the CI failure: the bindings REST fixture set OMNIVOICE_MCP_DISABLE=1 and reloaded main but never restored it, so a later 'from main import app' in test_mcp_mount saw /mcp un-mounted ({'/audio','/voice_audio'}). Reloading main mutates the shared module for every subsequent test. - REST fixture: drop the disable flag (the mount is harmless without a lifespan), yield the client, and restore main (+ core.config/db) to the default data dir in teardown so the global module is clean again. - test_main_mounts_mcp_route: reload main with the disable flag cleared so the assertion is independent of any earlier reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 3 个月前 | |
fix(dub): keep language and media tools ready (#1679) Fixes #1677 and #1678. Publishes first-run media tools to the live backend, provides precise cross-platform missing-process guidance, and keeps localized source-language selection available before transcription. Includes regression coverage and deterministic model-store test isolation. | 22 天前 | |
fix(catalogue): repair the guidance the Weights-list rename broke The rename to "the engine's Weights list in Model Catalogue" left several messages without a verb, and pointed others at the wrong place: - The offline and create-voice messages say what to do again. - pyannote has no owning engine, so diarization points at Other weights. - The Hugging Face mirror moved to Settings → Network, and voice previews moved to Settings → Storage. - Unloading and switching engines happen in the engine list, not a Weights list. - A bad saved path points at Settings → Storage or the env file. - Docstrings that read "the the" are fixed. The dub stream-drop fallback goes through i18n in all 21 locales. A Dictation pick on a row that is already downloading no longer starts a second install: the row's radio is disabled while it works, and useModelDownloads refuses a second mutation for a repo already in flight. The Supertonic-3 license test checks for the Accept wording. | 8 天前 | |
feat(electron): add full VoiceStudio desktop app | 4 天前 | |
fix(model_manager): add Ascend NPU cache clearing support Add NPU (Ascend) to free_vram() so its cache is properly freed, and to _has_dedicated_vram() so offloading decisions correctly consider NPU as a dedicated-VRAM device. Follows the existing hasattr(torch, 'xpu') pattern already used in device_caps.py and system.py. The project's device_caps.py already detects NPU — this extends the cache-management path to match. | 1 天前 | |
fix: clear cancelled share runtime | 1 个月前 | |
fix(dub): dialogue starts stop snapping to footsteps — sustained-energy onsets, bounded snap (#963) (#967) * fix(dub): dialogue starts stop snapping to footsteps — sustained-energy onsets, bounded snap distance (#963) Field report #963 (point 3): dubbed speakers start seconds early or late. The reporter's own theory was right on the money — 'when a noise is heard (a sigh or footsteps), it's interpreted as the start of the conversation.' The #280 onset snapper took the FIRST 20 ms frame above an adaptive RMS threshold as the speech onset, so any transient qualified; it also had no snap-distance bound (a wrong onset could move a start by the whole segment minus 0.3 s) and ran even when Demucs had failed and the 'vocals' track was really the raw mix, where every ambient sound is a candidate. Three layered guards, all pure NumPy (no new deps): - Sustained energy: an onset only counts when >=160 ms of the following 300 ms stays above the threshold. Footsteps/door thuds light up one or two frames and die; syllables keep the energy up. - Bounded snap distance: shifts beyond 1.5 s are only trusted when the skipped span is (near-)silent — that is exactly the genuine #280 whisper start-stretch on the vocals track (Demucs removed the music, leaving real silence), so long trims over silence still work in full. Long jumps over audible content (e.g. quiet speech under the relative threshold) are refused instead of playing the dub seconds late; an isolated transient in the span (<10% audible frames) doesn't block it. - Source-aware: snapping now runs only on the separated vocals track. dub_core detects the Demucs fallback (vocals_path == audio_path, see dub_pipeline) at both call sites and passes separated_vocals=False on mixed audio, disabling snapping — whisper's own timestamps beat a confidently wrong snap when music/ambience is sustained energy too. Tests (tests/test_onset_align.py, fail-before/pass-after): transient burst rejected at detect- and snap-level, transient-only window yields no onset, long jump over audible content refused, bounded shift over audible lead still allowed, >1.5 s trim over true silence still snaps (#280 regression guard), mixed-audio mode is a no-op. 28 pass in the file; full dub-adjacent suites green. Credit: theory and repro description by the #963 reporter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add onset-snap robustness under [Unreleased] (#967) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
fix(gpt-sovits): distinguish HTTP failures from connection failures | 1 天前 | |
feat(electron): add full VoiceStudio desktop app | 4 天前 | |
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. | 1 个月前 | |
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. | 1 个月前 | |
feat(dub): Smart Fit phase B — per-segment video retime export, drift absorption, fitted subtitles (#350) * feat(dub): Smart Fit phase B — per-segment video retime export, drift absorption, fitted subtitles Executes the video side of the Smart Fit plans persisted by Phase A (job["fit_plans"], #347) at export and preview time. Backend: - services/video_retime.py (new, clean-room): two-tier retime executor. ≤48 chunks → the proven single-pass split/trim/setpts/concat filter_complex; above → batches of 40 chunks rendered to intermediate slices (identical libx264 medium/crf20 params, keyframe at t=0) joined losslessly with the concat demuxer. Slices are CFR-resampled (fps=) because setpts leaves VFR-ish timestamps that broke tpad and drifted a frame per retimed chunk on ffmpeg 7.x. Temp slices cleaned on success AND failure/abort. - Drift absorption: fitted track longer than retimed video → freeze-frame tail (tpad=stop_mode=clone) predicted into the last slice / single-pass graph, with residual mux-side tpad; video longer → silence-pad the dub audio chain (apad=whole_dur). ±50 ms tolerance. - VFR guard: probe r_frame_rate vs avg_frame_rate; normalise with fps= before trim/setpts; probe failure degrades gracefully. - Plan resolution: _video_retime_plan_for spans legacy video_stretch_plans (byte-identical resolution + command construction) and fit_plans, gated on the track's own timing_strategy so stale plans never retime a track re-generated under another strategy. - Fitted subtitles: /dub/srt + /dub/vtt accept ?lang= and serve cue times from fitted_segments for Smart Fit tracks; _write_burn_srt does the same for burn-in. burn_subs+retime is now allowed for smart_fit (burn runs AFTER the retime graph); still rejected for legacy stretch_video. - /dub/preview-video resolves the same plan so in-app preview matches export. - Fallback ladder: batch encode failure/timeouts → un-retimed export with a structured core.failure warning (X-Dub-Export-Warning header + job["last_export_warning"]); concat join rejection → one single-pass retry while ≤96 chunks; abort → 409 + proc kill via run_ffmpeg job_id registration (/dub/abort reaches export encodes now) + temp cleanup. Frontend: - Export drawer passes ?lang= on subtitle exports and shows an i18n'd re-encode cost note (~0.5–2× video length on CPU) when a retiming strategy is active — translated in all 21 locales. Tests: tests/test_smart_fit_export.py — plan resolution, batch math, graph parity + new stages, fitted-cue SRT/VTT/burn selection, burn policy, VFR detection; ffmpeg-gated integration renders both executor tiers (batch size forced to 2) and the real /dub/download endpoint, ffprobing durations within ±50 ms across both pad branches. All existing dub export/subtitle/preview/timing tests pass unchanged. Refs docs/competitive-analysis.md Action 1 (dub-length fitting v2); completes Smart Fit (Phase A = #347). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): sanitize Smart Fit retime work paths at every sink (CodeQL py/path-injection) The job_id-derived retime work path (retimed_*.mp4 / preview_retimed_*.tmp.mp4) flowed unguarded from dub_export into prepare_smart_fit_video / render_retimed_video and their derived slice/concat paths and ffmpeg argv. Apply the repo's proven inline realpath+startswith containment pattern (helpers/commonpath are not recognized — see #309/#328/#329/#348): - dub_export.py: validate work_path against DUB_DIR at both construction sites (export + preview) and pass the validated realpath onward. - video_retime.py: make both entry points self-defending — realpath + DUB_DIR containment on out_path/work_path before any derivation, raising RetimeError(stage="plan") on escape; slices_dir/slice_path/list_path and RetimeDecision.file_path now all derive from the sanitized value. DUB_DIR is read via module attribute so test fixtures reloading core.config work. - ffmpeg_utils.py: document that all caller-assembled argv paths are realpath-validated upstream. - tests: sandbox DUB_DIR in the executor integration tests (tmp_path) so the new guard sees the test workspace. No behavior change for valid (server-built) paths — the guard only fires on traversal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(smart-fit): patch DUB_DIR on video_retime's own config ref — survives suite-wide reload The retime guard reads video_retime._config.DUB_DIR at call time; the sandbox fixture patched a fresh 'import core.config' instead. Another test reloads core.config in the full suite, so the two module refs diverged — the patch missed and the guard rejected the test's tmp paths (green in isolation, red in CI's full run). Patch the exact ref the guard dereferences. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dub): resolve DUB_DIR live at call time in retime guards — survive full-suite reload The path-containment guards bound DUB_DIR via a module-level 'from core import config as _config'. Other tests importlib.reload() core.config (sandboxing OMNIVOICE_DATA_DIR), after which the guard checked containment against a stale DUB_DIR while dub_export built the path under the reloaded one — every retime path then 'escaped the dub workspace' (green file-alone, red full-suite: the 5 integration failures CI hit). Re-import DUB_DIR locally in each guard so it always reads the current sys.modules value; simplify the sandbox fixture to patch the canonical module. Verified: full backend suite green on the Smart Fit tests (the 2 remaining settings_store failures are pre-existing on main, unrelated — local data-dir artifact). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): clear CodeQL alerts on Smart Fit export — job_id allowlist, proc-registry decouple - py/path-injection (8, video_retime.py): validate job_id with a strict inline regex allowlist (re.fullmatch [A-Za-z0-9_-]{1,64}) at the entry of dub_download and dub_preview_video, before it reaches any filesystem path or ffmpeg argv. The existing realpath containment guards stay as defense-in-depth; the regex barrier is the sanitizer CodeQL recognizes through the service-module call chain. - py/log-injection (4): newline-strip job_id inline at the logger calls in ffmpeg_utils.run_ffmpeg and the two retime-fallback logger.error sites in dub_export. - py/empty-except (3): best-effort cleanup os.remove handlers now log the OSError at debug instead of bare pass (video_retime + both dub_export mux finally blocks; _discard_tmp too for consistency). - py/cyclic-import (2): break the dub_pipeline <-> ffmpeg_utils cycle for real — the subprocess registry (register_proc/unregister_proc/ kill_job_procs/has_active_procs + state) moves to a new stdlib-only leaf module services/proc_registry.py. ffmpeg_utils now imports it at module top (no lazy import); dub_pipeline re-exports every name so dub_core aliases and tests keep working unchanged. No behavior change for valid inputs; invalid job ids now get a clean 400 instead of a 404/containment error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dub): address #350 review — cancelled-vs-failed retime, logged best-effort excepts, redacted probe logs, narrowed test assert - rc<0 (killed by user cancel) now raises RetimeError(stage='aborted') instead of reporting an ordinary render failure (CodeRabbit) - best-effort cleanup/QC-event excepts log at debug instead of bare pass (CodeQL empty-except x3) - probe failure logs use basename, not full user paths (CodeRabbit/CodeQL) - test_render_cleans_slices_on_failure asserts RetimeError, not Exception Rebuttals (no change needed, see PR comment): fitted-cue subtitles track the fitted AUDIO timeline which is correct even on retime fallback; the planner only emits stretch ratios >1 so the early-exit guard is a true no-op check; '\'' is ffmpeg's own utility quoting for concat lists; has_active_procs is an intentional re-export (noqa'd). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 3 个月前 | |
fix(pronunciation): say when an IPA/CMU entry is stored but not applied Closes #1949. Settings offers three notations. Only Respelling substitutes text today; IPA and CMU rows save cleanly, are validated, get a badge and can be toggled on, then get dropped before term matching and are never read again. That much is Phase 1 behaving as designed. The defect is that it was INVISIBLE: "Test a sentence" answered "No entries match — spoken as written" for a term that does match. Not a degraded answer, a wrong one — and it sent the user off to re-type an entry that was already correct, or to convert it to Respelling, where a phoneme string is then read as graphemes. docs/specs/01-expressive-tts.md asked for exactly the opposite: such entries "passed through and flagged 'phoneme not honored on this engine' (parity-rule: visible degradation)". That flag was never implemented. This is it. The dry run reports inert entries separately, and the panel names them. The substitution path is deliberately untouched — this does NOT start feeding raw phoneme strings into the grapheme stream, which is the thing Phase 1 refuses on purpose, and a test pins that it still refuses. Not Phase 2. Lowering IPA/CMU to engine markup is a real feature per engine and stays open; what changes here is that the gap is now honest rather than silent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S | 9 天前 | |
feat(settings): LLM Skills — per-feature enable/route control for every LLM call (#912) New Settings → System → LLM Skills area: every LLM-powered capability (Cinematic & Autofit translation, speech-rate slot fitting, glossary auto-extract, direction parsing, dictation cleanup) becomes a "skill" the user can toggle or route to a specific provider (local Ollama/LM Studio vs a remote key) instead of everything riding the one global active provider. Backend: - services/llm_skills.py — skill registry + settings_store persistence (llm_skill.<id>.enabled / .provider), resolution precedence override > active > none, resolve_skill_client() (OpenAI-compat client bound to the effective provider; None when disabled/unconfigured) and skill_backend() (OffBackend when disabled — the exact no-LLM object every caller already degrades on). - All five consumption points wired through the registry; a disabled skill degrades exactly like "no LLM configured" today (Fast translation fallback, refinement pass-through, heuristic direction parse, no-llm slot fit, 503 on glossary auto-extract). No new degradation modes; defaults (enabled + no override) keep existing setups byte-identical. - OpenAICompatBackend gains an optional bound provider (None = active, the historical behavior). - GET /api/settings/llm-skills + PUT /api/settings/llm-skills/{skill_id} (404 unknown skill/provider); route snapshot updated. Frontend: - LLMSkillsPanel (Sparkles, next to LLM Providers): one row per skill — i18n name/description, enable toggle, provider Select ("Use active provider" + configured providers, local ones tagged), ready / needs-setup badge linking to LLM Providers. All strings via t() (settings.llmskills_*). Tests: 30 backend (precedence, per-consumption-point disabled semantics, endpoint round-trips, validation) + 4 panel render/PUT tests. Docs: translation-engines.md gains an LLM Skills section. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
fix(backend): stop discarding tracebacks in error-level exception logs (#1160 follow-up) #1160 fixed one traceback-losing logger.error in dub_pipeline.save_job; this sweeps the remaining class. 19 sites across 10 files where a real, unexpected failure was summarized as "...: %s" at ERROR level — losing the stack trace that makes crash reports diagnosable — now use logger.exception (diarization/ASR crashes, dictation load/final failures, ffmpeg mixes that silently degrade output, Smart Fit retime fallbacks, RVC init/inference, models.yaml catalog load, gallery search/download 500s, dub-history JSON decode, MCP CLI fatal exit). Deliberately left alone: WARNING/INFO/DEBUG logs, expected classes with self-sufficient messages (GPU/ASR timeouts, request validation, cryptography-availability checks), sites that re-raise immediately (db migration, _ensure_mcp), subprocess returncode checks where stderr IS the diagnosis, and sites already logging exc_info/format_exc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
feat(electron): add full VoiceStudio desktop app | 4 天前 | |
fix(dub): correct long timeline rendering and repeated ASR context | 3 天前 | |
fix(download): segmenta em blocos limitados e retoma o acelerador O downloader segmentado gravava progresso no manifesto apenas quando um segmento INTEIRO terminava, e dimensionava os segmentos como tamanho/num_connections. Num blob de 806 MB isso dava 8 segmentos de ~100 MB: numa conexão que cai a cada ~50 MB nenhum segmento jamais completava, o manifesto nunca era escrito e cada tentativa recomeçava do zero. Pior, o acelerador só rodava na PRIMEIRA tentativa (`_attempt == 1`), então depois da primeira queda todas as retentativas iam para o `snapshot_download` e o `.part` acumulado ficava órfão para sempre. Agora os segmentos são limitados a 16 MB e a concorrência passa a ser controlada por semáforo (antes vinha da própria contagem de segmentos), e o acelerador é preservado entre tentativas quando o erro é de rede — reusando `_is_retryable_download_error`, que já é a fonte única dessa classificação. Ele só é desligado de vez quando a falha NÃO é transitória, ou seja, quando o acelerador de fato não serve naquele host. Reproduzido em rede real: `peer closed connection without sending complete message body (received 54260979, expected 100708200)`. | 10 天前 | |
feat(stream): sentence-by-sentence /ws/tts via ported chunker (Wave 1.4) (#358) Ports Patter's SentenceChunker (MIT, attribution header) behavior-identical — all 61 upstream golden parity scenarios ship as fixtures and pass, including documented quirks (current_behavior xfail semantics mirrored from their parity runner). Terminator tables carry functional CJK; file added to the test_no_hardcoded_cjk allowlist per convention. /ws/tts now splits the request into sentences and synthesizes each in turn, streaming the first sentence's PCM while later sentences are still generating — the time-to-first-audio win on multi-sentence input. Single-sentence requests behave exactly like the old single-shot path; 'start' metadata still waits for the first generation so lazy-loading engines report their true sample rate. Italian comma-decimal guard hard-disables aggressive first-clause flush per upstream. Spec 8a (docs/competitive-analysis.md) / parity program Wave 1.4. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 3 个月前 | |
feat(workers): package headless GPU nodes (#1638) (#1648) Closes #1638.\n\nPackages headless GPU workers with durable enrollment, bounded artifact handling, cross-platform lifecycle cleanup, and regression coverage. Incorporates CodeRabbit, Greptile, CodeQL, and platform-CI findings before merge. | 26 天前 | |
feat(electron): add full VoiceStudio desktop app | 4 天前 | |
fix(moss): detect outdated audio dependencies and harden verification | 1 天前 | |
fix: close log safety review gaps | 1 个月前 | |
fix(dub): restore cast voices from source audio (#1484) * fix(dub): restore cast voices from source audio * docs: record source-audio cast repair * fix(dub): honor explicit cross-speaker cast * fix(dub): sanitize restored cast metadata * fix(dub): preserve legacy per-line cast refs | 1 个月前 | |
feat(dub): save custom agent translation style instructions | 3 天前 | |
Merge branch 'fix/review-2077' into fix/community-integration | 2 天前 | |
feat(longform): SSML-lite prosody markup — [slow]/[fast]/[emphasis]/[spell] (PR 8b) (#421) Inline delivery hints within a narration line, wired into BOTH front doors so Audiobook and Stories behave identically. - services/ssml_lite.py (parallel-built, 18 tests): parse_ssml_lite splits a line into {text, speed, spell, emphasis} segments — nesting (innermost wins), unclosed-to-EOL, stray-close ignored, adjacent-merge; ReDoS-safe literal alternation. + spell_out(). - _parse_spans (audiobook script path) now applies SSML-lite as the innermost layer (precedence: [voice:] → [pause] → SSML); each segment becomes a Span with its speed (threaded to the renderer) and spelled-out text for [spell]. Trailing pause attaches to the run's last segment. - frontend/src/utils/ssmlLite.js: client port (kept in sync with the .py) + storyToSpans applies it per chunk — inline speed OVERRIDES the per-line slider, falls back to it otherwise. Tests: parse_ssml_lite (18 py + 10 js), script-level prosody parse, Stories SSML compile (override + spell). 70 backend + 345 frontend green; build clean. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 3 个月前 | |
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. | 1 个月前 | |
Merge branch 'fix/review-2109' into fix/community-integration # Conflicts: # docs/install/troubleshooting.md | 2 天前 | |
fix: close lifecycle and diagnostic review regressions | 2 天前 | |
refactor: update backend architecture, expand frontend state management, and synchronize voice-pro research modules. | 4 个月前 | |
Merge current main into GHAS empty-except fixes | 1 个月前 | |
fix(tts): normalize complete signed ranges without rewriting chains | 12 天前 | |
feat(dictation): rebuild to Wispr-Flow quality — live waveform, streaming commits, honest insertion, polished text (#888) * feat(dictation): rebuild to instant-feedback quality — waveform, streaming commits, honest insertion, text polish Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): dictation rebuild entry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(lint): Array.from over new Array(n) — oxlint no-array-constructor Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
fix(import): read UTF-32 marks, and survive a non-text declaration Two Greptile P1s on this PR, both real: - A UTF-32 LE byte-order mark starts with the UTF-16 LE one, so the BOM table matched UTF-16 first, stripped two bytes and read the file as NUL-interleaved UTF-16; a UTF-32 BE mark matched nothing and fell through to Windows-1252. The table lives in services.text_upload and is shared, so /dub/import-srt and the .txt/.md branch of /audiobook/import carried the same corruption -- it is fixed once, longest mark first, and both front doors and the EPUB path inherit it. - A declaration such as encoding="hex_codec" passes codecs.lookup but bytes.decode refuses it with LookupError, which would have failed the whole EPUB import. The codecs.lookup pre-check is gone: the decode itself decides, and an encoding that cannot produce text now guesses like an undeclared document does. text_upload gains bom_encoding() so the EPUB path asks the one table rather than keeping a second copy of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | 1 天前 | |
fix(auth): preserve local Hugging Face token paths and cleanup | 12 天前 | |
Merge branch 'fix/review-2143' into fix/community-integration # Conflicts: # CHANGELOG.md # docs/dubbing/translation-engines.md # tests/test_dub_translate.py | 2 天前 | |
feat(dub): LLM translations keep terms consistent and sound spoken — auto-glossary brief + reflect pass (#1050) One up-front LLM pass over the full transcript extracts a theme summary + terminology map, merges it under the user's manual glossary (user entries always win), caches it on the dub job per target language (job_data blob, no schema change), and injects the brief into every per-segment prompt. A new reflect pass then critiques each segment's direct translation for wordiness / stiff register and rewrites it as natural spoken dialogue — any failure or divergence silently keeps the direct translation. Both stages have Dub-tab toggles (default ON for the LLM engine, persisted; MT engines unaffected), with i18n strings across all 21 locales and docs updated. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
fix(dub): a rate-limited polish pass no longer skips fitting, fails the UI, or ignores Retry-After (#1135) * fix(dub): a rate-limited polish pass no longer skips fitting, fails the UI, or ignores Retry-After Observed live (owner's Bengali dub, 4 segments): every cinematic reflect call 429'd against a free-tier OpenRouter model and the UI declared "4/4 segment(s) failed" over a translate that succeeded. Root-causing that surfaced a class, not a message bug: The cinematic reflect/adapt chain is OPTIONAL polish — on any failure the segment keeps its literal translation and is fully usable. But every such degradation (no-llm, reflect/adapt errors, adapt-diverged, wrong-script, cinematic-budget) was reported under the same "error" key as real translation failures. Three consumers took that at face value: 1. useDubWorkflow counted the rows as failed -> the red N/N toast; 2. _stamp_predicted_rate_ratio and _stamp_duration_plan skipped them -> no rate badges, no fits/tight/impossible verdicts; 3. _apply_fit_pass and the condense pass skipped them -> overlong lines went to synthesis unfitted and came out audibly time-compressed at mix. This is a direct contributor to "later segments got worse" in rate-limited Cinematic dubs. Split the vocabulary: "error" now means the row has no usable text (base translation failed); optional-pass fallbacks ride a separate "degraded" key. Downstream filters keep gating on "error" only, so degraded rows flow through every fitting pass. The UI shows an amber "translated, polish skipped (<reason>)" toast and a mild row tooltip instead of a red failure, and editing a row clears the stale annotation. And the retry that makes most of this moot: _chat now honors a 429's Retry-After once (capped at 30s, jittered so the 6-wide segment fan-out does not re-stampede the same window). OpenRouter's free pool says "Retry-After: 2" - giving up instantly turned a two-second wait into a whole failed pass. Tests: producer contract (every cinematic fallback returns degraded, never error - 5 updated + retained), consumer contract (degraded rows still get rate-ratio prediction and duration plans; error rows stay excluded), and the retry (honors small Retry-After with jitter, caps absurd ones, one retry only, non-429s never retry). Full suite: 2981 backend + 1236 frontend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(changelog): correct PR ref to #1135 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(dub): review round — localize the degraded strings, un-suppress the mixed toast, clear stale annotations on edit Three review findings, all valid: - Localization parity (Greptile): the two new user-facing keys existed only in en.json. Every other key in these namespaces is translated in all 21 locales, so the fallback-to-English behavior would have been a regression of the repo's parity convention. Both keys now translated in all 20 non-en locales, inserted beside their siblings. - Mixed responses suppressed the degraded story (Greptile): when a translate returned both real failures and degraded rows, only the red failure toast fired. The degraded warning now fires alongside it — real failures don't erase what happened to the rows that succeeded plainly. - Ordinary edits kept stale annotations (CodeRabbit): the restore path cleared translate_error/translate_degraded but a normal text edit didn't, so a row kept wearing "polish pass skipped" over words the user had just written. Editing the text now clears both annotations. Frontend suite: 1236 passed; i18n probe green across all 21 locales. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: mergetest <nizam4103@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> | 2 个月前 | |
Merge branch 'main' into fix/2201-model-dependent-cloning | 1 天前 | |
fix(export): run the app's own ffmpeg for the video watermark Two call sites still reached for the bare name `ffmpeg` instead of services.ffmpeg_utils.find_ffmpeg(), which every other call site uses. The bare name only resolves a system install: imageio-ffmpeg -- the app's default source, and a locked dependency -- ships its binary as `ffmpeg-<platform>-v<version>`, and ensure_media_tools_on_path() publishes that directory on PATH without giving the file an `ffmpeg` name. So on a host with no separate system ffmpeg, which is most installs: - /export dropped the visible video watermark. is_visible_video_enabled() defaults to ON, the spawn raised FileNotFoundError, and the except arm quietly plain-copied the file -- the user asked for a watermarked export and got an unmarked one with no error anywhere. - video_context._extract_keyframes gated on shutil.which("ffmpeg") and logged "ffmpeg not found, skipping frame extraction", so the dubbing director's visual context was empty while the app's own ffmpeg sat on disk, resolvable. Same shape as #1256. The export also no longer spawns anything when nothing resolves: it goes straight to the plain copy instead of failing a subprocess to get there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | 1 天前 | |
fix(security): authorize export filesystem sinks | 1 个月前 | |
fix: restore desktop integration contracts | 4 天前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 4 个月前 | ||
| 3 个月前 | ||
| 15 天前 | ||
| 1 个月前 | ||
| 23 小时前 | ||
| 4 天前 | ||
| 2 天前 | ||
| 1 个月前 | ||
| 4 天前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 4 天前 | ||
| 4 天前 | ||
| 4 天前 | ||
| 1 个月前 | ||
| 3 天前 | ||
| 4 天前 | ||
| 2 天前 | ||
| 3 个月前 | ||
| 2 个月前 | ||
| 8 天前 | ||
| 11 天前 | ||
| 2 天前 | ||
| 10 天前 | ||
| 1 个月前 | ||
| 10 天前 | ||
| 2 天前 | ||
| 2 个月前 | ||
| 3 个月前 | ||
| 1 个月前 | ||
| 4 天前 | ||
| 3 个月前 | ||
| 1 个月前 | ||
| 4 天前 | ||
| 2 个月前 | ||
| 2 天前 | ||
| 17 天前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 天前 | ||
| 3 个月前 | ||
| 1 个月前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 22 天前 | ||
| 8 天前 | ||
| 4 天前 | ||
| 1 天前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 天前 | ||
| 4 天前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 3 个月前 | ||
| 9 天前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 4 天前 | ||
| 3 天前 | ||
| 10 天前 | ||
| 3 个月前 | ||
| 26 天前 | ||
| 4 天前 | ||
| 1 天前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 3 天前 | ||
| 2 天前 | ||
| 3 个月前 | ||
| 1 个月前 | ||
| 2 天前 | ||
| 2 天前 | ||
| 4 个月前 | ||
| 1 个月前 | ||
| 12 天前 | ||
| 2 个月前 | ||
| 1 天前 | ||
| 12 天前 | ||
| 2 天前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 天前 | ||
| 1 天前 | ||
| 1 个月前 | ||
| 4 天前 |