fix(subtitles): release prefetch pause on repeated subtitle events

onSubtitleChange paused prefetching unconditionally, but the processing
controller returns early when the text matches what it already has. Nothing
is tokenized, so nothing is emitted, so the resume that rides on the emit
never fires and prefetching idles for the rest of the cue. The reachable
trigger is a repeat arriving after a cache invalidation, such as mining a card
while the same line is still on screen.

The controller now reports whether it scheduled processing and the caller
resumes when it did not, so every pause has a matching resume.

Also harden the character-name candidate prefilter: Yomitan collapses emphatic
sequences before matching, so a stretched spelling still resolves to its entry
(ミナァァト matches ミナト). The candidate match now skips small kana and
prolonged marks, which only widens the probe set and so cannot drop a name.
This commit is contained in:
2026-08-03 23:37:04 -07:00
parent b0a2ce6e8a
commit f43674cc39
7 changed files with 161 additions and 7 deletions
@@ -17,7 +17,12 @@ export interface SubtitleProcessingControllerDeps {
export const DEFAULT_SUBTITLE_TOKENIZATION_CACHE_LIMIT = 2500;
export interface SubtitleProcessingController {
onSubtitleChange: (text: string) => void;
/**
* Returns whether the text was new and processing was scheduled. A false
* return means nothing will be emitted for this event, which callers that
* gate work on the emit (such as pausing subtitle prefetching) need to know.
*/
onSubtitleChange: (text: string) => boolean;
refreshCurrentSubtitle: (textOverride?: string) => void;
invalidateTokenizationCache: () => void;
preCacheTokenization: (text: string, data: SubtitleData) => void;
@@ -171,7 +176,7 @@ export function createSubtitleProcessingController(
return {
onSubtitleChange: (text: string) => {
if (text === latestText) {
return;
return false;
}
latestText = text;
if (
@@ -183,6 +188,7 @@ export function createSubtitleProcessingController(
lastPlainEmittedText = text;
}
processLatest();
return true;
},
refreshCurrentSubtitle: (textOverride?: string) => {
if (typeof textOverride === 'string') {