fix(overlay): show plain subtitle line immediately on tokenization cache miss (#184)

This commit is contained in:
2026-08-04 01:55:52 -07:00
committed by GitHub
parent b08cd0db35
commit fe4dacc1e7
11 changed files with 500 additions and 19 deletions
@@ -38,6 +38,9 @@ export function createSubtitleProcessingController(
: DEFAULT_SUBTITLE_TOKENIZATION_CACHE_LIMIT;
let latestText = '';
let lastEmittedText = '';
// Tracks the latest provisional plain emit across rapid changes and loop retries
// so the same line is never shown plain twice.
let lastPlainEmittedText: string | null = null;
let cacheGeneration = 0;
let lastEmittedGeneration = 0;
let processing = false;
@@ -81,9 +84,12 @@ export function createSubtitleProcessingController(
const startedAtMs = now();
if (!text.trim()) {
deps.emitSubtitle({ text, tokens: null });
if (lastPlainEmittedText !== text) {
deps.emitSubtitle({ text, tokens: null });
}
lastEmittedText = text;
lastEmittedGeneration = generation;
lastPlainEmittedText = null;
break;
}
@@ -93,11 +99,25 @@ export function createSubtitleProcessingController(
if (cachedTokenized) {
output = cachedTokenized;
} else {
// Cache miss: show the plain line on time; the tokenized payload
// upgrades it once ready. Skipped on refreshes of an already
// emitted line so downstream consumers never see a downgrade.
if (text !== lastEmittedText && text !== lastPlainEmittedText) {
deps.emitSubtitle({ text, tokens: null });
lastPlainEmittedText = text;
}
const tokenized = await deps.tokenizeSubtitle(text);
// A null result is a transient tokenizer failure, not a verdict on
// the line: caching the plain fallback would pin it untokenized for
// every later occurrence.
if (tokenized) {
output = tokenized;
// A result computed before an invalidation must not repopulate the
// fresh cache, or the retry below would serve the stale entry.
if (generation === cacheGeneration) {
setCachedTokenization(text, tokenized);
}
}
setCachedTokenization(text, output);
}
} catch (error) {
deps.logDebug?.(`Subtitle tokenization failed: ${(error as Error).message}`);
@@ -118,9 +138,16 @@ export function createSubtitleProcessingController(
continue;
}
deps.emitSubtitle(output);
// An untokenized result adds nothing when this line was already shown,
// either provisionally or as an earlier full emit (failed refresh) —
// emitting it would duplicate or downgrade what is on screen.
const plainAlreadyShown = lastPlainEmittedText === text || lastEmittedText === text;
if (!(output.tokens === null && output.text === text && plainAlreadyShown)) {
deps.emitSubtitle(output);
}
lastEmittedText = text;
lastEmittedGeneration = generation;
lastPlainEmittedText = null;
deps.logDebug?.(
`Subtitle tokenization delivered; elapsed=${now() - startedAtMs}ms, staleDrops=${staleDropCount}`,
);
@@ -147,6 +174,14 @@ export function createSubtitleProcessingController(
return;
}
latestText = text;
if (
processing &&
text !== lastPlainEmittedText &&
!tokenizationCache.has(normalizeSubtitleCacheKey(text))
) {
deps.emitSubtitle({ text, tokens: null });
lastPlainEmittedText = text;
}
processLatest();
},
refreshCurrentSubtitle: (textOverride?: string) => {
@@ -180,6 +215,7 @@ export function createSubtitleProcessingController(
latestText = text;
lastEmittedText = text;
lastEmittedGeneration = cacheGeneration;
lastPlainEmittedText = null;
return cached;
},
hasCachedSubtitle: (text: string) => {