fix(dub): auto-assign per-speaker cloned voices to segments (#486) - #576
Conversation
Multi-speaker dubbing diarizes the speakers and clones each one from the video
(the Voice dropdown shows "From Video → Speaker 1 / Speaker 2"), but every
segment was left on "Default" — the user had to set the voice on each row by
hand. The clone→segment binding simply never happened: the transcribe `final`
handler stored `speaker_clones` but set the segments without filling their
`profile_id`.
Bind them up front: new `applySpeakerCloneDefaults(segments, speakerClones)`
sets each segment's `profile_id` to its speaker's `auto:<safe>` clone id when a
clone exists and the user hasn't already chosen a voice. The id is computed by
`autoProfileId()`, which mirrors the backend clone-resolution key
(`speaker_id.lower().replace(" ","_")`) and the DubTab dropdown option value, so
all three agree. Only an *empty* profile_id is filled — an explicit per-speaker
or per-segment choice is never clobbered.
Pure helper + unit test (assign-when-cloned, never-clobber, no-clone-stays-
Default, no-op-without-clones).
Note: the issue's second symptom — different speakers' turns merged onto one
line — is a separate diarization/segment-grouping concern (speaker-turn
re-split) tracked as a follow-up; this fixes the per-speaker voice assignment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds ChangesSpeaker Clone Default Binding
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related issues
Possibly related PRs
Panel notes (ML inference, audio DSP, desktop systems, product polish):
🚥 Pre-merge checks | ✅ 8 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (8 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
| Filename | Overview |
|---|---|
| frontend/src/utils/segments.js | Adds autoProfileId() and applySpeakerCloneDefaults(); logic is correct and defensive (null-safe, non-clobbering). The slug formula matches all existing inline copies. |
| frontend/src/hooks/useDubWorkflow.js | Correctly wires applySpeakerCloneDefaults into the SSE "final" handler; reads speaker_clones directly from the message payload, not from stale store state. |
| frontend/src/test/segments.speakerClone.test.js | Four targeted unit tests cover the happy path, no-clobber invariant, missing-clone fallback, and null/empty clones no-op. Adequate for the surface area changed. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant BE as Backend (SSE)
participant WF as useDubWorkflow
participant SEG as segments.js
participant ST as Store
BE->>WF: SSE "segments" (streaming chunks)
WF->>ST: setDubSegments(prev + incoming) [no profile_id binding]
BE->>WF: "SSE "final" {segments, speaker_clones}"
WF->>SEG: applySpeakerCloneDefaults(normalized, m.speaker_clones)
SEG-->>WF: "segments with profile_id = auto:speaker_n (where clone exists)"
WF->>ST: setDubSegments(bound segments)
WF->>ST: setSpeakerClones(m.speaker_clones)
ST-->>WF: UI re-renders: Voice column shows Speaker 1/2 instead of Default
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant BE as Backend (SSE)
participant WF as useDubWorkflow
participant SEG as segments.js
participant ST as Store
BE->>WF: SSE "segments" (streaming chunks)
WF->>ST: setDubSegments(prev + incoming) [no profile_id binding]
BE->>WF: "SSE "final" {segments, speaker_clones}"
WF->>SEG: applySpeakerCloneDefaults(normalized, m.speaker_clones)
SEG-->>WF: "segments with profile_id = auto:speaker_n (where clone exists)"
WF->>ST: setDubSegments(bound segments)
WF->>ST: setSpeakerClones(m.speaker_clones)
ST-->>WF: UI re-renders: Voice column shows Speaker 1/2 instead of Default
Reviews (1): Last reviewed commit: "fix(dub): auto-assign per-speaker cloned..." | Re-trigger Greptile
What & why
#486 (reported on Discord with screenshots): multi-speaker dubbing diarizes the speakers and clones each from the video (the Voice dropdown shows From Video → Speaker 1 / Speaker 2), but every segment was left on "Default" — a row correctly labelled
Speaker 1had its Voice column readingDefault, so the user had to set the voice on every segment by hand.Root cause: the clone→segment binding never happened. The transcribe
finalhandler (useDubWorkflow.js) storedspeaker_clonesbut set the segments without filling theirprofile_id.The fix
New pure helper
applySpeakerCloneDefaults(segments, speakerClones)binds each segment to its detected speaker's clone up front:profile_id = autoProfileId(speaker_id)(auto:<safe>) when a clone exists for that speaker.autoProfileId()mirrors both the backend clone-resolution key (speaker_id.lower().replace(" ","_"), dub_generate.py) and the DubTab dropdown option value (DubTab.jsx:889) — so all three agree and the auto-selected option renders correctly.profile_id— an explicit per-speaker/per-segment choice is never clobbered.Tests
segments.speakerClone.test.js(4 cases): assign-when-cloned, never-clobber-explicit, no-clone-stays-Default, no-op-without-clones. Full vitest suite +typecheck:cigreen.Scope note
The issue's second symptom — different speakers' turns merged onto one line — is a separate diarization/segment-grouping concern (speaker-turn re-split). It's tracked as a follow-up; this PR fixes the per-speaker voice assignment (the primary complaint).
🤖 Generated with Claude Code
Multi-Speaker Dubbing Auto-Voice Assignment Fix
Overview
This PR fixes a critical issue in multi-speaker dubbing workflows where auto-cloned speaker voices were not being assigned to their corresponding segments. When users dubbed multi-speaker videos, the system would diarize speakers and clone each one, but all segments remained set to "Default," requiring manual reassignment.
Root Cause
The transcribe
finalhandler inuseDubWorkflow.jsstored thespeaker_clonesmetadata but created segments without populating theirprofile_idfield.Solution
New Helper Functions (
frontend/src/utils/segments.js)Two new exported utilities manage auto-assigned voice profiles for diarized speakers:
autoProfileId(speakerId): Normalizes a speaker ID into the backend-compatibleauto:<safe>form (lowercased, whitespace collapsed to underscores). This format aligns with the backend clone-resolution key and DubTab dropdown option values.applySpeakerCloneDefaults(segments, speakerClones): Binds each segment to its detected speaker's cloned voice at initialization by:profile_id = autoProfileId(speaker_id)when a clone exists for that speakerprofile_idvalues, preserving explicit per-speaker or per-segment voice selectionsIntegration (
frontend/src/hooks/useDubWorkflow.js)The transcription SSE "final" event handler now:
applySpeakerCloneDefaults(normalized, m.speaker_clones)Behavior Flow
graph TD A["SSE: final Event<br/>(Transcription Complete)"] --> B["Normalize Segments<br/>(id, text_original, speaker_id)"] B --> C["applySpeakerCloneDefaults<br/>(segments, speaker_clones)"] C --> D{Clone exists for<br/>this speaker?} D -->|Yes & profile_id empty| E["Set profile_id =<br/>auto:speaker_id"] D -->|No| F["Leave profile_id empty<br/>(Default)"] D -->|User already chose| G["Preserve user's<br/>profile_id"] E --> H["Store Segments<br/>with Auto-Bindings"] F --> H G --> H H --> I["UI Displays Voices<br/>Pre-assigned to Speakers"] I --> J{User satisfied?} J -->|Yes| K["Ready to Generate"] J -->|No| L["User can override<br/>per-speaker/segment"] L --> KTesting
frontend/src/test/segments.speakerClone.test.js):typecheck:cipass successfullyChanges Summary
frontend/src/utils/segments.jsautoProfileId()andapplySpeakerCloneDefaults()functionsfrontend/src/hooks/useDubWorkflow.jsapplySpeakerCloneDefaults()in SSE "final" handlerfrontend/src/test/segments.speakerClone.test.jsRelated Issue
A secondary concern—different speakers' turns merging onto one line—is identified as a separate diarization/segment-grouping concern and tracked as a follow-up item.