fix(dub): completed tracks always show their tabs + history keeps its language (P0) - #956
Conversation
… 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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughBackend ChangesBackend language column healing
Frontend dub track visibility, restore, and tooltip UI
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 7 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (7 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. 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 |
|---|---|
| backend/services/dub_pipeline.py | Adds guarded UPSERT updates for dub language columns so non-empty generated values persist without later empty clobbers. |
| frontend/src/App.jsx | Restores dub language fields from job_data when older database columns are empty. |
| frontend/src/pages/DubTab.jsx | Fixes initial tab visibility and restore preview selection, but preview reconciliation still misses track-list changes. |
| frontend/src/components/dub/DubLeftColumn.jsx | Adds local track metadata hydration for pill tooltips without blocking the main track switcher. |
| frontend/src/api/dub.ts | Adds a typed wrapper around the existing /dub/tracks/{job_id} endpoint. |
| frontend/src/i18n/locales/en.json | Adds English tooltip and timing labels, but matching keys are missing from the other locale files. |
| frontend/src/test/DubTrackPillTooltip.test.jsx | Adds tests for tooltip hydration and failure-silent metadata fetch behavior. |
| frontend/src/test/DubTrackTabsRestore.test.jsx | Adds tests for restored completed tracks, misaligned language codes, and empty track sets. |
| tests/test_dub_pipeline_state.py | Adds backend tests for healing language columns and preserving them during later empty saves. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant H as dub_history
participant A as App restore
participant S as Store
participant D as DubTab
participant V as Preview
H->>A: columns or job_data language
A->>S: set language + track keys
S->>D: done state + tracks
D->>D: show tabs when tracks exist
D->>V: preview matching lang or first track
%%{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 H as dub_history
participant A as App restore
participant S as Store
participant D as DubTab
participant V as Preview
H->>A: columns or job_data language
A->>S: set language + track keys
S->>D: done state + tracks
D->>D: show tabs when tracks exist
D->>V: preview matching lang or first track
Reviews (1): Last reviewed commit: "docs(changelog): open [Unreleased] with ..." | Re-trigger Greptile
| "timing_concise": "Concise", | ||
| "timing_stretch_video": "Stretch Video", | ||
| "timing_strict_slot": "Strict slot", | ||
| "track_tip_duration": "Duration {{duration}}", | ||
| "track_tip_timing": "Timing {{strategy}}", |
There was a problem hiding this comment.
These new tooltip and timing labels are user-facing, but they were added only to en.json. In any non-English locale, the track-pill tooltip falls back to English text such as Duration and Timing, which creates mixed-language UI and violates the locale coverage rule for new strings.
Context Used: CLAUDE.md (source)
| useEffect(() => { | ||
| if (hasDubbedTrack && previewMode === 'original' && dubLangCode && dubLangCode !== 'und') { | ||
| setPreviewMode(dubLangCode); | ||
| if (hasDubbedTrack && previewMode === 'original') { | ||
| setPreviewMode(dubTracks.includes(dubLangCode) ? dubLangCode : dubTracks[0]); | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [hasDubbedTrack, dubLangCode]); |
There was a problem hiding this comment.
Track Set Changes Leave Stale Preview
The guard validates previewMode against dubTracks, but the effect does not rerun when dubTracks changes while dubStep stays done and dubLangCode is unchanged. A completed regenerate that replaces ['bn'] with ['es'] can leave previewMode on bn, so the player requests /dub/preview-video/{jobId}?lang=bn even though that track no longer exists.
| useEffect(() => { | |
| if (hasDubbedTrack && previewMode === 'original' && dubLangCode && dubLangCode !== 'und') { | |
| setPreviewMode(dubLangCode); | |
| if (hasDubbedTrack && previewMode === 'original') { | |
| setPreviewMode(dubTracks.includes(dubLangCode) ? dubLangCode : dubTracks[0]); | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [hasDubbedTrack, dubLangCode]); | |
| useEffect(() => { | |
| if (hasDubbedTrack && previewMode === 'original') { | |
| setPreviewMode(dubTracks.includes(dubLangCode) ? dubLangCode : dubTracks[0]); | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [hasDubbedTrack, dubLangCode, dubTracks]); |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/components/dub/DubLeftColumn.jsx`:
- Line 21: The dub tooltip strings are only defined in the English locale, so
non-English users will see fallback text. Update the locale resource files for
the other 20 languages to add the new dub keys used by DubLeftColumn
(dub.track_tip_duration and dub.track_tip_timing), matching the existing
translation structure and keeping the key names consistent with en.json.
In `@frontend/src/pages/DubTab.jsx`:
- Around line 369-381: Reset the preview selection whenever the mounted DubTab
is showing a previewMode that is no longer valid for the current dubTracks, not
just when previewMode is original. Update the useEffect in DubTab to resync
previewMode when restoring a different job so videoSrc never requests a stale
language, using the existing hasDubbedTrack, previewMode, dubTracks, and
dubLangCode logic as the anchor. Add a regression test that restores one job and
then another in the same mounted component to verify previewMode is corrected to
a valid track instead of staying on the previous job’s language.
🪄 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: 15c32493-6014-4573-86d6-5bd449d1d69b
📒 Files selected for processing (10)
CHANGELOG.mdbackend/services/dub_pipeline.pyfrontend/src/App.jsxfrontend/src/api/dub.tsfrontend/src/components/dub/DubLeftColumn.jsxfrontend/src/i18n/locales/en.jsonfrontend/src/pages/DubTab.jsxfrontend/src/test/DubTrackPillTooltip.test.jsxfrontend/src/test/DubTrackTabsRestore.test.jsxtests/test_dub_pipeline_state.py
| import WaveformTimeline from '../WaveformTimeline'; | ||
| import MultiLangPicker from '../MultiLangPicker'; | ||
| import { API } from '../../api/client'; | ||
| import { dubListTracks } from '../../api/dub'; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd . frontend/src/i18n/locales -e json | while read -r f; do
echo "== $f =="
jq -e '.dub.track_tip_duration // "MISSING"' "$f"
jq -e '.dub.track_tip_timing // "MISSING"' "$f"
doneRepository: debpalash/OmniVoice-Studio
Length of output: 1456
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== DubLeftColumn.jsx (relevant lines) =="
sed -n '140,230p' frontend/src/components/dub/DubLeftColumn.jsx | cat -n
echo
echo "== locale keys under dub =="
python3 - <<'PY'
import json, pathlib
for path in sorted(pathlib.Path('frontend/src/i18n/locales').glob('*.json')):
data = json.loads(path.read_text(encoding='utf-8'))
dub = data.get('dub', {})
keys = sorted(k for k in dub.keys() if 'tip' in k or 'timing' in k)
print(path.name, keys)
PYRepository: debpalash/OmniVoice-Studio
Length of output: 5387
Add the new dub tooltip keys to the other 20 locale files
frontend/src/components/dub/DubLeftColumn.jsx:34-46 now calls dub.track_tip_duration and dub.track_tip_timing; only frontend/src/i18n/locales/en.json has those entries. Non-English locales will fall back to the English defaults, so the tooltip copy stays untranslated outside English.
🤖 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 `@frontend/src/components/dub/DubLeftColumn.jsx` at line 21, The dub tooltip
strings are only defined in the English locale, so non-English users will see
fallback text. Update the locale resource files for the other 20 languages to
add the new dub keys used by DubLeftColumn (dub.track_tip_duration and
dub.track_tip_timing), matching the existing translation structure and keeping
the key names consistent with en.json.
Source: Path instructions
| // When a dub finishes, jump the preview to the freshly-dubbed language so the | ||
| // result plays immediately — the user can tap back to Original any time. | ||
| // Membership guard: only jump to a language that actually has a track, | ||
| // otherwise fall back to the first track. Restored projects can have | ||
| // dubLangCode out of sync with the tracks (e.g. 'en'/'und' with tracks | ||
| // ['bn']) and an unguarded jump would point the player at | ||
| // /dub/preview-video?lang=en — a guaranteed 404. | ||
| useEffect(() => { | ||
| if (hasDubbedTrack && previewMode === 'original' && dubLangCode && dubLangCode !== 'und') { | ||
| setPreviewMode(dubLangCode); | ||
| if (hasDubbedTrack && previewMode === 'original') { | ||
| setPreviewMode(dubTracks.includes(dubLangCode) ? dubLangCode : dubTracks[0]); | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [hasDubbedTrack, dubLangCode]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== DubTab.jsx around previewMode / effects ==\n'
ast-grep outline frontend/src/pages/DubTab.jsx --view expanded || true
printf '\n-- relevant lines --\n'
sed -n '150,420p' frontend/src/pages/DubTab.jsx | cat -n
printf '\n== Search for previewMode setters/usages ==\n'
rg -n "setPreviewMode|previewMode" frontend/src/pages frontend/src -g '!**/node_modules/**' || true
printf '\n== App.jsx restore/load paths ==\n'
ast-grep outline frontend/src/App.jsx --view expanded || true
printf '\n-- relevant restore/load slices --\n'
rg -n "restoreDubHistory|loadProject|DubTab|dubJobId|dubTracks|dubLangCode" frontend/src/App.jsx frontend/src -g '!**/node_modules/**' || true
printf '\n== DubTrackTabsRestore tests ==\n'
rg -n "renderDone|restore|DubTrackTabsRestore|previewMode|dubLangCode|dubTracks" frontend/src tests -g '!**/node_modules/**' || trueRepository: debpalash/OmniVoice-Studio
Length of output: 50382
Reset the preview selection when restoring a different job
previewMode survives on the mounted DubTab, so the guard at lines 377-381 only covers the “from original” case. Restore job A with 'bn', then restore job B whose dubTracks is ['es'], and previewMode stays 'bn'; videoSrc then requests /dub/preview-video/{jobB}?lang=bn and 404s. Broaden the effect to resync when the current previewMode is no longer in dubTracks, and add a same-mount restore regression.
🤖 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 `@frontend/src/pages/DubTab.jsx` around lines 369 - 381, Reset the preview
selection whenever the mounted DubTab is showing a previewMode that is no longer
valid for the current dubTracks, not just when previewMode is original. Update
the useEffect in DubTab to resync previewMode when restoring a different job so
videoSrc never requests a stale language, using the existing hasDubbedTrack,
previewMode, dubTracks, and dubLangCode logic as the anchor. Add a regression
test that restores one job and then another in the same mounted component to
verify previewMode is corrected to a valid track instead of staying on the
previous job’s language.
#956 merged with a red Tests gate — my merge script ran unconditionally instead of aborting on the gate value; the failure was oxfmt-only on the two new test files. Whitespace-only fix, tests re-verified green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bug (owner report)
A project with a completed dubbed video doesn't show the video track tabs unless the language is re-selected.
Root cause (verified chain)
DubTab.jsx: the visibility gate contained a tautology (dubTracks?.length > 0 || !!dubTracks— always true since the store defaults to[]), so tabs were effectively keyed to the language dropdown (dubLangCode !== 'und'), not the persisted tracks.dubLangCodeto'und'becausedub_history.language_codeis frozen at "": the save UPSERT never updateslanguage/language_codeafter the ingest-time insert, even though generation sets them (the values live correctly inside thejob_dataJSON).Fixes
hasDubbedTrack = dubStep === 'done' && dubTracks.length > 0— the language dropdown only matters for generating new tracks.dubTracks.includes(dubLangCode) ? … : dubTracks[0]) — kills a preview-404 class on project restores.language/language_codewith the same empty-guarded CASE pattern ascontent_hash— new saves fix the columns.job_datavalues — existing rows in users' DBs heal with no migration (backward-compat rule).Plus P0 polish: track pills get duration/timing tooltips (hydrated lazily from the previously-unused
GET /dub/tracks/{job_id}), an accurate now-playing indicator, i18n'd strings.Tests
9 new (7 frontend across 2 files + 2 backend) — fail-before verified per-fix by stashing. Full frontend suite: 108 files / 855 tests green; typecheck + lint clean; backend save_job consumers 49/49.
🤖 Generated with Claude Code
Summary
dubTracks.language/language_codevalues and fall back tojob_datafor older rows.UI sketch
Before:
After:
Behavior
flowchart TD A[Dub job restored / saved] --> B{Has persisted tracks?} B -->|No| C[Keep preview on Original / hide tabs] B -->|Yes| D[Show dubbed track tabs] D --> E{Requested auto-jump lang exists in dubTracks?} E -->|Yes| F[Jump to that lang] E -->|No| G[Jump to first available track] A --> H[Save_job UPSERT] H --> I[Heal language fields only when incoming values are non-empty] A --> J[Restore from older DB row] J --> K[Fallback to job_data values / defaults] </mermaid> <!-- end of auto-generated comment: release notes by coderabbit.ai -->