Commit Graph
16 Commits
Author SHA1 Message Date
6b91205036 fix(dub): completed tracks always show their tabs + history keeps its language (P0) (#956)
* fix(dub): completed tracks always show their tabs + history keeps its language (P0)

Root cause chain: the track switcher's visibility expression required
dubLangCode !== 'und' and ended in a tautology (dubTracks?.length > 0 ||
!!dubTracks), so it was effectively keyed to the language dropdown, not
the persisted tracks. History restore always handed the frontend 'und'
because the dub_history language/language_code COLUMNS froze at the
ingest-time "" — the save_job UPSERT never updated them after generation
set them on the job dict (only the job_data JSON carried the real value).
Net effect: a restored project with finished tracks showed no track tabs
until the user re-picked a language.

- DubTab: hasDubbedTrack = done && dubTracks.length > 0 (tracks only;
  also stops the tautology from showing a trackless switcher).
- DubTab auto-jump: membership-guarded — the preview only jumps to a
  language that has a track, else tracks[0]. Kills the preview-404 class
  (restores falling back to 'en' with tracks ['bn'] pointed the player
  at /dub/preview-video?lang=en).
- dub_pipeline.save_job UPSERT: language/language_code now update when
  non-empty (same CASE guard as content_hash), so new saves heal the
  frozen columns and empty re-saves can't clobber them back.
- App.restoreDubHistory: falls back to job_data's language/language_code
  so EXISTING rows in users' DBs restore correctly with no migration.
- P0.2 polish: track pills get duration + timing-strategy tooltips,
  hydrated lazily and failure-silently from the existing
  GET /dub/tracks/{job_id} via new api/dub.dubListTracks; all new
  strings through i18n (en.json).

Tests (fail-before/pass-after): DubTab-level visibility + auto-jump
membership-guard tests (3 of 4 fail pre-fix), pill-tooltip hydration
tests, and save_job language heal/no-clobber tests (heal fails pre-fix).

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

* docs(changelog): open [Unreleased] with the dub track-tabs fix (#956)

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

---------

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 01:21:23 +05:30
935b38a962 fix(dub): pass OmniVoice's ffmpeg to yt-dlp so URL merge works off PATH (#712) (#716)
Dubbing a video URL on Windows (v0.3.8) failed with 'You have requested merging
of multiple formats but ffmpeg is not installed.' The download format selector
pulls separate video+audio streams, so yt-dlp muxes them via ffmpeg
(merge_output_format=mp4) — but yt-dlp only checks PATH, while OmniVoice's ffmpeg
is typically a bundled Tauri sidecar / imageio-ffmpeg binary that isn't on PATH.

yt_download_sync now sets ydl_opts['ffmpeg_location'] = find_ffmpeg() (the same
resolver the rest of the dub pipeline uses) when ffmpeg is resolvable; if it
isn't, the key is omitted so yt-dlp falls back to PATH as before (no regression).
Tests assert the location is passed when resolved and omitted when not.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:16:45 +05:30
6819feb8b2 fix(dub): skip yt-dlp mtime stamp to avoid [Errno 22] on Windows (#642) (#644)
Dubbing a URL could fail with 'Unable to download video: [Errno 22] Invalid
argument' on Windows: yt-dlp stamps the downloaded file's mtime with the video's
upload date, and an out-of-range/invalid timestamp makes os.utime raise
[Errno 22], aborting the ingest. We download to a throwaway original.* and never
use its mtime, so set updatetime=False (yt-dlp --no-mtime). Regression test
asserts the opt is set. No version bump.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:53:44 +05:30
b15acbaae9 fix(dub+generate): yt-dlp 403 player-client fallback (#625) + non-finite audio guard (#629) (#635)
Two independent fixes from issue triage; no version bump.

#625 — yt-dlp 403 on the media download (some videos serve formats
signature-protected to the default player client) is not transient, so the
existing broken-pipe retry (#579) kept 403ing. The URL download now escalates
the YouTube player client (tv → android → web_safari) on a 403 before giving up;
a 403 no longer counts against the transient-retry budget.

#629 — a numerical glitch in the model (seen on MPS) could leave NaN/inf samples
that write an unreadable WAV; a downstream decode then failed with an opaque
"ffmpeg returned error code: 183 / Invalid data", surfaced to the user as a
misleading "ran out of memory". Sanitize non-finite samples to silence in
_apply_effect_chain (single chokepoint, covers the raw path too) so the WAV is
always decodable, and classify a decode/ffmpeg failure as unreadable-audio
rather than OOM in _oom_friendly_reraise.

Tests: 403 escalation order + success-on-alternate-client; NaN/inf sanitize +
finite-passthrough + decode-error classification. Full suite 1851 passed.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:36:32 +05:30
7b70d82322 fix(dub): retry transient broken-pipe on URL download (#579, #598) (#605)
Pasting a video URL into the dubber could fail outright with
`download: Unable to download video: [Errno 32] Broken pipe`. A broken
pipe raised while the write side of a pipe closes mid-stream (a killed
ffmpeg merge child, a CDN reset during muxing) aborts the whole
`extract_info` call and is NOT covered by yt-dlp's own per-fragment
retries, so a single transient blip killed the entire ingest.

Root cause: no download-level retry around `yt_download_sync`'s
`extract_info`. The failure was already classified as
`VIDEO_DOWNLOAD_NETWORK` (#554/#536) and carried a "just retry" hint, but
nothing actually retried.

Fix: wrap the download in a bounded retry (1 + 2 attempts) that retries
only on transient/broken-pipe-class failures, reusing the single
`failure.classify() == VIDEO_DOWNLOAD_NETWORK` taxonomy (plus the
BrokenPipeError/ConnectionError classes) rather than a parallel keyword
list. Partial `original.*` files are wiped between attempts so a
half-written download can't poison the next try. Unsupported links still
fail fast with their own hint (no wasted retries); after retries are
exhausted the existing actionable network hint is surfaced.

Adds tests/test_dub_download_retry.py: retryability classification +
retry-then-recover, bounded give-up, and no-retry-on-unsupported-URL.
Fails before (no retry loop / helper), passes after.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 05:09:51 +05:30
d6562d6f30 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>
2026-06-12 14:06:00 +05:30
853b9eefc7 fix(dub): burn translated subtitles, fix subtitle save JSON error (#309) (#328)
* fix(dub): burn translated subtitles, fix subtitle save JSON error (#309)

Two symptoms, one root: the job kept the original-language ASR transcript
while the editor only sent translated/edited text in the generate request.

- dub_generate now persists the segments the dub was actually generated
  from back onto the job (metadata carried over by stable id, fallback
  index; text_original retained for dual-subtitle layouts) — SRT/VTT
  export and ffmpeg burn-in now render the dub language, not the source.
- The SRT/VTT export endpoints honor the save_path query param the Tauri
  save dialog appends (like every other export) and return the standard
  JSON envelope — previously they ignored it and returned the raw body,
  so the frontend's JSON.parse choked on the SRT cue index ('Unexpected
  non-whitespace character after JSON').
- Frontend guards the save response content-type so any future raw-body
  response surfaces as a clear error.

Fixes #309

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

* Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix(dub): use the file's established realpath+startswith containment idiom (CodeQL)

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

* fix(dub): write subtitle saves from the Tauri process, not the backend (#309)

The backend save_path variant on /dub/srt and /dub/vtt routed a
user-controlled destination through the loopback HTTP surface — six new
CodeQL path-injection flows plus two log-injection flows. Subtitles are
small text bodies, so the frontend now fetches them raw and writes the
file via a new save_text_file Tauri command: the OS save dialog in the
trusted process is the write authorization, and the backend never sees
a destination path. Binary exports keep the established save_path flow.
Also strips newlines from user-derived values in the two flagged log
lines.

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

* fix(dub): leave _native_save byte-identical to main

The newline-strip on the log line moved a path sink onto a changed line,
which made CodeQL re-attribute the long-standing binary-export flow to
this PR as a new alert. The subtitle endpoints no longer feed this
function at all, so restore the exact original line — the baseline alert
stays baseline, and hardening pre-existing flows belongs in its own PR.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-11 12:15:53 +05:30
Palash DebnathandClaude Opus 4.8 47729057bd 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>
2026-06-01 07:53:06 +05:30
8fa54ccae4 l10n(zh-CN): full Chinese localization + Windows/settings fixes (absorbed from #66) (#157)
* @
fix: skip torch.compile on Windows where Triton is unavailable

torch.compile with mode="reduce-overhead" depends on Triton, which has no
official Windows support. On Windows the compile call succeeds but generates
code paths that crash at inference time with an OOM-like error
("Cannot find a working triton installation").

Check for Triton availability before compiling so TTS gracefully falls back
to eager mode on platforms without Triton.

Closes #65
SummerSec
@

* feat: comprehensive Chinese (zh-CN) localization for Settings and navigation

Add full Chinese (zh-CN) translation support across the frontend:
- NavRail, Launchpad, Clone/Design tabs, Settings (all tabs)
- Sidebar navigation labels, hero text, action cards, section headings
- Fix: Settings missing General tab in TABS array
- Fix: i18n locale not persisted after page reload (useEffect deps)
- Fix: NavRail key prop spreading into JSX elements

Co-authored-by: SummerSec

* fix: translate production override parameter labels (Speed, t_shift, etc.)

* fix: translate voice design category labels (Gender, Age, Pitch, etc.)

* feat: translate Transcriptions and Voice Gallery pages

* fix: translate gallery category names (Disney, Anime, etc.)

* feat: translate DubTab, personality presets, and voice design presets

* fix: remove duplicated emoji in personality name translations

* fix: correct preset translation keys to match actual preset IDs

* fix: filter natural language from personality instruct to prevent validation error

* fix: handle edge case where instruct has no valid tags

* chore: remove debug logging from personality instruct filter

* fix: address CodeRabbit review — importlib.util, English comment, grammar, theme, placeholder

* fix: localize selected category label in VoiceGallery header

* feat: translate remaining DubTab UI text (CAST, Generate Dub, Translate All, etc.)

* fix: improve ffmpeg detection on Windows, error messages, and yt-dlp download timeout

* feat: add proxy setting in Settings → General for downloading via proxy

* fix: improve ffmpeg detection on Windows, error messages, and yt-dlp download timeout

* feat: allow HTTP_PROXY/HTTPS_PROXY env vars via /system/set-env

* fix: support SOCKS5 proxy, also set ALL_PROXY env var

* fix: increase yt-dlp extractor retries for subtitle 429 errors

* feat: translate prep overlay stage labels (download, extract, demucs, scene)

* feat: translate BatchQueue, VoiceProfile, ToolsPage, Projects pages

* feat: translate SetupWizard, DonatePage, EnterprisePage + fix NotImplementedError handling

* feat: add ffmpeg status + manual path setting in Settings → General

* fix: validate ffmpeg path exists when user sets it manually

* fix: fall back to thread-based subprocess when asyncio raises NotImplementedError on Windows

* fix: pin setuptools<70 — ctranslate2 requires pkg_resources removed in 70+

* fix: translate transcribing overlay text

* fix: complete DubTab zh-CN localization + argostranslate preflight check

* fix: add cmn-Hans language code mapping for Google Translate

* fix: fall back to thread-based pip install on Windows when asyncio subprocess raises NotImplementedError

* feat: add DeepL/Microsoft/LLM credential fields to Settings

* feat(i18n): localize GlossaryPanel, DubSegmentTable, DubSegmentRow

* fix: Windows-safe log rotation handler avoids PermissionError on rename

* feat: persist proxy/FFmpeg/LLM/translation credentials, separate DeepL/Microsoft keys, add glossary max-height scroll and collapse

- /system/set-env writes env.* to prefs.json via prefs.set_()/delete()
- Backend startup reads env.* from prefs.json into os.environ (.setdefault)
- PERSISTENT_KEYS covers proxy, FFmpeg, LLM, DeepL/Microsoft keys
- DeepL uses DEEPL_API_KEY, Microsoft uses MICROSOFT_API_KEY (fallback TRANSLATE_API_KEY)
- DeepL/Microsoft _build_translator reads DEEPL_BASE_URL/MICROSOFT_BASE_URL
- Google/MyMemory/Microsoft translators bypass Windows registry proxy
- Frontend CREDENTIAL_GROUPS splits into 4 groups with password/text fields
- Glossary panel body max-height: 35vh + overflow-y: auto
- Glossary panel can be collapsed via ChevronDown button
- queryClient.invalidateQueries after save for immediate refresh
- SystemInfoResponse adds proxy_url, ffmpeg_ok, ffmpeg_path

* feat(i18n): localize ExportModal with zh-CN support

- Add useTranslation + replace ~50 hardcoded strings with t() calls
- Add exportModal namespace to en.json and zh-CN.json
- Cover presets, tracks, tabs, video/audio/subs/package tabs, license notice, and output footer

* fix: address PR #66 security review feedback

- Regenerate uv.lock against pypi.org (remove TUNA mirror URLs)
- Route HF_TOKEN through huggingface_hub.login() instead of prefs.json
- Add os.chmod(prefs_path, 0o600) for restricted file permissions
- Add warning logs to _WindowsSafeRotatingFileHandler bare except blocks

* docs: add Chinese translation README_CN.md

* docs: add link to Simplified Chinese translation in README.md

* docs: add English/Simplified Chinese cross-links between READMEs

* fix(l10n): restore clickable Discord/email footer links in EnterprisePage

The i18n extraction replaced main's clickable <button onClick=openExternal>
footer links with bare {t()} labels, dropping both the clickable behavior
and the literal Discord URL — which broke test_discord_link_updated
(EnterprisePage missing discord.gg/bzQavDfVV9). Restore both as clickable
links wrapping the translated label, with the hardcoded URL/mailto (URLs are
not translated). Keeps i18n, restores functional parity with main.

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

* chore(l10n): no-hardcoded-CJK rule + enforce; clean dead LLM block; harden set-env

- Add 'Localization (hard rule)' to CLAUDE.md: no hardcoded non-English UI
  text outside frontend/src/i18n/; functional CJK allowed via allowlist.
- New tests/test_no_hardcoded_cjk.py enforces it (allowlists text-processing
  regexes, model/engine vocab & IDs, error matching, demo/eval data, fixtures).
- Settings.jsx: remove dead saveLlm block (Chinese toasts + unused llm* state,
  flagged by CodeQL js/unused-local-variable); render language-picker native
  names from new LANGUAGES export in i18n/index.ts instead of hardcoding.
- main.py: drop unused 'import shutil' (CodeQL py/unused-import).
- system.py: harden FFMPEG_PATH/FFPROBE_PATH set-env (reject control chars;
  defense-in-depth for the py/path-injection finding). Endpoint stays
  loopback-only — network sharing must never expose /system/set-env.

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

* chore: drop unused ui import (Panel) + fix implicit str-concat in cjk test

Clears the two CodeQL notes introduced/attributed to this PR:
- Settings.jsx: remove unused 'Panel' from the '../ui' import (js/unused-local-variable).
- test_no_hardcoded_cjk.py: collapse multi-line message strings to single lines
  (py/implicit-string-concatenation-in-list).

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

---------

Co-authored-by: SummerSec <summersec@qq.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:48:21 +05:30
Palash DebnathandClaude Opus 4.8 87eb5ad078 feat(dub): audio-only dubbing mode (#119) (#150)
* feat(dub): audio-only dubbing mode (#119)

Add an audio→audio dubbing path: upload an audio file, get dubbed audio
out, with no video processing. The transcribe → translate → TTS core is
unchanged; only the video-coupled stages are skipped.

Backend:
- dub_core /dub/upload: new `input_type` form field ("video"|"audio").
  Audio mode validates the upload is a known audio container (else 400)
  and threads input_type into the ingest source dict.
- dub_pipeline ingest: for audio input, skip scene detection + thumbnail
  ffmpeg passes (still emits scene_done count=0 so the prep SSE contract
  the frontend waits on is unchanged); stores input_type on the job.
- dub_export /dub/download: for audio jobs, branch to an audio-only export
  (_build_audio_export_cmd) — no video input/map/codec/subtitle pass.
  Outputs dubbed_audio_{lang}_{stamp}.{wav|m4a|mp3|flac} via `out_format`
  (default m4a), optionally mixed with the separated background. Unknown
  formats fall back to AAC.

Frontend:
- dubSlice: dubInputType state + setter (default 'video').
- DubTab: auto-select audio-only mode when an audio file is dropped/picked.
- dub.ts/useDubWorkflow: pass input_type on upload.

Tests (11): _build_audio_export_cmd format/mix matrix; end-to-end audio-only
export produces an audio file (no video mux); unknown-format fallback;
upload rejects a video extension in audio mode.

Closes #119.

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

* harden(#119): allowlist-sanitize lang_code in audio export path

The track id is already constrained to an existing track key, but
allowlist-sanitize it before it reaches the output path (same pattern as
the existing safe_name) so a path component can never carry separators —
clears the CodeQL path-injection flag on the new audio-export branch.

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

* polish(#119): address Greptile P2s on audio-only dubbing

- dub_pipeline: emit scene_start before scene_done(count=0) for audio so
  the prep SSE stage sequence is symmetric with the video path.
- useDubWorkflow: 'Preparing audio…' pill for audio jobs (was always
  'Preparing video…').
- DubTab: widen the drop-accept regex + file-input accept to the full
  supported audio set (aac/opus/wma) so it matches the input-type
  detection and the backend allowlist.

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

* fix(#119): drop unused dubInputType read in DubTab (CodeQL)

Only setDubInputType is used; the value read was dead. Clears the
CodeQL unused-variable alert.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 19:07:38 +05:30
Palash DebnathandClaude Opus 4.8 8b00dc1f4f feat: onboarding demos, opt-in bug reporting, error-docs deeplinks + issue triage (#133)
* feat: onboarding demos, opt-in bug reporting, error-docs deeplinks + issue triage

Working-tree snapshot bundling several in-flight workstreams (v0.3.0):

- Onboarding/demo system: DemoPresetGrid, DictationDemo, DubbingDemo components
  + tests, render scripts (render_demos_omnivoice.py, build_demos.sh,
  build_dub_demo.sh), personalities preview URLs, alembic 0002 voice-profile
  demo fields.
- Opt-in bug reporting: ReportBugButton (prefilled GitHub-issue URL path).
- Error transparency UX: errorDocsMap deeplinks + BootstrapSplash/error wiring.
- Dub workspace: DubSegmentRow/Table, WaveformTimeline, dubSlice tweaks.
- Issue triage: .planning/issue-clusters/ (plan-01..05 root-cause masters,
  GH #128-#132).
- CLAUDE.md: hard rule — everything ships on v0.3.0, no version bumps.

KNOWN GAP (why this is a draft): the generated demo audio assets are NOT in
this tree, and backend/assets/samples/demo_voice.wav is deleted. onboarding.py
guards the missing file (skips seeding the demo profile with a warning), so no
crash — but first-run Launchpad will be empty and /demo_audio/ preview URLs
404 until assets are regenerated via scripts/build_demos.sh. Do not merge
before regenerating + committing the demo assets.

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

* feat(dub): timing strategies — kill audio compression, add Concise + Stretch Video

Replaces the current audio time-compression default (atempo squeeze to fit
slot) that produced chipmunk/alien output on high-density target languages
like Bengali. Two new user-selectable modes; legacy behaviour kept behind
an explicit "Strict slot" choice.

New `DubRequest.timing_strategy` enum (default "concise"):
  - "concise"        Translator trims text to fit at natural rate; if it
                     still overflows, hard-trim at slot with a fade so we
                     never overlap the next speaker. Surface overflow_s
                     per segment so the user can shorten the text.
  - "stretch_video"  Audio plays at natural 1.0× rate. Backend computes a
                     per-segment new timeline; persists a video_stretch_plan
                     on the job. Mux step (dub_export) builds an ffmpeg
                     trim+setpts+concat filter graph that stretches each
                     segment's video portion to match the natural-rate dub
                     audio. Gaps/pre-roll/tail pass through at 1.0×.
                     Sub burn under stretch_video is skipped in one pass
                     (cues would drift).
  - "strict_slot"    Legacy atempo squeeze. Retained for back-compat.

Director rate-bias side-effect (seg_speed *= bias) now gated on strict_slot
only, so "urgent"/"slow" direction tokens keep their instruct effect in
the new modes without chipmunking.

Per-segment fit_status emitted in the SSE done event:
  {status: "fits" | "overflows" | "video_stretched", overflow_s?, stretch_ratio?}
DubSegmentRow's "Sync: 100%" badge (which was lying — sync_ratio was always
~1.0 because the TTS loop pre-trimmed to slot) is replaced with a truthful
"Fits / Overflows +Ns / Video 1.18×" label.

Frontend:
  - prefsSlice.timingStrategy (persisted, store v3→v4 with safe migrate).
  - DubTab footer Segmented control: "Concise · Stretch Video · Strict slot".
  - useDubWorkflow passes timing_strategy on /dub/generate; consumes fit_status.

Tests: tests/test_dub_timing_strategy.py — 13 cases covering schema
defaults/validation, _build_video_stretch_filter_graph (pre-roll, gap,
tail, empty-plan early return, post-subtitle chain-in), and
_video_stretch_plan_for guards. 30/30 existing dub tests still pass.

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

* fix(waveform): surface missing source as "Source media missing" instead of code-4 black box

When a project's underlying media file is gone (moved or deleted between
save and reload) the <video> element fires MediaError code 4 and the
companion audio fetch returns HTTP 404 — both were silently warned to
the console while the user stared at an unresponsive black panel and an
empty waveform.

- WaveformTimeline now flips loadError when the video element rejects
  code 3 (decode) or 4 (src not supported), and tracks `sourceMissing`
  separately so the error UI can name the actual problem.
- The audio decode fallback chain catches HTTP 404 specifically and
  treats it as source-missing instead of loading silent empty peaks —
  an empty waveform on a deleted source is more confusing than a clear
  "Re-upload the video to continue" message.

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

* fix(tray): "Show OmniVoice" reloads when the webview is blank

When the dev Vite server restarts (or the main window is created before
the backend is ready), the webview load fails and the window is left
with `<body></body>` plus a "Could not connect to the server" console
error. Clicking "Show OmniVoice" from the tray menu just re-showed the
broken window — there was no recovery path short of quit+relaunch.

Now the show handler runs a tiny eval after `show()`/`set_focus()` that
calls `location.reload()` only when `document.body.childElementCount === 0`.
A healthy window doesn't blink (body is non-empty); a blank one
self-recovers as soon as the user clicks Show.

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

* fix(#133): bug-report diagnostics field mapping + drop unused imports

Address PR #133 review:
- ReportBugButton: /system/info exposes `platform` + `device`, not
  `os`/`torch_device`/`gpu` — those reads silently dropped OS/GPU from every
  bug report. Map to the real fields (CodeRabbit). Also remove the dead
  `home` local in stripHome (CodeQL unused-variable).
- DictationDemo: drop unused `Loader` import (CodeQL unused-import).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 17:24:25 +05:30
Palash DebnathandClaude Opus 4.8 b64f53b0af feat: pipeline error transparency — no more silent "unknown error" (plan-04, closes #131) (#136)
* docs(plan-04): spec + plan for pipeline error transparency (#131)

speckit spec/plan/research/data-model/contract/quickstart for plan-04.
Grounds the fix in the real code map: shared failure-event builder
(backend/core/failure.py) feeding tasks.py + dub_pipeline.py + dub_core.py,
non-empty reason guarantee, sanitized diagnostic block, frontend renderer
with docs deeplink. Closes-target: #131 (children #122, #63).

Design only — no code changes yet.

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

* feat(pipeline): structured, non-empty failure events + logged tracebacks (#131)

plan-04 backend: no more silent "unknown error". A shared failure helper
guarantees a non-empty reason at every emit site and a sanitized,
copyable diagnostic block.

- backend/core/failure.py: build_failure()/build_failure_event() (reason
  falls back to the exception class name), sanitize() (reuses the
  logging_filter HF-token regex + redacts *TOKEN*/*KEY*/*SECRET* env values
  + home→~), diagnostic() (reuses the env capture), classify() reusing the
  error_docs_map 5-class taxonomy for the docs deeplink + hint.
- core/tasks.py worker: structured event instead of bare str(e); keeps the
  logged traceback.
- services/dub_pipeline.py: enrich download/extract error yields; ADD the
  missing outer `except Exception` (the #122 path — unhandled ingest errors
  were never surfaced with stage context); surface the previously-silent
  demucs/scene/thumbnail degradations as non-fatal `warning` events.
- api/routers/batch.py: guaranteed non-empty batch failure reason.

SSE payload is additive (legacy `error`/`stage`/`detail` keys preserved),
so existing frontends keep working and already show the specific reason.

Tests (TDD, fail-before/pass-after): 14 cases — non-empty-reason guarantee,
redaction, diagnostic sanitization, and the 3 Test-matrix triggers
(worker / extract / url). 483 passed, 0 regressions.

Closes #131. Refs #122, #63.

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

* feat(dub-ui): show specific cause + docs deeplink + copyable diagnostic (#131)

plan-04 frontend. The backend now sends a structured, non-empty failure;
surface it to the user instead of "extract: unknown error".

- dubSlice: DubFailure type + dubFailure state/setter.
- useDubWorkflow: capture the structured failure on the SSE error event
  (reason/error_class/stage/hint/docs_topic/diagnostic); clear on new runs.
- DubTab: DubFailureNotice renders the actionable hint, an "Open docs"
  deeplink (via the existing errorDocsMap classifier), and a "Copy
  diagnostic" button — shown beneath the error badge in both failure banners.

typecheck + build clean; 66 frontend tests pass.

Refs #131, #122, #63.

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

* fix(failure): annotate intentional best-effort excepts (CodeQL)

The new security workflow's CodeQL flagged 5 bare `except: pass` blocks.
All are deliberate best-effort guards (sanitize/diagnostic must never throw
on the failure path; the test cancels the worker to tear it down). Added
explanatory comments per CodeQL's py/empty-except rule. No behavior change.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:12:23 +05:30
Palash DebnathandClaude Opus 4.7 a1ef66c321 Stability pass: DB leaks, App.jsx hooks refactor, desktop bootstrap (#49)
* fix: eliminate DB connection leaks, race conditions, and deprecated asyncio API

## DB Connection Leaks (P0)
- Convert 38 raw get_db() calls to db_conn() context manager across 14 router files
- Connections are now guaranteed to close even when exceptions are raised
- profiles.py create_profile: clean up orphaned audio file if DB insert fails
- profiles.py lock_profile: consolidate 3 separate conn.close() error paths

## Race Condition (P1)
- Add _dub_jobs_lock (threading.Lock) to protect _dub_jobs dict in dub_pipeline.py
- get_job/put_job now thread-safe for concurrent dub sessions

## asyncio Deprecation (P2)
- Replace 23 asyncio.get_event_loop() calls with asyncio.get_running_loop()
- Prevents DeprecationWarning on Python 3.12+ and future breakage on 3.14

## Quick Fixes
- gallery.py preview_voice: remove filesystem path from error response (P2)
- dub_pipeline.py parse_vtt_segments: remove redundant `import re` inside loop (P3)
- gallery.py _init_gallery_db: use db_conn() context manager (P2)

* refactor: extract hooks, centralize isTauri, add pytest-cov

## Frontend
- Extract useTTS hook (150 LOC) — TTS generation, streaming, audio ingestion
- Extract useProfiles hook (219 LOC) — voice profile CRUD, lock/unlock, preview
- Centralize isTauri detection: dialog.js, VoiceGallery.jsx, Settings.jsx
  now import from utils/media.js instead of 4 different detection patterns

## Backend
- Add pytest-cov to dev dependencies
- Baseline coverage: 39% across backend/ (214 tests pass)
- Add .coverage to .gitignore

* feat: add Vitest + checkJs, extract useDubWorkflow + useAppData hooks

## Frontend Testing (new)
- Set up Vitest with jsdom environment + @testing-library/react
- 11 tests: utils (isTauri, formatTime, constants) + Zustand store (mode, text, dubStep, pill)
- Scripts: 'test' (vitest run), 'test:watch' (vitest), 'test:legacy' (node runner)

## App.jsx Decomposition (continued)
- Extract useDubWorkflow hook (387 LOC) — upload, ingest, transcribe SSE,
  translate, generate SSE, abort, stop, cleanup
- Extract useAppData hook (181 LOC) — data loading, localStorage persistence,
  WebSocket real-time updates, model-status pill management

## TypeScript checkJs
- Enable checkJs: true in tsconfig.json for IDE-level type checking
- 947 existing errors (informational, not blocking builds)
- noImplicitAny remains false to avoid blocking

* ci: add Vitest step, fix useProfiles duplicate state

## CI
- Add 'Run Vitest (frontend)' step — runs 11 unit tests
- Override --checkJs false in CI typecheck to avoid 947 pre-existing errors
- Rename legacy test step for clarity

## Hooks
- Fix useProfiles to accept loadProfiles from parent (useAppData)
  instead of managing its own duplicate profiles array

* refactor: wire hooks into App.jsx — 2067 → 1129 LOC (-45%)

App.jsx now delegates to extracted hooks instead of inline logic:
- useAppData: data loading, localStorage, WebSocket, model pill
- useProfiles: voice profile CRUD, lock/unlock, preview
- useTTS: generation, streaming, audio ingestion
- useDubWorkflow: upload, transcribe SSE, translate, generate SSE

988 lines removed. All handler logic lives in focused,
independently testable hooks. Store selectors and render
JSX stay in App.jsx as the shell.

Verified: vite build clean, 11 frontend + 214 backend tests pass.

* feat: show real-time percentage on model loading pill

Backend: register hf_progress listener during _load_model_sync()
so download/weight-loading tqdm events update _loading_detail with
a progress percentage (0-99%). get_model_status() now includes a
'progress' field that the frontend polls.

Frontend: useAppData reads msQuery.data.progress and calls
setPillProgress() — the FloatingPill already renders the percentage
text and progress bar width from this value.

* fix: prevent FileNotFoundError in desktop bundle during model init

transformers >=4.52 calls _can_set_experts_implementation() and
_can_set_attn_implementation() during PreTrainedModel.__init__,
which open the class source file via open(class_file). In a Tauri
desktop bundle, module.__file__ points to a path that doesn't
exist on disk, causing:

  FileNotFoundError: .../omnivoice/models/omnivoice.py

Override both classmethods on OmniVoice to return static values
without filesystem access. OmniVoice doesn't use MoE experts
(return False), but does support flex/flash attn (return True).

* fix: sync source dirs on every bootstrap, not just first run

The Tauri bootstrap previously only copied omnivoice/ and backend/
to Application Support on the first run. Subsequent app updates
kept using stale source files, preventing bug fixes from landing.

Now ensure_venv_ready() always syncs both directories from the
bundle resources before returning, even when the venv is healthy.
This fixes the FileNotFoundError crash where the old omnivoice.py
lacked the _can_set_experts_implementation override.

* ui: premium setup wizard polish

- Primary button: solid gradient fill with hover glow + lift + press
- Stepper nav: connected pills with glow ring on active step
- Welcome cards: glassmorphism with stagger-in animations, lucide icons,
  left-border accent strip, hover translate
- Preflight panel: colored icon pill backgrounds, stagger-slide entrance
- Step transitions: fade+slide animation via keyed wrapper
- Footnote: shortened paths (~/ notation), Reveal in Finder button
- Recommendation banner: gradient background with accent glow
- Compact spacing throughout for denser, professional layout

* fix: kill zombie backend on clean+retry bootstrap

When clean_and_retry_bootstrap removes the project dir, any old
uvicorn process still running from the deleted paths remains alive
on port 3900. The subsequent retry_bootstrap sees the port is
healthy and attaches to the zombie instead of re-bootstrapping.

Now explicitly kill any process on the backend port after cleaning,
before calling retry_bootstrap.

* feat: integrate speaker clones into dubbing interface, sanitize system environment variables for subprocesses, and improve FFMPEG binary path resolution.

* fix: restore docker compose default + drop dead setSeed call

- deploy/docker-compose.yml: remove profiles: ["cpu"] from the default
  service so `docker compose up` matches the comment on line 5. With the
  profile present, no service auto-started.

- frontend/src/App.jsx: drop the setSeed call in restoreHistory. The
  selector was never reintroduced after the App.jsx hooks split, and
  there is no seed state in the store — seeds are generated fresh per
  call in useTTS and only read from history items for display.

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

* fix: address CodeRabbit review — async detection, dub stream, bootstrap fail-fast

- backend/services/tts_backend.py: invert async-context detection in
  _ensure_loaded. The previous code unconditionally caught its own
  diagnostic RuntimeError and then called asyncio.run() inside a
  running loop, masking the intended error message.

- frontend/src/hooks/useDubWorkflow.js: require a terminal `done` event
  before reporting dub success. Without this, a dropped stream after
  partial progress would flip the UI to `done`, refresh history, and
  play the completion ping as if generation finished.

- frontend/src/hooks/useDubWorkflow.js: restore the previous step when
  tasksCancel() fails. The UI was getting stuck in `stopping` forever
  on cancel errors.

- frontend/src-tauri/src/bootstrap.rs: fail-fast when source sync fails
  after the existing directory has already been removed. The previous
  warn-and-continue path could leave the install with no backend/ or
  omnivoice/ sources and defer the failure to backend startup with a
  cryptic error.

- backend/api/routers/generation.py: add `from e` to the ValueError →
  HTTPException re-raise (Ruff B904).

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

* fix: preserve % suffix in TTS generation timer

The 100ms timer in useTTS was rewriting generationTime to a plain
elapsed-seconds string, which immediately wiped the "(xx%)" download
suffix written on the next iteration of the response-body loop. The
real-time percentage was flickering on/off as a result.

Read the previous value inside the setter and reattach any existing
percent suffix.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:49:04 +05:30
Palash Debnath 34610ca091 feat: real-time WebSocket event bus + sidebar reactivity fixes (#27)
## Core Infrastructure
- Add backend event bus (core/event_bus.py) — in-memory pub/sub with
  emit(), subscribe(), unsubscribe()
- Add WebSocket endpoint /ws/events (api/routers/events.py) with 25s
  keepalive pings and auto-cleanup on disconnect
- Add frontend hook useRealtimeEvents.js — single WS connection with
  exponential backoff reconnect (2s→60s)

## Backend Event Integration
- projects.py: emit on create/update/delete
- profiles.py: emit on create/update/lock/unlock/delete
- dub_core.py: emit on clear/delete history
- dub_pipeline.py: emit on save_job (every pipeline write)
- exports.py: emit on export/record
- generation.py: emit on generate/clear/delete
- gallery.py: emit on save-as-profile/to-profile

## Frontend Improvements
- Replace 45s polling interval with instant WS-based invalidation
- Fix critical bug: apiModelStatus was undefined, causing loadAll()
  to loop forever — sidebar data never loaded on startup
- Add websockets to main deps (was optional, got removed by uv sync)
- Reduce model/status polling from 5s to 10s, disable background
  polling for logs
- Add ReadinessChecklist and FloatingPill components
- Default UI scale changed from S (1.0) to M (1.3)

## Dependencies
- Add websockets>=16.0 to main dependencies for uvicorn WS support

Closes #3 (native desktop app exists via Tauri)
Closes #5 (Dockerfile already uses root bun.lock)
Resolves #26 (Triton workaround documented)
2026-04-27 22:16:28 +05:30
debpalashandClaude Opus 4.7 994c6cf065 feat(backend): setup wizard router, translation engines, export options, client-disconnect handling
- Add setup router (backend/api/routers/setup.py) for first-run wizard:
  system checks, engine probes, model downloads with progress
- Add translation engines service with pluggable backends
- Add utils/hf_progress for HuggingFace download progress streaming
- Add PyInstaller runtime hooks (numpy compat, torch compiler disable)
- Global exception handler short-circuits h11 LocalProtocolError and
  Starlette ClientDisconnect with HTTP 499 to silence noisy stack traces
  when users scrub or cancel video mid-stream
- /dub/download-mp3 accepts bitrate query param (clamped 64–320kbps)
- Refactor ASR/TTS backends, dub pipeline, engine management
- Update backend.spec for PyInstaller packaging
- Bump pyproject version to 0.2.0; refresh uv.lock

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:49:04 +05:30
debpalash 52d68d05dc refactor: update backend architecture, expand frontend state management, and synchronize voice-pro research modules. 2026-04-21 18:32:25 +05:30