fix(dub): Export-step language tabs switch the transcript segments too (#1148)

Owner request with screenshot: the Original/Bengali/German/… pills above a
finished dub only swapped the preview VIDEO; the segment list kept showing
the last generated/edited language — German audio playing over Bengali text.

The pills now also route through switchDubLangCode (the P1.2 user-driven
language switch: outgoing text snapshotted into translations[prev], incoming
swapped in, non-destructive when no saved entry exists) plus setDubLang —
exactly what the language dropdown and the multi-language generate loop
already do, so fingerprint/staleness semantics are identical. The Original
pill deliberately leaves the editing language untouched: there is no
'original' editing language, and every row already renders the original
line under its translation.

Tests: clicking the German pill swaps segment text to the stored German
translation, snapshots the outgoing Bengali, and sets dubLangCode; the
Original pill leaves dubLangCode alone. Fail-before verified (wiring
stashed → text swap test fails). Full frontend suite: 1251 passed.

Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Palash Debnath
2026-07-14 01:39:29 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent 9ecb810946
commit 22a513a404
3 changed files with 113 additions and 1 deletions
+2
View File
@@ -16,6 +16,8 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently.
### Fixed
- **The Export step's language tabs now switch the transcript too.** Clicking Bengali/German/Hindi… above the finished dub swapped the *video* but left the segment list showing whichever language you generated last — German audio over Bengali text. The tabs now also swap every segment's text to that language (through the same per-language store the language picker uses, so nothing is lost when you switch back); the Original tab keeps your editing language as-is, since each row already shows the original line beneath its translation. (#1148)
- **A "backend crashed" notice can no longer outlive the update that fixed the crash — and the desktop shell's self-repair paths are now pinned by tests that CI actually runs.** Crash notices now record which app version wrote them, and a notice left behind by an older version is ignored and cleaned up after you upgrade instead of resurfacing as if the new build had crashed. The Windows blank-window repair (the one-click WebView cache fix after a BSOD) also gets regression tests pinning its safety contract — one attempt per request, never touches anything unasked, never blocks startup on a locked cache — and CI now runs the desktop shell's entire Rust unit-test suite on macOS, Windows, and Linux, which it previously never executed at all. (#1145)
- **The MLX-Audio phonemizer's language model now ships with the app environment instead of being fetched mid-generation.** Follow-up to the pip fix: with the installer present, the first English MLX-Audio generation would auto-download a small model straight from GitHub — an outbound request that bypasses the app's mirror system (a problem on restricted networks) and fails offline. The model is now a pinned dependency of the managed environment: it arrives at install/update time through the normal dependency flow, and first generation works fully offline. (#1146)
+12 -1
View File
@@ -223,7 +223,18 @@ export default function DubLeftColumn({
role="radio"
aria-checked={previewMode === code}
className={`dub-lang-pill ${previewMode === code ? 'is-active' : ''}`}
onClick={() => setPreviewMode(code)}
onClick={() => {
setPreviewMode(code);
// The transcript/segment list follows the previewed track:
// swap segment texts to this language's saved translations
// (the P1.2 per-language store non-destructive, exactly
// what the language dropdown and multi-language generate
// already do). Without this, previewing German played
// German audio over, say, Bengali segment text.
const st = useAppStore.getState();
st.setDubLang(label);
st.switchDubLangCode(code);
}}
title={trackTooltip(code)}
>
{label}
@@ -0,0 +1,99 @@
import React, { createRef } from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import i18n from '../i18n';
// Preview-tab transcript sync: clicking a dubbed-language pill on the
// Export step must ALSO switch the segment texts to that language (the P1.2
// per-language translations store) previously the tabs only swapped the
// video, so previewing German played German audio over Bengali segment text.
vi.mock('../components/WaveformTimeline', () => ({ default: () => <div data-testid="wf" /> }));
vi.mock('../components/MultiLangPicker', () => ({ default: () => <div data-testid="mlp" /> }));
vi.mock('react-hot-toast', () => ({
default: { error: vi.fn(), success: vi.fn(), loading: vi.fn() },
}));
const dubListTracks = vi.hoisted(() => vi.fn());
vi.mock('../api/dub', () => ({ dubListTracks: (...a) => dubListTracks(...a) }));
import DubLeftColumn from '../components/dub/DubLeftColumn';
import { useAppStore } from '../store';
const t = i18n.t.bind(i18n);
function makeProps(over = {}) {
return {
hasDubbedTrack: true,
t,
i18n,
previewMode: 'bn',
setPreviewMode: vi.fn(),
dubTracks: ['bn', 'de'],
videoSrc: '',
waveformRef: createRef(),
dubJobId: 'job1',
dubSegments: [{ id: '1', text: 'hi' }],
timelineOnsets: [],
timelineSelSegId: null,
setTimelineSelSegId: vi.fn(),
incrementalPlan: null,
segmentMoveResize: vi.fn(),
segmentDelete: vi.fn(),
onTimelinePreviewSegment: vi.fn(),
dubStep: 'done',
dubProgress: { current: 0, total: 0, text: '' },
fmtDur: (s) => `${s}s`,
genElapsed: 0,
genRemaining: null,
speakerClones: {},
setDubSegments: vi.fn(),
profiles: [],
settingsOpen: false,
setSettingsOpen: vi.fn(),
dubLang: 'Bengali',
dubLangCode: 'bn',
translateQuality: 'fast',
activeEngineUnavailable: false,
translateProvider: 'google',
dubInstruct: '',
setDubInstruct: vi.fn(),
handleTranslateAll: vi.fn(),
isTranslating: false,
editSegments: vi.fn(),
...over,
};
}
beforeEach(() => {
dubListTracks.mockResolvedValue({ tracks: {} });
useAppStore.setState({
dubLangCode: 'bn',
dubLang: 'Bengali',
dubSegments: [
{
id: '1',
text: 'বাংলা লাইন',
text_original: 'the original line',
translations: { bn: 'বাংলা লাইন', de: 'die deutsche Zeile' },
},
],
});
});
describe('preview tab → transcript language sync', () => {
it('clicking a language pill swaps segment text to that language', () => {
render(<DubLeftColumn {...makeProps()} />);
fireEvent.click(screen.getByRole('radio', { name: /german|deutsch/i }));
const st = useAppStore.getState();
expect(st.dubLangCode).toBe('de');
expect(st.dubSegments[0].text).toBe('die deutsche Zeile');
// Outgoing language snapshotted, not lost.
expect(st.dubSegments[0].translations.bn).toBe('বাংলা লাইন');
});
it('the Original pill leaves the editing language untouched', () => {
render(<DubLeftColumn {...makeProps()} />);
fireEvent.click(screen.getByRole('radio', { name: /original/i }));
expect(useAppStore.getState().dubLangCode).toBe('bn');
});
});