Skip to content

feat(dub): audio-only dubbing mode (#119) - #150

Merged
debpalash merged 4 commits into
mainfrom
feat/audio-only-dubbing
May 29, 2026
Merged

feat(dub): audio-only dubbing mode (#119)#150
debpalash merged 4 commits into
mainfrom
feat/audio-only-dubbing

Conversation

@debpalash

@debpalash debpalash commented May 29, 2026

Copy link
Copy Markdown
Owner

Closes #119.

What

Adds an audio→audio dubbing path: upload an audio file, pick source/target language, get dubbed audio out — no video processing, no mandatory video output. The transcribe → translate → TTS core is identical; only the video-coupled stages are skipped.

How

Backend

  • dub_core /dub/upload: new input_type form field ("video" default | "audio"). Audio mode validates the upload is a known audio container (.wav/.mp3/.m4a/.aac/.flac/.ogg/.opus/.wma, else 400) and threads input_type into the ingest source dict.
  • dub_pipeline ingest: for audio input, skip the scene-detect + thumbnail ffmpeg passes (still emits scene_done count=0 so the prep SSE contract the frontend waits on is unchanged). input_type is persisted on the job. Audio extraction (-vn) and Demucs are already audio-generic — reused as-is.
  • dub_export /dub/download: for audio jobs, branch to a focused audio-only export (_build_audio_export_cmd) — no video input/stream-map/codec/subtitle/stretch. Outputs dubbed_audio_{lang}_{stamp}.{wav|m4a|mp3|flac} via a new out_format query (default m4a), optionally mixed with the separated background (no_vocals). Unknown formats fall back to AAC.

Frontend

  • dubSlice: dubInputType state + setter (default 'video').
  • DubTab: auto-selects audio-only mode when an audio file is dropped/picked (the drop zone already accepted audio).
  • dub.ts / useDubWorkflow: pass input_type on upload.

Constraints

  • Cross-platform default parity: pure ffmpeg/Python branch, identical on mac/Win/Linux. ✅
  • Local-first: no network. ✅
  • Backward-compatible: input_type defaults to video; existing video jobs and on-disk job state are untouched (absent field → video). ✅
  • No version bump.

Tests (11)

  • test_dub_audio_export.py: _build_audio_export_cmd format/mix matrix (wav→pcm, m4a→aac, mp3→lame, bg→amix, unknown→aac).
  • test_dub_export_unique.py::TestAudioOnlyDubbing: end-to-end audio-only export produces an audio file (asserts no dubbed_video_*.mp4 is created), unknown-format→m4a fallback, and upload rejects a video extension in audio mode.

All green (80 dub-suite tests pass, 0 failures).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Audio-only dubbing: upload and process standalone audio files
    • Choose audio export format (wav, m4a, mp3, flac)
    • Optionally preserve/mix separated background with dubbed audio
  • Improvements

    • UI shows "Preparing audio…" vs "Preparing video…" based on file detection
    • Faster audio-only processing by skipping video-specific steps (no scenes/thumbnails)
    • Upload validation rejects mismatched audio/video file types
  • Tests

    • Added coverage for audio-only export, mixing, format selection, and upload validation

Review Change Stack

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>
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8864ec3e-69ed-4432-80f3-d4a6a7d630ea

📥 Commits

Reviewing files that changed from the base of the PR and between a07cf71 and 1daa1a2.

📒 Files selected for processing (1)
  • frontend/src/pages/DubTab.jsx
💤 Files with no reviewable changes (1)
  • frontend/src/pages/DubTab.jsx

📝 Walkthrough

Walkthrough

Adds audio-only dubbing: frontend tracks input modality and sends input_type on upload; backend validates audio uploads, persists input_type in ingest, skips scene/thumbnail for audio, and provides an audio-only export path that builds/runs ffmpeg with selectable output formats and optional background mixing. Tests added for command builder and end-to-end audio flows.

Changes

Audio-Only Dubbing Feature

Layer / File(s) Summary
Frontend state management for input type
frontend/src/store/dubSlice.ts
Zustand store extended with dubInputType state field and setDubInputType setter to track whether the current upload is audio or video.
Frontend upload API and workflow hook
frontend/src/api/dub.ts, frontend/src/hooks/useDubWorkflow.js
dubUpload accepts optional inputType and appends input_type to FormData; useDubWorkflow reads dubInputType from store, sets the preparing pill text, and forwards inputType to dubUpload.
Backend upload endpoint validation and routing
backend/api/routers/dub_core.py, tests/test_dub_export_unique.py
dub_upload validates input_type ("video"
Backend ingest pipeline audio handling
backend/services/dub_pipeline.py
ingest_pipeline derives input_type from source, persists it into job state, and skips scene detection/thumbnail extraction for audio jobs (emits scene_done count=0 and clears thumb_path).
Backend audio-only export with ffmpeg
backend/api/routers/dub_export.py, tests/test_dub_audio_export.py, tests/test_dub_export_unique.py
Export endpoint gains out_format parameter and _build_audio_export_cmd helper; audio-only branch selects a dubbed audio track, optionally mixes background via amix, runs ffmpeg with format→codec mapping, validates output file, and returns file or native-save JSON. Unit and integration tests cover codec selection, mixing, and end-to-end behavior.
Frontend file detection and UI wiring
frontend/src/pages/DubTab.jsx
Drag-and-drop and hidden file picker acceptance expanded to additional audio extensions and set dubInputType to audio for matching files.

Sequence Diagram

sequenceDiagram
  participant User as User / Browser
  participant DubTab as DubTab (UI)
  participant Store as AppStore
  participant dubUpload as frontend dubUpload
  participant BackendUpload as /dub/upload
  participant Pipeline as IngestPipeline
  participant Export as /dub/download

  User->>DubTab: Select/drag audio file
  DubTab->>Store: setDubInputType("audio")
  DubTab->>dubUpload: Call upload (includes input_type="audio")
  dubUpload->>BackendUpload: POST /dub/upload with input_type
  BackendUpload->>BackendUpload: validate input_type and file extension
  BackendUpload->>Pipeline: Enqueue prep task with source.input_type="audio"
  Pipeline->>Pipeline: derive/persist input_type
  Pipeline->>Pipeline: skip scene/thumb for audio
  User->>Export: Request /dub/download?out_format=m4a
  Export->>Export: detect job.input_type == "audio"
  Export->>Export: build ffmpeg command (codec mapping, optional amix)
  Export->>Export: run ffmpeg and validate output
  Export->>User: return audio file (m4a/mp3/wav/flac)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(dub): audio-only dubbing mode (#119)' clearly and concisely summarizes the main feature addition—audio-only dubbing—matching the changeset's primary objective.
Description check ✅ Passed The description includes a clear summary, detailed backend/frontend changes, testing info, constraints, and completeness against the template sections, though some checklist items are unchecked.
Linked Issues check ✅ Passed The changeset fully implements the #119 requirements: audio-only input/output, video-skipping for audio jobs, core transcribe→translate→TTS reuse, language selection, and optional transcription editing support.
Out of Scope Changes check ✅ Passed All file changes are scoped to implementing audio-only dubbing: backend upload/pipeline/export routing, frontend state and UI for audio detection, tests for new audio export logic, and no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/audio-only-dubbing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread backend/api/routers/dub_export.py Dismissed
Comment thread backend/api/routers/dub_export.py Dismissed
Comment thread backend/api/routers/dub_export.py Dismissed
Comment thread backend/api/routers/dub_export.py Dismissed
Comment thread backend/api/routers/dub_export.py Dismissed
Comment thread frontend/src/pages/DubTab.jsx Fixed
@greptile-apps

greptile-apps Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds an audio-only dubbing mode: users can upload an audio file and receive dubbed audio out, skipping all video-coupled pipeline stages (scene detection, thumbnailing, video mux). The transcribe → translate → TTS core is reused unchanged.

  • Backend: dub_upload accepts a new input_type form field; audio mode validates the file extension against _AUDIO_EXTS and threads input_type through ingest. The pipeline skips scene/thumbnail passes but emits the scene_start/scene_done SSE pair to keep the frontend contract intact. dub_download branches to a new _build_audio_export_cmd that outputs directly to an audio container (wav/m4a/mp3/flac) with optional background mixing via amix.
  • Frontend: dubSlice adds dubInputType state (default 'video'); DubTab auto-sets it when an audio file is picked; useDubWorkflow reads it to pass inputType on upload and show the correct loading pill.
  • Tests: 11 new tests cover the export command matrix, end-to-end audio-only download, unknown-format fallback, and upload rejection for mismatched extensions.

Confidence Score: 5/5

Safe to merge; the audio-only path is well-isolated and backward-compatible with existing video jobs.

The new code paths are cleanly branched on input_type, the SSE contract is preserved (scene_start/done pair emitted even when skipped), path-traversal vectors in lang_code and out_format are properly sanitized, and resetDubState correctly resets dubInputType. The one minor inconsistency in UI text does not affect correctness.

No files require special attention; useDubWorkflow.js has the minor UI text inconsistency noted above.

Important Files Changed

Filename Overview
backend/api/routers/dub_core.py Adds input_type form field with validation: unknown values → 400, audio mode + non-audio extension → 400. Extension check uses an allowset (_AUDIO_EXTS). Looks correct.
backend/api/routers/dub_export.py New _build_audio_export_cmd and audio-only branch in dub_download. Path sanitization for lang_code and out_format is correct; unknown format falls back to m4a. amix weights use space-separated floats which ffmpeg parses correctly within the filtergraph string.
backend/services/dub_pipeline.py Audio jobs skip scene-detect and thumbnail ffmpeg passes; now correctly emits scene_start+scene_done(count=0) pair to keep SSE stage sequence consistent. input_type is persisted in both the full and partial job dicts.
frontend/src/hooks/useDubWorkflow.js Reads dubInputType from store and conditionally shows "Preparing audio…" vs "Preparing video…". The follow-up "Extracting audio & scenes…" pill is not updated for audio jobs.
frontend/src/pages/DubTab.jsx Both drag-drop and file-picker paths now call setDubInputType based on file type/extension. The accept attribute now includes all backend-supported audio extensions.
frontend/src/store/dubSlice.ts Adds dubInputType state (default 'video') and setDubInputType setter. Included in INITIAL so resetDubState correctly resets it.
frontend/src/api/dub.ts Adds inputType option to dubUpload, appended to the FormData as input_type. Typed as `'video'
tests/test_dub_audio_export.py Unit tests for _build_audio_export_cmd covering all four formats, background mixing, and unknown-format fallback. All assertions are clear and meaningful.
tests/test_dub_export_unique.py End-to-end audio-only export test asserts audio file produced and no video file created; upload rejection test for mismatched extension. Good coverage of the new path.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant FE as DubTab / useDubWorkflow
    participant API as /dub/upload
    participant PP as dub_pipeline (ingest)
    participant EX as /dub/download

    U->>FE: Drop / pick audio file
    FE->>FE: setDubInputType('audio')
    FE->>API: "POST /dub/upload (input_type=audio)"
    API->>API: Validate ext in _AUDIO_EXTS
    API->>PP: "ingest_pipeline(source={kind:file, input_type:audio})"
    PP->>PP: extract audio (-vn)
    PP->>PP: Demucs separation
    PP-->>FE: SSE demucs_start / demucs_done
    PP->>PP: Skip scene detect and thumbnail
    PP-->>FE: "SSE scene_start / scene_done(count=0)"
    PP-->>FE: SSE ready
    Note over FE,EX: transcribe → translate → TTS (unchanged)
    FE->>EX: "GET /dub/download/{job_id}?out_format=m4a"
    EX->>EX: "job.input_type == 'audio' → audio branch"
    EX->>EX: _build_audio_export_cmd(track, [bg], out, fmt)
    EX-->>U: "FileResponse dubbed_audio_es_*.m4a"
Loading

Fix All in Claude Code

Reviews (2): Last reviewed commit: "fix(#119): drop unused dubInputType read..." | Re-trigger Greptile

Comment thread frontend/src/pages/DubTab.jsx Outdated
Comment thread frontend/src/hooks/useDubWorkflow.js Outdated
Comment thread backend/services/dub_pipeline.py
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/services/dub_pipeline.py (1)

748-790: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't reuse cached scene/thumbnail artifacts for audio-only jobs.

This branch still copies scene_cuts and thumb_path from a prior cached job even when input_type == "audio". If the same soundtrack was previously ingested from a video, the new audio-only job will report cached scene metadata and segment against video cuts, which breaks the audio-only contract.

Suggested fix
-            if cached["thumb_path"] and os.path.isfile(cached["thumb_path"]):
+            if input_type != "audio" and cached["thumb_path"] and os.path.isfile(cached["thumb_path"]):
                 shutil.copy2(cached["thumb_path"], thumb_path)
             else:
                 thumb_path = None

-            scene_cuts = cached["scene_cuts"] or []
+            scene_cuts = [] if input_type == "audio" else (cached["scene_cuts"] or [])
@@
             yield prep_event("cached",
                              has_bg=bool(no_vocals_path and os.path.exists(no_vocals_path)),
                              scene_count=len(scene_cuts))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/dub_pipeline.py` around lines 748 - 790, The cached-branch
is reusing video-specific artifacts for audio-only jobs; update the cached
handling so when input_type == "audio" you do NOT copy or reuse
cached["thumb_path"] or cached["scene_cuts"] (and ensure thumb_path ends up None
and scene_cuts is an empty list for audio jobs). Concretely, in the cached block
around the existing vocals/no_vocals/thumb logic, guard the thumbnail copy and
the assignment scene_cuts = cached["scene_cuts"] with a check input_type !=
"audio" (or explicitly set thumb_path = None and scene_cuts = [] when input_type
== "audio"); keep the vocals/no_vocals logic unchanged, and ensure the full_job
dict uses the adjusted thumb_path and scene_cuts before calling put_job/save_job
and yielding events.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/pages/DubTab.jsx`:
- Around line 416-417: The drop/picker gate that filters selected files is
out-of-sync with the regex used in setDubInputType (which now accepts .mp3 .wav
.flac .m4a .aac .ogg .opus .wma), so audio files with empty/generic file.type
get misclassified; update the file extension whitelist used in the drop/picker
logic to match the same regex/extension list used in the setDubInputType call
(and any other checks around the file input gate referenced near the same area
and at the other occurrence around lines 474-475), ensuring both file.type
checks and the fallback extension test use the identical pattern for audio
extensions (.mp3, .wav, .flac, .m4a, .aac, .ogg, .opus, .wma).

In `@tests/test_dub_audio_export.py`:
- Line 22: The current assertion uses OR and may pass incorrectly; update the
assertion that references the string variable s to ensure video mapping isn't
present by either asserting "-map" not in s (strict audio-only) or asserting the
specific combination "-map 0:v" not in s (allowing other -map entries like
audio), e.g., replace the line containing "assert \"-map\" not in s or \"0:v\"
not in s" with one of those stronger checks so the test fails if a video mapping
is actually present.
- Around line 17-52: Tests in test_dub_audio_export.py use hard-coded Unix-style
paths which break on Windows; update the test fixtures to construct
platform-agnostic paths (e.g., via pathlib.Path or os.path.join) when calling
_build_audio_export_cmd in test_wav_uses_pcm16le, test_m4a_uses_aac,
test_mp3_uses_lame, test_background_mix_adds_amix_and_second_input, and
test_unknown_format_falls_back_to_aac_m4a so the input/output paths are created
with Path("j") / "dubbed_de.wav" (or equivalent) rather than "/j/dubbed_de.wav",
ensuring the string passed into _build_audio_export_cmd is platform-correct
across macOS/Windows/Linux.

---

Outside diff comments:
In `@backend/services/dub_pipeline.py`:
- Around line 748-790: The cached-branch is reusing video-specific artifacts for
audio-only jobs; update the cached handling so when input_type == "audio" you do
NOT copy or reuse cached["thumb_path"] or cached["scene_cuts"] (and ensure
thumb_path ends up None and scene_cuts is an empty list for audio jobs).
Concretely, in the cached block around the existing vocals/no_vocals/thumb
logic, guard the thumbnail copy and the assignment scene_cuts =
cached["scene_cuts"] with a check input_type != "audio" (or explicitly set
thumb_path = None and scene_cuts = [] when input_type == "audio"); keep the
vocals/no_vocals logic unchanged, and ensure the full_job dict uses the adjusted
thumb_path and scene_cuts before calling put_job/save_job and yielding events.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c0825f90-39e3-48cd-b2f2-73c9543432b7

📥 Commits

Reviewing files that changed from the base of the PR and between 8b00dc1 and 654dd87.

📒 Files selected for processing (9)
  • backend/api/routers/dub_core.py
  • backend/api/routers/dub_export.py
  • backend/services/dub_pipeline.py
  • frontend/src/api/dub.ts
  • frontend/src/hooks/useDubWorkflow.js
  • frontend/src/pages/DubTab.jsx
  • frontend/src/store/dubSlice.ts
  • tests/test_dub_audio_export.py
  • tests/test_dub_export_unique.py

Comment thread frontend/src/pages/DubTab.jsx
Comment on lines +17 to +52
cmd = _build_audio_export_cmd("ffmpeg", "/j/dubbed_de.wav", None, "/j/out.wav", "wav")
s = _flat(cmd)
# the dubbed track is the only input; never the source media / a video map
assert cmd.count("-i") == 1
assert "/j/dubbed_de.wav" in s
assert "-map" not in s or "0:v" not in s # no video stream mapping
assert "-c:v" not in s # no video codec
assert "pcm_s16le" in s # wav → PCM
assert cmd[-1] == "/j/out.wav"


def test_m4a_uses_aac():
cmd = _build_audio_export_cmd("ffmpeg", "/j/dubbed_de.wav", None, "/j/out.m4a", "m4a")
s = _flat(cmd)
assert "aac" in s
assert "-c:v" not in s


def test_mp3_uses_lame():
cmd = _build_audio_export_cmd("ffmpeg", "/j/dubbed_de.wav", None, "/j/out.mp3", "mp3")
assert "libmp3lame" in _flat(cmd)


def test_background_mix_adds_amix_and_second_input():
cmd = _build_audio_export_cmd("ffmpeg", "/j/dubbed_de.wav", "/j/no_vocals.wav", "/j/out.m4a", "m4a")
s = _flat(cmd)
assert cmd.count("-i") == 2 # track + background
assert "/j/no_vocals.wav" in s
assert "amix" in s # mixed, not just concatenated
assert "-filter_complex" in s


def test_unknown_format_falls_back_to_aac_m4a():
# Defensive: an unexpected format string must not produce a broken command.
cmd = _build_audio_export_cmd("ffmpeg", "/j/dubbed_de.wav", None, "/j/out.bin", "weird")
assert "aac" in _flat(cmd)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Ensure cross-platform path compatibility in test fixtures.

All test functions use Unix-style forward-slash paths (/j/dubbed_de.wav), which may cause failures on Windows where paths typically use backslashes or drive letters. Since the coding guidelines require features to behave identically on macOS, Windows, and Linux, the test paths should use pathlib.Path or os.path.join to ensure cross-platform compatibility.

🔧 Proposed fix using pathlib
+from pathlib import Path
+
 def _flat(cmd):
     return " ".join(cmd)
 
 
 def test_wav_track_only_no_video():
-    cmd = _build_audio_export_cmd("ffmpeg", "/j/dubbed_de.wav", None, "/j/out.wav", "wav")
+    cmd = _build_audio_export_cmd("ffmpeg", str(Path("/j/dubbed_de.wav")), None, str(Path("/j/out.wav")), "wav")
     s = _flat(cmd)
     # the dubbed track is the only input; never the source media / a video map
     assert cmd.count("-i") == 1
-    assert "/j/dubbed_de.wav" in s
+    assert str(Path("/j/dubbed_de.wav")) in s

Alternatively, use platform-agnostic test fixtures:

+from pathlib import Path
+
+# Platform-agnostic test paths
+TEST_DUBBED = str(Path("j") / "dubbed_de.wav")
+TEST_OUT_WAV = str(Path("j") / "out.wav")
+TEST_OUT_M4A = str(Path("j") / "out.m4a")
+TEST_OUT_MP3 = str(Path("j") / "out.mp3")
+TEST_BG = str(Path("j") / "no_vocals.wav")
+TEST_OUT_BIN = str(Path("j") / "out.bin")
+
 def test_wav_track_only_no_video():
-    cmd = _build_audio_export_cmd("ffmpeg", "/j/dubbed_de.wav", None, "/j/out.wav", "wav")
+    cmd = _build_audio_export_cmd("ffmpeg", TEST_DUBBED, None, TEST_OUT_WAV, "wav")
     s = _flat(cmd)
     assert cmd.count("-i") == 1
-    assert "/j/dubbed_de.wav" in s
+    assert TEST_DUBBED in s

As per coding guidelines: "Default features must behave identically on macOS, Windows and Linux. Platform-specific implementation is allowed, but a divergent user-visible default is a P0 bug — flag it and suggest an opt-in (Settings/env/flag)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_dub_audio_export.py` around lines 17 - 52, Tests in
test_dub_audio_export.py use hard-coded Unix-style paths which break on Windows;
update the test fixtures to construct platform-agnostic paths (e.g., via
pathlib.Path or os.path.join) when calling _build_audio_export_cmd in
test_wav_uses_pcm16le, test_m4a_uses_aac, test_mp3_uses_lame,
test_background_mix_adds_amix_and_second_input, and
test_unknown_format_falls_back_to_aac_m4a so the input/output paths are created
with Path("j") / "dubbed_de.wav" (or equivalent) rather than "/j/dubbed_de.wav",
ensuring the string passed into _build_audio_export_cmd is platform-correct
across macOS/Windows/Linux.

# the dubbed track is the only input; never the source media / a video map
assert cmd.count("-i") == 1
assert "/j/dubbed_de.wav" in s
assert "-map" not in s or "0:v" not in s # no video stream mapping

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Strengthen the video mapping assertion.

The assertion assert "-map" not in s or "0:v" not in s uses OR logic that passes if either condition is true. This could allow a command containing -map 0:a (audio mapping, valid) plus an unrelated 0:v string elsewhere to pass incorrectly. For audio-only commands, either verify that -map is completely absent or that the specific pattern -map 0:v doesn't appear together.

♻️ Proposed assertion improvements

Option 1: Assert no -map at all (strictest for audio-only):

-    assert "-map" not in s or "0:v" not in s  # no video stream mapping
+    assert "-map" not in s  # audio-only commands should not map streams

Option 2: Check that video mapping specifically doesn't appear:

-    assert "-map" not in s or "0:v" not in s  # no video stream mapping
+    assert "-map 0:v" not in s and "0:v" not in s  # no video stream mapping
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert "-map" not in s or "0:v" not in s # no video stream mapping
assert "-map" not in s # audio-only commands should not map streams
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_dub_audio_export.py` at line 22, The current assertion uses OR and
may pass incorrectly; update the assertion that references the string variable s
to ensure video mapping isn't present by either asserting "-map" not in s
(strict audio-only) or asserting the specific combination "-map 0:v" not in s
(allowing other -map entries like audio), e.g., replace the line containing
"assert \"-map\" not in s or \"0:v\" not in s" with one of those stronger checks
so the test fails if a video mapping is actually present.

- 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>
@debpalash

Copy link
Copy Markdown
Owner Author

Addressed review in ec50023 + a07cf71:

  • CodeQL path-injection (new audio-export branch): allowlist-sanitized lang_code before it reaches the output path (same safe_name pattern already used below), so the path component can't carry separators. The remaining CodeQL alerts are the pre-existing _native_save (user-chosen save_path from the Tauri dialog) and the video-mux video_path flows — same loopback-gated, same-user local-file threat model as feat(settings): configurable models directory (#64) #149, surfaced here only because this PR touches the file.
  • Greptile P2 — scene_start symmetry: now emit scene_start then scene_done(count=0) for audio so the prep SSE stage sequence matches the video path.
  • Greptile P2 — pill copy: shows "Preparing audio…" for audio jobs.
  • Greptile P2 — accept mismatch: widened the drop-accept regex + file-input accept to the full supported set (aac/opus/wma), matching the input-type detection and the backend allowlist.

11 tests green; frontend typecheck+build clean.

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>
@debpalash
debpalash merged commit 87eb5ad into main May 29, 2026
15 checks passed
@debpalash
debpalash deleted the feat/audio-only-dubbing branch May 29, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Title: Audio-Only Dubbing Mode with Optional Transcription Editing

2 participants