* 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>