mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 18:22:41 -08:00
fix(osd): show subtitle-annotation loading status during tokenization
This commit is contained in:
@@ -2322,6 +2322,11 @@ const {
|
|||||||
ensureJlptDictionaryLookup: () => jlptDictionaryRuntime.ensureJlptDictionaryLookup(),
|
ensureJlptDictionaryLookup: () => jlptDictionaryRuntime.ensureJlptDictionaryLookup(),
|
||||||
ensureFrequencyDictionaryLookup: () =>
|
ensureFrequencyDictionaryLookup: () =>
|
||||||
frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(),
|
frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(),
|
||||||
|
showMpvOsd: (message: string) => showMpvOsd(message),
|
||||||
|
shouldShowOsdNotification: () => {
|
||||||
|
const type = getResolvedConfig().ankiConnect.behavior.notificationType;
|
||||||
|
return type === 'osd' || type === 'both';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
warmups: {
|
warmups: {
|
||||||
@@ -2366,6 +2371,7 @@ const {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
startJellyfinRemoteSession: () => startJellyfinRemoteSession(),
|
startJellyfinRemoteSession: () => startJellyfinRemoteSession(),
|
||||||
|
logDebug: (message) => logger.debug(message),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export type MpvRuntimeComposerResult<
|
|||||||
tokenizeSubtitle: (text: string) => Promise<TTokenizedSubtitle>;
|
tokenizeSubtitle: (text: string) => Promise<TTokenizedSubtitle>;
|
||||||
createMecabTokenizerAndCheck: () => Promise<void>;
|
createMecabTokenizerAndCheck: () => Promise<void>;
|
||||||
prewarmSubtitleDictionaries: () => Promise<void>;
|
prewarmSubtitleDictionaries: () => Promise<void>;
|
||||||
|
startTokenizationWarmups: () => Promise<void>;
|
||||||
launchBackgroundWarmupTask: ReturnType<typeof createLaunchBackgroundWarmupTaskFromStartup>;
|
launchBackgroundWarmupTask: ReturnType<typeof createLaunchBackgroundWarmupTaskFromStartup>;
|
||||||
startBackgroundWarmups: ReturnType<typeof createStartBackgroundWarmupsFromStartup>;
|
startBackgroundWarmups: ReturnType<typeof createStartBackgroundWarmupsFromStartup>;
|
||||||
}>;
|
}>;
|
||||||
@@ -132,12 +133,23 @@ export function composeMpvRuntimeHandlers<
|
|||||||
const prewarmSubtitleDictionaries = createPrewarmSubtitleDictionariesMainHandler(
|
const prewarmSubtitleDictionaries = createPrewarmSubtitleDictionariesMainHandler(
|
||||||
options.tokenizer.prewarmSubtitleDictionariesMainDeps,
|
options.tokenizer.prewarmSubtitleDictionariesMainDeps,
|
||||||
);
|
);
|
||||||
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
|
let tokenizationWarmupInFlight: Promise<void> | null = null;
|
||||||
|
const startTokenizationWarmups = (): Promise<void> => {
|
||||||
|
if (!tokenizationWarmupInFlight) {
|
||||||
|
tokenizationWarmupInFlight = (async () => {
|
||||||
await options.warmups.startBackgroundWarmupsMainDeps.ensureYomitanExtensionLoaded();
|
await options.warmups.startBackgroundWarmupsMainDeps.ensureYomitanExtensionLoaded();
|
||||||
if (!options.tokenizer.createMecabTokenizerAndCheckMainDeps.getMecabTokenizer()) {
|
if (!options.tokenizer.createMecabTokenizerAndCheckMainDeps.getMecabTokenizer()) {
|
||||||
await createMecabTokenizerAndCheck().catch(() => {});
|
await createMecabTokenizerAndCheck().catch(() => {});
|
||||||
}
|
}
|
||||||
await prewarmSubtitleDictionaries();
|
await prewarmSubtitleDictionaries({ showLoadingOsd: true });
|
||||||
|
})().finally(() => {
|
||||||
|
tokenizationWarmupInFlight = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return tokenizationWarmupInFlight;
|
||||||
|
};
|
||||||
|
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
|
||||||
|
await startTokenizationWarmups();
|
||||||
return options.tokenizer.tokenizeSubtitle(
|
return options.tokenizer.tokenizeSubtitle(
|
||||||
text,
|
text,
|
||||||
options.tokenizer.createTokenizerRuntimeDeps(buildTokenizerDepsHandler()),
|
options.tokenizer.createTokenizerRuntimeDeps(buildTokenizerDepsHandler()),
|
||||||
@@ -165,6 +177,7 @@ export function composeMpvRuntimeHandlers<
|
|||||||
tokenizeSubtitle,
|
tokenizeSubtitle,
|
||||||
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
||||||
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
||||||
|
startTokenizationWarmups,
|
||||||
launchBackgroundWarmupTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
launchBackgroundWarmupTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
||||||
startBackgroundWarmups: () => startBackgroundWarmups(),
|
startBackgroundWarmups: () => startBackgroundWarmups(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ import {
|
|||||||
createPrewarmSubtitleDictionariesMainHandler,
|
createPrewarmSubtitleDictionariesMainHandler,
|
||||||
} from './subtitle-tokenization-main-deps';
|
} 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', () => {
|
test('tokenizer deps builder records known-word lookups and maps readers', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const deps = createBuildTokenizerDepsMainHandler({
|
const deps = createBuildTokenizerDepsMainHandler({
|
||||||
@@ -77,3 +88,93 @@ test('dictionary prewarm runs both dictionary loaders', async () => {
|
|||||||
await prewarm();
|
await prewarm();
|
||||||
assert.deepEqual(calls.sort(), ['freq', 'jlpt']);
|
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: {
|
export function createPrewarmSubtitleDictionariesMainHandler(deps: {
|
||||||
ensureJlptDictionaryLookup: () => Promise<void>;
|
ensureJlptDictionaryLookup: () => Promise<void>;
|
||||||
ensureFrequencyDictionaryLookup: () => 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> => {
|
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()]);
|
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