mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-02 06:22:42 -08:00
fix(osd): show subtitle-annotation loading status during tokenization
This commit is contained in:
@@ -87,6 +87,7 @@ export type MpvRuntimeComposerResult<
|
||||
tokenizeSubtitle: (text: string) => Promise<TTokenizedSubtitle>;
|
||||
createMecabTokenizerAndCheck: () => Promise<void>;
|
||||
prewarmSubtitleDictionaries: () => Promise<void>;
|
||||
startTokenizationWarmups: () => Promise<void>;
|
||||
launchBackgroundWarmupTask: ReturnType<typeof createLaunchBackgroundWarmupTaskFromStartup>;
|
||||
startBackgroundWarmups: ReturnType<typeof createStartBackgroundWarmupsFromStartup>;
|
||||
}>;
|
||||
@@ -132,12 +133,23 @@ export function composeMpvRuntimeHandlers<
|
||||
const prewarmSubtitleDictionaries = createPrewarmSubtitleDictionariesMainHandler(
|
||||
options.tokenizer.prewarmSubtitleDictionariesMainDeps,
|
||||
);
|
||||
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
|
||||
await options.warmups.startBackgroundWarmupsMainDeps.ensureYomitanExtensionLoaded();
|
||||
if (!options.tokenizer.createMecabTokenizerAndCheckMainDeps.getMecabTokenizer()) {
|
||||
await createMecabTokenizerAndCheck().catch(() => {});
|
||||
let tokenizationWarmupInFlight: Promise<void> | null = null;
|
||||
const startTokenizationWarmups = (): Promise<void> => {
|
||||
if (!tokenizationWarmupInFlight) {
|
||||
tokenizationWarmupInFlight = (async () => {
|
||||
await options.warmups.startBackgroundWarmupsMainDeps.ensureYomitanExtensionLoaded();
|
||||
if (!options.tokenizer.createMecabTokenizerAndCheckMainDeps.getMecabTokenizer()) {
|
||||
await createMecabTokenizerAndCheck().catch(() => {});
|
||||
}
|
||||
await prewarmSubtitleDictionaries({ showLoadingOsd: true });
|
||||
})().finally(() => {
|
||||
tokenizationWarmupInFlight = null;
|
||||
});
|
||||
}
|
||||
await prewarmSubtitleDictionaries();
|
||||
return tokenizationWarmupInFlight;
|
||||
};
|
||||
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
|
||||
await startTokenizationWarmups();
|
||||
return options.tokenizer.tokenizeSubtitle(
|
||||
text,
|
||||
options.tokenizer.createTokenizerRuntimeDeps(buildTokenizerDepsHandler()),
|
||||
@@ -165,6 +177,7 @@ export function composeMpvRuntimeHandlers<
|
||||
tokenizeSubtitle,
|
||||
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
||||
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
||||
startTokenizationWarmups,
|
||||
launchBackgroundWarmupTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
||||
startBackgroundWarmups: () => startBackgroundWarmups(),
|
||||
};
|
||||
|
||||
@@ -6,6 +6,17 @@ import {
|
||||
createPrewarmSubtitleDictionariesMainHandler,
|
||||
} from './subtitle-tokenization-main-deps';
|
||||
|
||||
function createDeferred(): {
|
||||
promise: Promise<void>;
|
||||
resolve: () => void;
|
||||
} {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>((nextResolve) => {
|
||||
resolve = nextResolve;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
test('tokenizer deps builder records known-word lookups and maps readers', () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildTokenizerDepsMainHandler({
|
||||
@@ -77,3 +88,93 @@ test('dictionary prewarm runs both dictionary loaders', async () => {
|
||||
await prewarm();
|
||||
assert.deepEqual(calls.sort(), ['freq', 'jlpt']);
|
||||
});
|
||||
|
||||
test('dictionary prewarm shows OSD spinner while loading and completion when loaded', async () => {
|
||||
const osdMessages: string[] = [];
|
||||
const clearedTimers: unknown[] = [];
|
||||
let tick: (() => void) | null = null;
|
||||
const jlptDeferred = createDeferred();
|
||||
const freqDeferred = createDeferred();
|
||||
|
||||
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
||||
ensureJlptDictionaryLookup: async () => jlptDeferred.promise,
|
||||
ensureFrequencyDictionaryLookup: async () => freqDeferred.promise,
|
||||
shouldShowOsdNotification: () => true,
|
||||
showMpvOsd: (message) => {
|
||||
osdMessages.push(message);
|
||||
},
|
||||
setInterval: (callback) => {
|
||||
tick = callback;
|
||||
return 1;
|
||||
},
|
||||
clearInterval: (timer) => {
|
||||
clearedTimers.push(timer);
|
||||
},
|
||||
});
|
||||
|
||||
const prewarmPromise = prewarm({ showLoadingOsd: true });
|
||||
assert.deepEqual(osdMessages, ['Loading subtitle annotations |']);
|
||||
|
||||
if (!tick) {
|
||||
throw new Error('expected loading spinner tick callback');
|
||||
}
|
||||
const tickCallback: () => void = tick;
|
||||
tickCallback();
|
||||
assert.deepEqual(osdMessages, ['Loading subtitle annotations |', 'Loading subtitle annotations /']);
|
||||
|
||||
jlptDeferred.resolve();
|
||||
freqDeferred.resolve();
|
||||
await prewarmPromise;
|
||||
|
||||
assert.deepEqual(osdMessages, [
|
||||
'Loading subtitle annotations |',
|
||||
'Loading subtitle annotations /',
|
||||
'Subtitle annotations loaded',
|
||||
]);
|
||||
assert.deepEqual(clearedTimers, [1]);
|
||||
});
|
||||
|
||||
test('dictionary prewarm can show OSD while awaiting background-started load', async () => {
|
||||
const osdMessages: string[] = [];
|
||||
const jlptDeferred = createDeferred();
|
||||
const freqDeferred = createDeferred();
|
||||
|
||||
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
||||
ensureJlptDictionaryLookup: async () => jlptDeferred.promise,
|
||||
ensureFrequencyDictionaryLookup: async () => freqDeferred.promise,
|
||||
shouldShowOsdNotification: () => true,
|
||||
showMpvOsd: (message) => {
|
||||
osdMessages.push(message);
|
||||
},
|
||||
setInterval: () => 1,
|
||||
clearInterval: () => undefined,
|
||||
});
|
||||
|
||||
const backgroundWarmupPromise = prewarm();
|
||||
const tokenizationWarmupPromise = prewarm({ showLoadingOsd: true });
|
||||
assert.deepEqual(osdMessages, ['Loading subtitle annotations |']);
|
||||
|
||||
jlptDeferred.resolve();
|
||||
freqDeferred.resolve();
|
||||
await backgroundWarmupPromise;
|
||||
await tokenizationWarmupPromise;
|
||||
|
||||
assert.deepEqual(osdMessages, ['Loading subtitle annotations |', 'Subtitle annotations loaded']);
|
||||
});
|
||||
|
||||
test('dictionary prewarm does not show OSD when notifications are disabled', async () => {
|
||||
const osdMessages: string[] = [];
|
||||
|
||||
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
||||
ensureJlptDictionaryLookup: async () => undefined,
|
||||
ensureFrequencyDictionaryLookup: async () => undefined,
|
||||
shouldShowOsdNotification: () => false,
|
||||
showMpvOsd: (message) => {
|
||||
osdMessages.push(message);
|
||||
},
|
||||
});
|
||||
|
||||
await prewarm({ showLoadingOsd: true });
|
||||
|
||||
assert.deepEqual(osdMessages, []);
|
||||
});
|
||||
|
||||
@@ -66,8 +66,88 @@ export function createCreateMecabTokenizerAndCheckMainHandler<TMecab>(deps: {
|
||||
export function createPrewarmSubtitleDictionariesMainHandler(deps: {
|
||||
ensureJlptDictionaryLookup: () => Promise<void>;
|
||||
ensureFrequencyDictionaryLookup: () => Promise<void>;
|
||||
showMpvOsd?: (message: string) => void;
|
||||
shouldShowOsdNotification?: () => boolean;
|
||||
setInterval?: (callback: () => void, delayMs: number) => unknown;
|
||||
clearInterval?: (timer: unknown) => void;
|
||||
}) {
|
||||
return async (): Promise<void> => {
|
||||
await Promise.all([deps.ensureJlptDictionaryLookup(), deps.ensureFrequencyDictionaryLookup()]);
|
||||
let prewarmed = false;
|
||||
let prewarmPromise: Promise<void> | null = null;
|
||||
let loadingOsdDepth = 0;
|
||||
let loadingOsdFrame = 0;
|
||||
let loadingOsdTimer: unknown = null;
|
||||
const showMpvOsd = deps.showMpvOsd;
|
||||
const shouldShowOsdNotification = deps.shouldShowOsdNotification ?? (() => false);
|
||||
const setIntervalHandler =
|
||||
deps.setInterval ??
|
||||
((callback: () => void, delayMs: number): unknown => setInterval(callback, delayMs));
|
||||
const clearIntervalHandler =
|
||||
deps.clearInterval ??
|
||||
((timer: unknown): void => clearInterval(timer as ReturnType<typeof setInterval>));
|
||||
const spinnerFrames = ['|', '/', '-', '\\'];
|
||||
|
||||
const beginLoadingOsd = (): boolean => {
|
||||
if (!showMpvOsd || !shouldShowOsdNotification()) {
|
||||
return false;
|
||||
}
|
||||
loadingOsdDepth += 1;
|
||||
if (loadingOsdDepth > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
loadingOsdFrame = 0;
|
||||
showMpvOsd(`Loading subtitle annotations ${spinnerFrames[loadingOsdFrame]}`);
|
||||
loadingOsdFrame += 1;
|
||||
loadingOsdTimer = setIntervalHandler(() => {
|
||||
if (!showMpvOsd) {
|
||||
return;
|
||||
}
|
||||
showMpvOsd(`Loading subtitle annotations ${spinnerFrames[loadingOsdFrame % spinnerFrames.length]}`);
|
||||
loadingOsdFrame += 1;
|
||||
}, 180);
|
||||
return true;
|
||||
};
|
||||
|
||||
const endLoadingOsd = (): void => {
|
||||
if (!showMpvOsd || !shouldShowOsdNotification()) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingOsdDepth = Math.max(0, loadingOsdDepth - 1);
|
||||
if (loadingOsdDepth > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadingOsdTimer) {
|
||||
clearIntervalHandler(loadingOsdTimer);
|
||||
loadingOsdTimer = null;
|
||||
}
|
||||
showMpvOsd('Subtitle annotations loaded');
|
||||
};
|
||||
|
||||
return async (options?: { showLoadingOsd?: boolean }): Promise<void> => {
|
||||
if (prewarmed) {
|
||||
return;
|
||||
}
|
||||
const shouldTrackLoadingOsd = options?.showLoadingOsd === true;
|
||||
const loadingOsdStarted = shouldTrackLoadingOsd ? beginLoadingOsd() : false;
|
||||
|
||||
if (!prewarmPromise) {
|
||||
prewarmPromise = (async () => {
|
||||
try {
|
||||
await Promise.all([deps.ensureJlptDictionaryLookup(), deps.ensureFrequencyDictionaryLookup()]);
|
||||
prewarmed = true;
|
||||
} finally {
|
||||
prewarmPromise = null;
|
||||
}
|
||||
})();
|
||||
}
|
||||
try {
|
||||
await prewarmPromise;
|
||||
} finally {
|
||||
if (loadingOsdStarted) {
|
||||
endLoadingOsd();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user