From 0d91c2ec22fb39abc9c1b119704df45dca62f443 Mon Sep 17 00:00:00 2001 From: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:44:49 -0400 Subject: [PATCH] fix(dub): skip empty cues in translation readiness Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --- CHANGELOG.md | 2 ++ docs/dubbing/export.md | 2 ++ electron/src/shared/utils/multiLang.js | 12 +++------ electron/src/shared/utils/multiLang.test.js | 27 ++++++++++++++++++++- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 534142f5a..dfe31f5dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ metadata and the backend fallback mirror it. ### Fixed +- Let empty spoken cues coexist with complete cached dubbing translations, matching translation progress — thanks @rudycelekli! (#2662) + - MCP speech tools wait through model loading and progress-extended CPU renders instead of timing out before the backend (#2609) ## [0.5.7] — 2026-10-05 diff --git a/docs/dubbing/export.md b/docs/dubbing/export.md index 86f555a23..2a061ef80 100644 --- a/docs/dubbing/export.md +++ b/docs/dubbing/export.md @@ -23,3 +23,5 @@ filters; POSIX filenames keep literal backslashes. Two caption styles are availa Smart Fit exports burn captions after the video retime, so both styles follow the fitted timeline. The karaoke script can also be downloaded on its own from `GET /dub/ass/{job_id}` as an `.ass` sidecar. + +A cached multi-language translation is complete when every nonempty spoken cue has text for that language. Empty cues do not block a fully translated target or cause its cache to be translated again; a job with no spoken cues is not considered ready. This uses the same cue set as the translation progress indicator. diff --git a/electron/src/shared/utils/multiLang.js b/electron/src/shared/utils/multiLang.js index c0d31a575..5647af441 100644 --- a/electron/src/shared/utils/multiLang.js +++ b/electron/src/shared/utils/multiLang.js @@ -1,12 +1,8 @@ export function hasCompleteTranslation(segments, languageCode) { - return ( - !!languageCode && - segments.length > 0 && - segments.every((segment) => { - const text = segment.translations?.[languageCode]; - return typeof text === 'string' && text.trim().length > 0; - }) - ); + if (!languageCode) return false; + // Match the progress indicator: empty spoken cues need no translation. + const { ready, total } = translationProgressByCode(segments, [{ code: languageCode }])[languageCode]; + return total > 0 && ready === total; } export function multiLangTargets(activeLanguage, activeCode, selected) { diff --git a/electron/src/shared/utils/multiLang.test.js b/electron/src/shared/utils/multiLang.test.js index c1cf3c46d..213c6400a 100644 --- a/electron/src/shared/utils/multiLang.test.js +++ b/electron/src/shared/utils/multiLang.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { translationProgressByCode } from './multiLang'; +import { hasCompleteTranslation, translationProgressByCode } from './multiLang'; describe('translationProgressByCode', () => { it('never credits stale visible text to a different language', () => { @@ -17,3 +17,28 @@ describe('translationProgressByCode', () => { }); }); }); + + +describe('hasCompleteTranslation', () => { + it('accepts a fully translated target with an empty spoken cue', () => { + const segments = [ + { text_original: 'Hello', text: 'Hola', translations: { es: 'Hola' } }, + { text_original: ' ', text: '' }, + ]; + expect(translationProgressByCode(segments, [{ code: 'es' }])).toEqual({ es: { ready: 1, total: 1 } }); + expect(hasCompleteTranslation(segments, 'es')).toBe(true); + }); + it('still requires each nonempty cue to have the requested language', () => { + const segments = [ + { text_original: 'Hello', text: 'Hola', translations: { es: 'Hola' } }, + { text_original: 'Goodbye', text: 'Goodbye' }, + ]; + expect(hasCompleteTranslation(segments, 'es')).toBe(false); + expect(hasCompleteTranslation(segments, 'ja')).toBe(false); + }); + it('does not mark an empty job or an empty language complete', () => { + expect(hasCompleteTranslation([], 'es')).toBe(false); + expect(hasCompleteTranslation([{ text_original: '', text: '' }], 'es')).toBe(false); + expect(hasCompleteTranslation([{ text_original: 'Hello', translations: { es: 'Hola' } }], '')).toBe(false); + }); +});