Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/dubbing/export.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
12 changes: 4 additions & 8 deletions electron/src/shared/utils/multiLang.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
27 changes: 26 additions & 1 deletion electron/src/shared/utils/multiLang.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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);
});
});
Loading