mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-29 00:55:15 -07:00
feat: use cached annotations on subtitle change and skip pre-warmed cues (#97)
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: subtitles
|
||||||
|
|
||||||
|
- Improved subtitle annotation prefetching so cached colored annotations and character images are ready for more live subtitle changes without delaying raw subtitle display.
|
||||||
@@ -19,6 +19,7 @@ interface IntegrationTestContext {
|
|||||||
function createIntegrationTestContext(
|
function createIntegrationTestContext(
|
||||||
options: {
|
options: {
|
||||||
highlightEnabled?: boolean;
|
highlightEnabled?: boolean;
|
||||||
|
nPlusOneEnabled?: boolean;
|
||||||
onFindNotes?: () => Promise<number[]>;
|
onFindNotes?: () => Promise<number[]>;
|
||||||
onNotesInfo?: () => Promise<unknown[]>;
|
onNotesInfo?: () => Promise<unknown[]>;
|
||||||
stateDirPrefix?: string;
|
stateDirPrefix?: string;
|
||||||
@@ -59,6 +60,12 @@ function createIntegrationTestContext(
|
|||||||
knownWords: {
|
knownWords: {
|
||||||
highlightEnabled: options.highlightEnabled ?? true,
|
highlightEnabled: options.highlightEnabled ?? true,
|
||||||
},
|
},
|
||||||
|
nPlusOne:
|
||||||
|
options.nPlusOneEnabled === undefined
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
enabled: options.nPlusOneEnabled,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{} as never,
|
{} as never,
|
||||||
{} as never,
|
{} as never,
|
||||||
@@ -161,6 +168,47 @@ test('AnkiIntegration.refreshKnownWordCache bypasses stale checks', async () =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('AnkiIntegration.refreshKnownWordCache notifies annotation cache listeners', async () => {
|
||||||
|
const ctx = createIntegrationTestContext({
|
||||||
|
stateDirPrefix: 'subminer-anki-integration-refresh-notify-',
|
||||||
|
});
|
||||||
|
let notifications = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.integration.setKnownWordCacheUpdatedCallback(() => {
|
||||||
|
notifications += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.integration.refreshKnownWordCache();
|
||||||
|
|
||||||
|
assert.equal(notifications, 1);
|
||||||
|
} finally {
|
||||||
|
cleanupIntegrationTestContext(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AnkiIntegration.refreshKnownWordCache notifies when n+1 is enabled without highlights', async () => {
|
||||||
|
const ctx = createIntegrationTestContext({
|
||||||
|
highlightEnabled: false,
|
||||||
|
nPlusOneEnabled: true,
|
||||||
|
stateDirPrefix: 'subminer-anki-integration-nplusone-notify-',
|
||||||
|
});
|
||||||
|
let notifications = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.integration.setKnownWordCacheUpdatedCallback(() => {
|
||||||
|
notifications += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.integration.refreshKnownWordCache();
|
||||||
|
|
||||||
|
assert.equal(ctx.calls.findNotes, 1);
|
||||||
|
assert.equal(notifications, 1);
|
||||||
|
} finally {
|
||||||
|
cleanupIntegrationTestContext(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('AnkiIntegration.refreshKnownWordCache skips work when highlight mode is disabled', async () => {
|
test('AnkiIntegration.refreshKnownWordCache skips work when highlight mode is disabled', async () => {
|
||||||
const ctx = createIntegrationTestContext({
|
const ctx = createIntegrationTestContext({
|
||||||
highlightEnabled: false,
|
highlightEnabled: false,
|
||||||
|
|||||||
@@ -526,7 +526,9 @@ export class AnkiIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isKnownWordCacheEnabled(): boolean {
|
private isKnownWordCacheEnabled(): boolean {
|
||||||
return this.config.knownWords?.highlightEnabled === true;
|
return (
|
||||||
|
this.config.knownWords?.highlightEnabled === true || this.config.nPlusOne?.enabled === true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getConfiguredAnkiTags(): string[] {
|
private getConfiguredAnkiTags(): string[] {
|
||||||
@@ -549,7 +551,11 @@ export class AnkiIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refreshKnownWordCache(): Promise<void> {
|
async refreshKnownWordCache(): Promise<void> {
|
||||||
return this.knownWordCache.refresh(true);
|
const shouldNotify = this.isKnownWordCacheEnabled();
|
||||||
|
await this.knownWordCache.refresh(true);
|
||||||
|
if (shouldNotify) {
|
||||||
|
this.notifyKnownWordCacheUpdated();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private appendKnownWordsFromNoteInfo(noteInfo: NoteInfo): void {
|
private appendKnownWordsFromNoteInfo(noteInfo: NoteInfo): void {
|
||||||
|
|||||||
@@ -242,3 +242,59 @@ test('prefetch service pause/resume halts and continues tokenization', async ()
|
|||||||
|
|
||||||
assert.ok(tokenizeCalls > callsWhenPaused + 1, 'Should resume tokenizing after unpause');
|
assert.ok(tokenizeCalls > callsWhenPaused + 1, 'Should resume tokenizing after unpause');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('prefetch service skips cues already present in tokenization cache', async () => {
|
||||||
|
const cues = makeCues(5);
|
||||||
|
const tokenizedTexts: string[] = [];
|
||||||
|
|
||||||
|
const service = createSubtitlePrefetchService({
|
||||||
|
cues,
|
||||||
|
tokenizeSubtitle: async (text) => {
|
||||||
|
tokenizedTexts.push(text);
|
||||||
|
return { text, tokens: [] };
|
||||||
|
},
|
||||||
|
preCacheTokenization: () => {},
|
||||||
|
hasCachedTokenization: (text) => text === 'line-0' || text === 'line-1',
|
||||||
|
isCacheFull: () => false,
|
||||||
|
priorityWindowSize: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
service.start(0);
|
||||||
|
for (let i = 0; i < 10; i += 1) {
|
||||||
|
await flushMicrotasks();
|
||||||
|
}
|
||||||
|
service.stop();
|
||||||
|
|
||||||
|
assert.ok(!tokenizedTexts.includes('line-0'));
|
||||||
|
assert.ok(!tokenizedTexts.includes('line-1'));
|
||||||
|
assert.ok(tokenizedTexts.includes('line-2'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prefetch service deduplicates repeated cue text within a run', async () => {
|
||||||
|
const cues: SubtitleCue[] = [
|
||||||
|
{ startTime: 0, endTime: 1, text: 'same' },
|
||||||
|
{ startTime: 1, endTime: 2, text: 'same' },
|
||||||
|
{ startTime: 2, endTime: 3, text: 'other' },
|
||||||
|
];
|
||||||
|
const tokenizedTexts: string[] = [];
|
||||||
|
|
||||||
|
const service = createSubtitlePrefetchService({
|
||||||
|
cues,
|
||||||
|
tokenizeSubtitle: async (text) => {
|
||||||
|
tokenizedTexts.push(text);
|
||||||
|
return { text, tokens: [] };
|
||||||
|
},
|
||||||
|
preCacheTokenization: () => {},
|
||||||
|
isCacheFull: () => false,
|
||||||
|
priorityWindowSize: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
service.start(0);
|
||||||
|
for (let i = 0; i < 10; i += 1) {
|
||||||
|
await flushMicrotasks();
|
||||||
|
}
|
||||||
|
service.stop();
|
||||||
|
|
||||||
|
assert.deepEqual(tokenizedTexts.filter((text) => text === 'same'), ['same']);
|
||||||
|
assert.ok(tokenizedTexts.includes('other'));
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { SubtitleData } from '../../types';
|
import type { SubtitleData } from '../../types';
|
||||||
import type { SubtitleCue } from '../../types';
|
import type { SubtitleCue } from '../../types';
|
||||||
|
import { normalizeSubtitleCacheKey } from './subtitle-processing-controller';
|
||||||
|
|
||||||
export interface SubtitlePrefetchServiceDeps {
|
export interface SubtitlePrefetchServiceDeps {
|
||||||
cues: SubtitleCue[];
|
cues: SubtitleCue[];
|
||||||
tokenizeSubtitle: (text: string) => Promise<SubtitleData | null>;
|
tokenizeSubtitle: (text: string) => Promise<SubtitleData | null>;
|
||||||
preCacheTokenization: (text: string, data: SubtitleData) => void;
|
preCacheTokenization: (text: string, data: SubtitleData) => void;
|
||||||
|
hasCachedTokenization?: (text: string) => boolean;
|
||||||
isCacheFull: () => boolean;
|
isCacheFull: () => boolean;
|
||||||
priorityWindowSize?: number;
|
priorityWindowSize?: number;
|
||||||
}
|
}
|
||||||
@@ -58,6 +60,7 @@ export function createSubtitlePrefetchService(
|
|||||||
async function tokenizeCueList(
|
async function tokenizeCueList(
|
||||||
cuesToProcess: SubtitleCue[],
|
cuesToProcess: SubtitleCue[],
|
||||||
runId: number,
|
runId: number,
|
||||||
|
warmedKeys: Set<string>,
|
||||||
options: { allowWhenCacheFull?: boolean } = {},
|
options: { allowWhenCacheFull?: boolean } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (const cue of cuesToProcess) {
|
for (const cue of cuesToProcess) {
|
||||||
@@ -78,6 +81,15 @@ export function createSubtitlePrefetchService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cacheKey = normalizeSubtitleCacheKey(cue.text);
|
||||||
|
if (!cacheKey || warmedKeys.has(cacheKey) || deps.hasCachedTokenization?.(cue.text)) {
|
||||||
|
if (cacheKey) {
|
||||||
|
warmedKeys.add(cacheKey);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
warmedKeys.add(cacheKey);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await deps.tokenizeSubtitle(cue.text);
|
const result = await deps.tokenizeSubtitle(cue.text);
|
||||||
if (result && !stopped && runId === currentRunId) {
|
if (result && !stopped && runId === currentRunId) {
|
||||||
@@ -94,10 +106,11 @@ export function createSubtitlePrefetchService(
|
|||||||
|
|
||||||
async function startPrefetching(currentTimeSeconds: number, runId: number): Promise<void> {
|
async function startPrefetching(currentTimeSeconds: number, runId: number): Promise<void> {
|
||||||
const cues = deps.cues;
|
const cues = deps.cues;
|
||||||
|
const warmedKeys = new Set<string>();
|
||||||
|
|
||||||
// Phase 1: Priority window
|
// Phase 1: Priority window
|
||||||
const priorityCues = computePriorityWindow(cues, currentTimeSeconds, windowSize);
|
const priorityCues = computePriorityWindow(cues, currentTimeSeconds, windowSize);
|
||||||
await tokenizeCueList(priorityCues, runId, { allowWhenCacheFull: true });
|
await tokenizeCueList(priorityCues, runId, warmedKeys, { allowWhenCacheFull: true });
|
||||||
|
|
||||||
if (stopped || runId !== currentRunId) {
|
if (stopped || runId !== currentRunId) {
|
||||||
return;
|
return;
|
||||||
@@ -108,7 +121,7 @@ export function createSubtitlePrefetchService(
|
|||||||
const remainingCues = cues.filter(
|
const remainingCues = cues.filter(
|
||||||
(cue) => cue.startTime > currentTimeSeconds && !priorityTexts.has(cue.text),
|
(cue) => cue.startTime > currentTimeSeconds && !priorityTexts.has(cue.text),
|
||||||
);
|
);
|
||||||
await tokenizeCueList(remainingCues, runId);
|
await tokenizeCueList(remainingCues, runId, warmedKeys);
|
||||||
|
|
||||||
if (stopped || runId !== currentRunId) {
|
if (stopped || runId !== currentRunId) {
|
||||||
return;
|
return;
|
||||||
@@ -118,7 +131,7 @@ export function createSubtitlePrefetchService(
|
|||||||
const earlierCues = cues.filter(
|
const earlierCues = cues.filter(
|
||||||
(cue) => cue.startTime <= currentTimeSeconds && !priorityTexts.has(cue.text),
|
(cue) => cue.startTime <= currentTimeSeconds && !priorityTexts.has(cue.text),
|
||||||
);
|
);
|
||||||
await tokenizeCueList(earlierCues, runId);
|
await tokenizeCueList(earlierCues, runId, warmedKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -236,6 +236,31 @@ test('consumeCachedSubtitle returns prefetched payload and prevents reprocessing
|
|||||||
assert.deepEqual(emitted, []);
|
assert.deepEqual(emitted, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('hasCachedSubtitle checks prefetched entries without consuming them', async () => {
|
||||||
|
const emitted: SubtitleData[] = [];
|
||||||
|
let tokenizeCalls = 0;
|
||||||
|
const controller = createSubtitleProcessingController({
|
||||||
|
tokenizeSubtitle: async (text) => {
|
||||||
|
tokenizeCalls += 1;
|
||||||
|
return { text, tokens: [] };
|
||||||
|
},
|
||||||
|
emitSubtitle: (payload) => emitted.push(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.preCacheTokenization('猫\\Nです', { text: '猫\nです', tokens: [] });
|
||||||
|
|
||||||
|
assert.equal(controller.hasCachedSubtitle('猫\nです'), true);
|
||||||
|
|
||||||
|
controller.onSubtitleChange('猫\nです');
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
assert.equal(tokenizeCalls, 0);
|
||||||
|
assert.deepEqual(emitted, [{ text: '猫\nです', tokens: [] }]);
|
||||||
|
|
||||||
|
controller.invalidateTokenizationCache();
|
||||||
|
assert.equal(controller.hasCachedSubtitle('猫\nです'), false);
|
||||||
|
});
|
||||||
|
|
||||||
test('isCacheFull returns false when cache is below limit', () => {
|
test('isCacheFull returns false when cache is below limit', () => {
|
||||||
const controller = createSubtitleProcessingController({
|
const controller = createSubtitleProcessingController({
|
||||||
tokenizeSubtitle: async (text) => ({ text, tokens: null }),
|
tokenizeSubtitle: async (text) => ({ text, tokens: null }),
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ export interface SubtitleProcessingController {
|
|||||||
invalidateTokenizationCache: () => void;
|
invalidateTokenizationCache: () => void;
|
||||||
preCacheTokenization: (text: string, data: SubtitleData) => void;
|
preCacheTokenization: (text: string, data: SubtitleData) => void;
|
||||||
consumeCachedSubtitle: (text: string) => SubtitleData | null;
|
consumeCachedSubtitle: (text: string) => SubtitleData | null;
|
||||||
|
hasCachedSubtitle: (text: string) => boolean;
|
||||||
isCacheFull: () => boolean;
|
isCacheFull: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSubtitleCacheKey(text: string): string {
|
export function normalizeSubtitleCacheKey(text: string): string {
|
||||||
return text.replace(/\r\n/g, '\n').replace(/\\N/g, '\n').replace(/\\n/g, '\n').trim();
|
return text.replace(/\r\n/g, '\n').replace(/\\N/g, '\n').replace(/\\n/g, '\n').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +153,9 @@ export function createSubtitleProcessingController(
|
|||||||
refreshRequested = false;
|
refreshRequested = false;
|
||||||
return cached;
|
return cached;
|
||||||
},
|
},
|
||||||
|
hasCachedSubtitle: (text: string) => {
|
||||||
|
return tokenizationCache.has(normalizeSubtitleCacheKey(text));
|
||||||
|
},
|
||||||
isCacheFull: () => {
|
isCacheFull: () => {
|
||||||
return tokenizationCache.size >= SUBTITLE_TOKENIZATION_CACHE_LIMIT;
|
return tokenizationCache.size >= SUBTITLE_TOKENIZATION_CACHE_LIMIT;
|
||||||
},
|
},
|
||||||
|
|||||||
+16
-1
@@ -1754,10 +1754,17 @@ function emitAutoplayPrimedSubtitle(mediaPath: string, text: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
appState.currentSubText = text;
|
appState.currentSubText = text;
|
||||||
|
subtitlePrefetchService?.pause();
|
||||||
|
const cachedPayload = subtitleProcessingController.consumeCachedSubtitle(text);
|
||||||
|
if (cachedPayload) {
|
||||||
|
subtitleProcessingController.onSubtitleChange(text);
|
||||||
|
emitSubtitlePayload(cachedPayload);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const rawPayload = withCurrentSubtitleTiming({ text, tokens: null });
|
const rawPayload = withCurrentSubtitleTiming({ text, tokens: null });
|
||||||
appState.currentSubtitleData = rawPayload;
|
appState.currentSubtitleData = rawPayload;
|
||||||
broadcastToOverlayWindows('subtitle:set', rawPayload);
|
broadcastToOverlayWindows('subtitle:set', rawPayload);
|
||||||
subtitlePrefetchService?.pause();
|
|
||||||
subtitleProcessingController.onSubtitleChange(text);
|
subtitleProcessingController.onSubtitleChange(text);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1834,6 +1841,7 @@ const subtitlePrefetchInitController = createSubtitlePrefetchInitController({
|
|||||||
preCacheTokenization: (text, data) => {
|
preCacheTokenization: (text, data) => {
|
||||||
subtitleProcessingController.preCacheTokenization(text, data);
|
subtitleProcessingController.preCacheTokenization(text, data);
|
||||||
},
|
},
|
||||||
|
hasCachedTokenization: (text) => subtitleProcessingController.hasCachedSubtitle(text),
|
||||||
isCacheFull: () => subtitleProcessingController.isCacheFull(),
|
isCacheFull: () => subtitleProcessingController.isCacheFull(),
|
||||||
logInfo: (message) => logger.info(message),
|
logInfo: (message) => logger.info(message),
|
||||||
logWarn: (message) => logger.warn(message),
|
logWarn: (message) => logger.warn(message),
|
||||||
@@ -4219,6 +4227,12 @@ const recordTrackedCardsMined = (count: number, noteIds?: number[]): void => {
|
|||||||
appState.immersionTracker?.recordCardsMined(count, noteIds);
|
appState.immersionTracker?.recordCardsMined(count, noteIds);
|
||||||
};
|
};
|
||||||
const refreshCurrentSubtitleAfterKnownWordUpdate = (): void => {
|
const refreshCurrentSubtitleAfterKnownWordUpdate = (): void => {
|
||||||
|
const hasCurrentSubtitle = appState.currentSubText.trim().length > 0;
|
||||||
|
if (hasCurrentSubtitle) {
|
||||||
|
subtitlePrefetchService?.pause();
|
||||||
|
}
|
||||||
|
subtitleProcessingController.invalidateTokenizationCache();
|
||||||
|
subtitlePrefetchService?.onSeek(lastObservedTimePos);
|
||||||
subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
|
subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
|
||||||
};
|
};
|
||||||
let hasAttemptedImmersionTrackerStartup = false;
|
let hasAttemptedImmersionTrackerStartup = false;
|
||||||
@@ -4603,6 +4617,7 @@ const {
|
|||||||
},
|
},
|
||||||
onSubtitleChange: (text) => {
|
onSubtitleChange: (text) => {
|
||||||
subtitlePrefetchService?.pause();
|
subtitlePrefetchService?.pause();
|
||||||
|
subtitlePrefetchService?.onSeek(lastObservedTimePos);
|
||||||
subtitleProcessingController.onSubtitleChange(text);
|
subtitleProcessingController.onSubtitleChange(text);
|
||||||
},
|
},
|
||||||
refreshDiscordPresence: () => {
|
refreshDiscordPresence: () => {
|
||||||
|
|||||||
@@ -89,6 +89,65 @@ test('subtitle sidebar media path tag is assigned after prefetch succeeds', () =
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('subtitle change re-prioritizes prefetch around live playback before tokenizing current line', () => {
|
||||||
|
const source = readMainSource();
|
||||||
|
const actionBlock = source.match(
|
||||||
|
/onSubtitleChange:\s*\(text\)\s*=>\s*\{(?<body>[\s\S]*?)\n \},\n refreshDiscordPresence:/,
|
||||||
|
)?.groups?.body;
|
||||||
|
|
||||||
|
assert.ok(actionBlock);
|
||||||
|
assert.match(actionBlock, /subtitlePrefetchService\?\.pause\(\);/);
|
||||||
|
assert.match(actionBlock, /subtitlePrefetchService\?\.onSeek\(lastObservedTimePos\);/);
|
||||||
|
assert.match(actionBlock, /subtitleProcessingController\.onSubtitleChange\(text\);/);
|
||||||
|
assert.ok(
|
||||||
|
actionBlock.indexOf('subtitlePrefetchService?.pause();') <
|
||||||
|
actionBlock.indexOf('subtitlePrefetchService?.onSeek(lastObservedTimePos);'),
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
actionBlock.indexOf('subtitlePrefetchService?.onSeek(lastObservedTimePos);') <
|
||||||
|
actionBlock.indexOf('subtitleProcessingController.onSubtitleChange(text);'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('autoplay subtitle prime prefers cached annotated payload before raw fallback', () => {
|
||||||
|
const source = readMainSource();
|
||||||
|
const actionBlock = source.match(
|
||||||
|
/function emitAutoplayPrimedSubtitle\([\s\S]*?\): boolean \{(?<body>[\s\S]*?)\n\}/,
|
||||||
|
)?.groups?.body;
|
||||||
|
|
||||||
|
assert.ok(actionBlock);
|
||||||
|
assert.match(
|
||||||
|
actionBlock,
|
||||||
|
/const cachedPayload = subtitleProcessingController\.consumeCachedSubtitle\(text\);/,
|
||||||
|
);
|
||||||
|
assert.match(actionBlock, /if \(cachedPayload\) \{/);
|
||||||
|
assert.match(actionBlock, /emitSubtitlePayload\(cachedPayload\);/);
|
||||||
|
assert.match(actionBlock, /const rawPayload = withCurrentSubtitleTiming\(\{ text, tokens: null \}\);/);
|
||||||
|
assert.ok(
|
||||||
|
actionBlock.indexOf('consumeCachedSubtitle(text)') <
|
||||||
|
actionBlock.indexOf('withCurrentSubtitleTiming({ text, tokens: null })'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('known-word updates invalidate prefetched tokenizations before refreshing current subtitle', () => {
|
||||||
|
const source = readMainSource();
|
||||||
|
const actionBlock = source.match(
|
||||||
|
/const refreshCurrentSubtitleAfterKnownWordUpdate = \(\): void => \{(?<body>[\s\S]*?)\n\};/,
|
||||||
|
)?.groups?.body;
|
||||||
|
|
||||||
|
assert.ok(actionBlock);
|
||||||
|
assert.match(actionBlock, /subtitleProcessingController\.invalidateTokenizationCache\(\);/);
|
||||||
|
assert.match(actionBlock, /subtitlePrefetchService\?\.onSeek\(lastObservedTimePos\);/);
|
||||||
|
assert.match(
|
||||||
|
actionBlock,
|
||||||
|
/subtitleProcessingController\.refreshCurrentSubtitle\(appState\.currentSubText\);/,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
actionBlock.indexOf('subtitleProcessingController.invalidateTokenizationCache();') <
|
||||||
|
actionBlock.indexOf('subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('manual visible overlay changes notify mpv plugin visibility state', () => {
|
test('manual visible overlay changes notify mpv plugin visibility state', () => {
|
||||||
const source = readMainSource();
|
const source = readMainSource();
|
||||||
const setBlock = source.match(
|
const setBlock = source.match(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface SubtitlePrefetchInitControllerDeps {
|
|||||||
createSubtitlePrefetchService: (deps: SubtitlePrefetchServiceDeps) => SubtitlePrefetchService;
|
createSubtitlePrefetchService: (deps: SubtitlePrefetchServiceDeps) => SubtitlePrefetchService;
|
||||||
tokenizeSubtitle: (text: string) => Promise<SubtitleData | null>;
|
tokenizeSubtitle: (text: string) => Promise<SubtitleData | null>;
|
||||||
preCacheTokenization: (text: string, data: SubtitleData) => void;
|
preCacheTokenization: (text: string, data: SubtitleData) => void;
|
||||||
|
hasCachedTokenization?: (text: string) => boolean;
|
||||||
isCacheFull: () => boolean;
|
isCacheFull: () => boolean;
|
||||||
logInfo: (message: string) => void;
|
logInfo: (message: string) => void;
|
||||||
logWarn: (message: string) => void;
|
logWarn: (message: string) => void;
|
||||||
@@ -67,6 +68,7 @@ export function createSubtitlePrefetchInitController(
|
|||||||
cues,
|
cues,
|
||||||
tokenizeSubtitle: (text) => deps.tokenizeSubtitle(text),
|
tokenizeSubtitle: (text) => deps.tokenizeSubtitle(text),
|
||||||
preCacheTokenization: (text, data) => deps.preCacheTokenization(text, data),
|
preCacheTokenization: (text, data) => deps.preCacheTokenization(text, data),
|
||||||
|
hasCachedTokenization: (text) => deps.hasCachedTokenization?.(text) ?? false,
|
||||||
isCacheFull: () => deps.isCacheFull(),
|
isCacheFull: () => deps.isCacheFull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user