Add opt-in JLPT tagging flow

This commit is contained in:
2026-02-15 16:28:00 -08:00
parent ca2b7bb2fe
commit f492622a8b
27 changed files with 1116 additions and 38 deletions

View File

@@ -95,6 +95,7 @@ import {
createOverlayContentMeasurementStoreService,
createOverlayWindowService,
createTokenizerDepsRuntimeService,
createJlptVocabularyLookupService,
cycleSecondarySubModeService,
enforceOverlayLayerOrderService,
ensureOverlayWindowLevelService,
@@ -227,6 +228,8 @@ const isDev =
process.argv.includes("--dev") || process.argv.includes("--debug");
const texthookerService = new TexthookerService();
const subtitleWsService = new SubtitleWebSocketService();
let jlptDictionaryLookupInitialized = false;
let jlptDictionaryLookupInitialization: Promise<void> | null = null;
const appLogger = {
logInfo: (message: string) => {
console.log(message);
@@ -464,6 +467,139 @@ function loadSubtitlePosition(): SubtitlePosition | null {
return appState.subtitlePosition;
}
function getJlptDictionarySearchPaths(): string[] {
const homeDir = os.homedir();
const userDataPath = app.getPath("userData");
return [
path.join(__dirname, "..", "..", "vendor", "yomitan-jlpt-vocab"),
path.join(
__dirname,
"..",
"..",
"vendor",
"yomitan-jlpt-vocab",
"yomitan-jlpt-vocab",
),
path.join(__dirname, "..", "..", "..", "vendor", "yomitan-jlpt-vocab"),
path.join(
__dirname,
"..",
"..",
"..",
"vendor",
"yomitan-jlpt-vocab",
"yomitan-jlpt-vocab",
),
path.join(process.resourcesPath, "yomitan-jlpt-vocab"),
path.join(
process.resourcesPath,
"yomitan-jlpt-vocab",
"yomitan-jlpt-vocab",
),
path.join(app.getAppPath(), "vendor", "yomitan-jlpt-vocab"),
path.join(
app.getAppPath(),
"vendor",
"yomitan-jlpt-vocab",
"yomitan-jlpt-vocab",
),
path.join(process.resourcesPath, "app.asar", "vendor", "yomitan-jlpt-vocab"),
path.join(
process.resourcesPath,
"app.asar",
"vendor",
"yomitan-jlpt-vocab",
"yomitan-jlpt-vocab",
),
path.join(USER_DATA_PATH, "yomitan-jlpt-vocab"),
path.join(USER_DATA_PATH, "yomitan-jlpt-vocab", "yomitan-jlpt-vocab"),
path.join(userDataPath, "yomitan-jlpt-vocab"),
path.join(userDataPath, "yomitan-jlpt-vocab", "yomitan-jlpt-vocab"),
path.join(homeDir, ".config", "SubMiner", "yomitan-jlpt-vocab"),
path.join(
homeDir,
".config",
"SubMiner",
"yomitan-jlpt-vocab",
"yomitan-jlpt-vocab",
),
path.join(homeDir, ".config", "subminer", "yomitan-jlpt-vocab"),
path.join(
homeDir,
".config",
"subminer",
"yomitan-jlpt-vocab",
"yomitan-jlpt-vocab",
),
path.join(
homeDir,
"Library",
"Application Support",
"SubMiner",
"yomitan-jlpt-vocab",
),
path.join(
homeDir,
"Library",
"Application Support",
"SubMiner",
"yomitan-jlpt-vocab",
"yomitan-jlpt-vocab",
),
path.join(
homeDir,
"Library",
"Application Support",
"subminer",
"yomitan-jlpt-vocab",
),
path.join(
homeDir,
"Library",
"Application Support",
"subminer",
"yomitan-jlpt-vocab",
"yomitan-jlpt-vocab",
),
path.join(process.cwd(), "vendor", "yomitan-jlpt-vocab"),
path.join(
process.cwd(),
"vendor",
"yomitan-jlpt-vocab",
"yomitan-jlpt-vocab",
),
];
}
async function initializeJlptDictionaryLookup(): Promise<void> {
appState.jlptLevelLookup = await createJlptVocabularyLookupService({
searchPaths: getJlptDictionarySearchPaths(),
log: (message) => {
console.log(`[JLPT] ${message}`);
},
});
}
async function ensureJlptDictionaryLookup(): Promise<void> {
if (!getResolvedConfig().subtitleStyle.enableJlpt) {
return;
}
if (jlptDictionaryLookupInitialized) {
return;
}
if (!jlptDictionaryLookupInitialization) {
jlptDictionaryLookupInitialization = initializeJlptDictionaryLookup()
.then(() => {
jlptDictionaryLookupInitialized = true;
})
.catch((error) => {
jlptDictionaryLookupInitialization = null;
throw error;
});
}
await jlptDictionaryLookupInitialization;
}
function saveSubtitlePosition(position: SubtitlePosition): void {
appState.subtitlePosition = position;
saveSubtitlePositionService({
@@ -804,6 +940,7 @@ function updateMpvSubtitleRenderMetrics(
}
async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
await ensureJlptDictionaryLookup();
return tokenizeSubtitleService(
text,
createTokenizerDepsRuntimeService({
@@ -825,6 +962,9 @@ async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
getKnownWordMatchMode: () =>
appState.ankiIntegration?.getKnownWordMatchMode() ??
getResolvedConfig().ankiConnect.nPlusOne.matchMode,
getJlptLevel: (text) => appState.jlptLevelLookup(text),
getJlptEnabled: () =>
getResolvedConfig().subtitleStyle.enableJlpt,
getMecabTokenizer: () => appState.mecabTokenizer,
}),
);
@@ -1345,6 +1485,7 @@ registerIpcRuntimeServices({
...resolvedConfig.subtitleStyle,
nPlusOneColor: resolvedConfig.ankiConnect.nPlusOne.nPlusOne,
knownWordColor: resolvedConfig.ankiConnect.nPlusOne.knownWord,
enableJlpt: resolvedConfig.subtitleStyle.enableJlpt,
};
},
saveSubtitlePosition: (position: unknown) =>