diff --git a/Makefile b/Makefile index 4c3469c..f5ac372 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ build-macos-unsigned: deps build-launcher: @printf '%s\n' "[INFO] Bundling launcher script" @bun build ./launcher/main.ts --target=bun --packages=bundle --outfile=subminer - @sed -i '1s|^// @bun|#!/usr/bin/env bun\n// @bun|' subminer + @python3 -c 'from pathlib import Path; p=Path("subminer"); c=p.read_text(); c=("#!/usr/bin/env bun\n"+c) if not c.startswith("#!/usr/bin/env bun\n") else c; p.write_text(c)' @chmod +x subminer clean: diff --git a/scripts/get_frequency.ts b/scripts/get_frequency.ts index 89345a8..f29d5db 100644 --- a/scripts/get_frequency.ts +++ b/scripts/get_frequency.ts @@ -3,7 +3,7 @@ import path from "node:path"; import process from "node:process"; import { createTokenizerDepsRuntime, tokenizeSubtitle } from "../src/core/services/tokenizer.js"; -import { createFrequencyDictionaryLookup } from "../src/core/services/index.js"; +import { createFrequencyDictionaryLookup } from "../src/core/services/frequency-dictionary.js"; import { MecabTokenizer } from "../src/mecab-tokenizer.js"; import type { MergedToken, FrequencyDictionaryLookup } from "../src/types.js"; @@ -496,6 +496,27 @@ interface YomitanRuntimeState { note?: string; } +function withTimeout( + promise: Promise, + timeoutMs: number, + label: string, +): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`${label} timed out after ${timeoutMs}ms`)); + }, timeoutMs); + promise + .then((value) => { + clearTimeout(timer); + resolve(value); + }) + .catch((error) => { + clearTimeout(timer); + reject(error); + }); + }); +} + function destroyUnknownParserWindow(window: unknown): void { if (!window || typeof window !== "object") { return; @@ -785,30 +806,31 @@ async function main(): Promise { ) : null; const hasYomitan = Boolean(yomitanState?.available && yomitanState?.yomitanExt); + let useYomitan = hasYomitan; const deps = createTokenizerDepsRuntime({ getYomitanExt: () => - (hasYomitan ? yomitanState!.yomitanExt : null) as never, + (useYomitan ? yomitanState!.yomitanExt : null) as never, getYomitanParserWindow: () => - (hasYomitan ? yomitanState!.parserWindow : null) as never, + (useYomitan ? yomitanState!.parserWindow : null) as never, setYomitanParserWindow: (window) => { - if (!hasYomitan) { + if (!useYomitan) { return; } yomitanState!.parserWindow = window; }, getYomitanParserReadyPromise: () => - (hasYomitan ? yomitanState!.parserReadyPromise : null) as never, + (useYomitan ? yomitanState!.parserReadyPromise : null) as never, setYomitanParserReadyPromise: (promise) => { - if (!hasYomitan) { + if (!useYomitan) { return; } yomitanState!.parserReadyPromise = promise; }, getYomitanParserInitPromise: () => - (hasYomitan ? yomitanState!.parserInitPromise : null) as never, + (useYomitan ? yomitanState!.parserInitPromise : null) as never, setYomitanParserInitPromise: (promise) => { - if (!hasYomitan) { + if (!useYomitan) { return; } yomitanState!.parserInitPromise = promise; @@ -823,7 +845,31 @@ async function main(): Promise { }), }); - const subtitleData = await tokenizeSubtitle(args.input, deps); + let subtitleData; + if (useYomitan) { + try { + subtitleData = await withTimeout( + tokenizeSubtitle(args.input, deps), + 8000, + "Yomitan tokenizer", + ); + } catch (error) { + useYomitan = false; + destroyUnknownParserWindow(yomitanState?.parserWindow ?? null); + if (yomitanState) { + yomitanState.parserWindow = null; + yomitanState.parserReadyPromise = null; + yomitanState.parserInitPromise = null; + const fallbackNote = error instanceof Error ? error.message : "Yomitan tokenizer timed out"; + yomitanState.note = yomitanState.note + ? `${yomitanState.note}; ${fallbackNote}` + : fallbackNote; + } + subtitleData = await tokenizeSubtitle(args.input, deps); + } + } else { + subtitleData = await tokenizeSubtitle(args.input, deps); + } const tokenCount = subtitleData.tokens?.length ?? 0; const mergedCount = subtitleData.tokens?.filter((token) => token.isMerged).length ?? 0; const tokens = @@ -835,7 +881,7 @@ async function main(): Promise { const diagnostics = { yomitan: { available: Boolean(yomitanState?.available), - loaded: hasYomitan, + loaded: useYomitan, forceMecabOnly: args.forceMecabOnly, note: yomitanState?.note ?? null, }, @@ -848,7 +894,7 @@ async function main(): Promise { sourceHint: tokenCount === 0 ? "none" - : hasYomitan ? "yomitan-merged" : "mecab-merge", + : useYomitan ? "yomitan-merged" : "mecab-merge", mergedTokenCount: mergedCount, totalTokenCount: tokenCount, }, diff --git a/scripts/test-yomitan-parser.ts b/scripts/test-yomitan-parser.ts index ae6dce3..8ecd651 100644 --- a/scripts/test-yomitan-parser.ts +++ b/scripts/test-yomitan-parser.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import process from "node:process"; @@ -54,6 +55,12 @@ interface YomitanRuntimeState { parserInitPromise: Promise | null; } +const DEFAULT_YOMITAN_USER_DATA_PATH = path.join( + os.homedir(), + ".config", + "SubMiner", +); + function destroyParserWindow(window: Electron.BrowserWindow | null): void { if (!window || window.isDestroyed()) { return; @@ -72,11 +79,11 @@ async function shutdownYomitanRuntime(yomitan: YomitanRuntimeState): Promise Optional path to Yomitan extension directory. - --yomitan-user-data Optional Electron userData directory. + --yomitan-user-data Optional Electron userData directory (default: ~/.config/SubMiner). --mecab-command Optional MeCab binary path (default: mecab). --mecab-dictionary Optional MeCab dictionary directory. -h, --help Show usage.