chore(scripts): align tooling with runtime/service updates

This commit is contained in:
2026-02-17 19:00:29 -08:00
parent 1233e3630f
commit 817a949f99
3 changed files with 68 additions and 15 deletions

View File

@@ -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:

View File

@@ -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<T>(
promise: Promise<T>,
timeoutMs: number,
label: string,
): Promise<T> {
return new Promise<T>((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<void> {
)
: 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<void> {
}),
});
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<void> {
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<void> {
sourceHint:
tokenCount === 0
? "none"
: hasYomitan ? "yomitan-merged" : "mecab-merge",
: useYomitan ? "yomitan-merged" : "mecab-merge",
mergedTokenCount: mergedCount,
totalTokenCount: tokenCount,
},

View File

@@ -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<boolean> | 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<voi
function parseCliArgs(argv: string[]): CliOptions {
const args = [...argv];
const inputParts: string[] = [];
let emitPretty = false;
let emitPretty = true;
let emitJson = false;
let forceMecabOnly = false;
let yomitanExtensionPath: string | undefined;
let yomitanUserDataPath: string | undefined;
let yomitanUserDataPath: string | undefined = DEFAULT_YOMITAN_USER_DATA_PATH;
let mecabCommand: string | undefined;
let mecabDictionaryPath: string | undefined;
@@ -212,7 +219,7 @@ function printUsage(): void {
--json Emit machine-readable JSON output.
--force-mecab Skip Yomitan parser setup and test MeCab fallback only.
--yomitan-extension <path> Optional path to Yomitan extension directory.
--yomitan-user-data <path> Optional Electron userData directory.
--yomitan-user-data <path> Optional Electron userData directory (default: ~/.config/SubMiner).
--mecab-command <path> Optional MeCab binary path (default: mecab).
--mecab-dictionary <path> Optional MeCab dictionary directory.
-h, --help Show usage.