mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
Add N1 word highlighting flow and mpv/overlay service updates
This commit is contained in:
@@ -13,6 +13,7 @@ test("createFieldGroupingOverlayRuntimeService sends overlay messages and sets r
|
||||
getMainWindow: () => ({
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
isLoading: () => false,
|
||||
send: (...args: unknown[]) => {
|
||||
sent.push(args);
|
||||
},
|
||||
|
||||
@@ -92,13 +92,16 @@ function createDeps(overrides: Partial<MpvProtocolHandleMessageDeps> = {}): {
|
||||
state.commands.push(payload);
|
||||
return true;
|
||||
},
|
||||
restorePreviousSecondarySubVisibility: () => {
|
||||
state.restored += 1;
|
||||
restorePreviousSecondarySubVisibility: () => {
|
||||
state.restored += 1;
|
||||
},
|
||||
setPreviousSecondarySubVisibility: () => {
|
||||
// intentionally not tracked in this unit test
|
||||
},
|
||||
...overrides,
|
||||
},
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test("dispatchMpvProtocolMessage emits subtitle text on property change", async () => {
|
||||
const { deps, state } = createDeps();
|
||||
|
||||
@@ -75,6 +75,7 @@ export interface MpvProtocolHandleMessageDeps {
|
||||
autoLoadSecondarySubTrack: () => void;
|
||||
setCurrentVideoPath: (value: string) => void;
|
||||
emitSecondarySubtitleVisibility: (payload: { visible: boolean }) => void;
|
||||
setPreviousSecondarySubVisibility: (visible: boolean) => void;
|
||||
setCurrentAudioStreamIndex: (
|
||||
tracks: Array<{
|
||||
type?: string;
|
||||
@@ -300,6 +301,7 @@ export async function dispatchMpvProtocolMessage(
|
||||
} else if (msg.request_id === MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY) {
|
||||
const previous = parseVisibilityProperty(msg.data);
|
||||
if (previous !== null) {
|
||||
deps.setPreviousSecondarySubVisibility(previous);
|
||||
deps.emitSecondarySubtitleVisibility({ visible: previous });
|
||||
}
|
||||
deps.setSecondarySubVisibility(false);
|
||||
|
||||
@@ -319,7 +319,7 @@ test("MpvIpcClient restorePreviousSecondarySubVisibility restores and clears tra
|
||||
command: ["set_property", "secondary-sub-visibility", "no"],
|
||||
},
|
||||
{
|
||||
command: ["set_property", "secondary-sub-visibility", "no"],
|
||||
command: ["set_property", "secondary-sub-visibility", "yes"],
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -293,6 +293,9 @@ export class MpvIpcClient implements MpvClient {
|
||||
emitSecondarySubtitleVisibility: (payload) => {
|
||||
this.emit("secondary-subtitle-visibility", payload);
|
||||
},
|
||||
setPreviousSecondarySubVisibility: (visible: boolean) => {
|
||||
this.previousSecondarySubVisibility = visible;
|
||||
},
|
||||
setCurrentAudioStreamIndex: (tracks) => {
|
||||
this.updateCurrentAudioStreamIndex(tracks);
|
||||
},
|
||||
|
||||
@@ -12,14 +12,15 @@ test("sendToVisibleOverlayRuntimeService restores visibility flag when opening h
|
||||
let visibleOverlayVisible = false;
|
||||
|
||||
const ok = sendToVisibleOverlayRuntimeService({
|
||||
mainWindow: {
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
send: (...args: unknown[]) => {
|
||||
sent.push(args);
|
||||
mainWindow: {
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
isLoading: () => false,
|
||||
send: (...args: unknown[]) => {
|
||||
sent.push(args);
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as Electron.BrowserWindow,
|
||||
} as unknown as Electron.BrowserWindow,
|
||||
visibleOverlayVisible,
|
||||
setVisibleOverlayVisible: (visible) => {
|
||||
visibleOverlayVisible = visible;
|
||||
|
||||
@@ -301,11 +301,11 @@ test("runSubsyncManualService constructs alass command and returns failure on no
|
||||
test("runSubsyncManualService resolves string sid values from mpv stream properties", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-stream-sid-"));
|
||||
const ffsubsyncPath = path.join(tmpDir, "ffsubsync.sh");
|
||||
const ffsubsyncLogPath = path.join(tmpDir, "ffsubsync-args.log");
|
||||
const ffmpegPath = path.join(tmpDir, "ffmpeg.sh");
|
||||
const alassPath = path.join(tmpDir, "alass.sh");
|
||||
const videoPath = path.join(tmpDir, "video.mkv");
|
||||
const primaryPath = path.join(tmpDir, "primary.srt");
|
||||
const syncOutputPath = path.join(tmpDir, "synced.srt");
|
||||
|
||||
fs.writeFileSync(videoPath, "video");
|
||||
fs.writeFileSync(primaryPath, "subtitle");
|
||||
@@ -313,7 +313,7 @@ test("runSubsyncManualService resolves string sid values from mpv stream propert
|
||||
writeExecutableScript(alassPath, "#!/bin/sh\nexit 0\n");
|
||||
writeExecutableScript(
|
||||
ffsubsyncPath,
|
||||
`#!/bin/sh\nmkdir -p "${tmpDir}"\nprev=""; for arg in "$@"; do if [ "$prev" = "--reference-stream" ]; then :; fi; if [ "$prev" = "-o" ]; then echo "$arg" > "${syncOutputPath}"; fi; prev="$arg"; done`,
|
||||
`#!/bin/sh\n: > "${ffsubsyncLogPath}"\nfor arg in "$@"; do\n printf '%s\\n' "$arg" >> "${ffsubsyncLogPath}"\ndone\nprev=""\nfor arg in "$@"; do\n if [ "$prev" = "-o" ]; then\n : > "$arg"\n fi\n prev="$arg"\ndone`,
|
||||
);
|
||||
|
||||
const deps = makeDeps({
|
||||
@@ -354,5 +354,9 @@ test("runSubsyncManualService resolves string sid values from mpv stream propert
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.message, "Subtitle synchronized with ffsubsync");
|
||||
assert.equal(fs.readFileSync(syncOutputPath, "utf8"), "");
|
||||
const ffsubsyncArgs = fs.readFileSync(ffsubsyncLogPath, "utf8").trim().split("\n");
|
||||
const outputIndex = ffsubsyncArgs.findIndex((value) => value === "-o");
|
||||
assert.ok(outputIndex >= 0);
|
||||
const outputPath = ffsubsyncArgs[outputIndex + 1];
|
||||
assert.equal(fs.readFileSync(outputPath, "utf8"), "");
|
||||
});
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { PartOfSpeech } from "../../types";
|
||||
import { tokenizeSubtitleService, TokenizerServiceDeps } from "./tokenizer-service";
|
||||
import {
|
||||
createTokenizerDepsRuntimeService,
|
||||
TokenizerServiceDeps,
|
||||
TokenizerDepsRuntimeOptions,
|
||||
tokenizeSubtitleService,
|
||||
} from "./tokenizer-service";
|
||||
|
||||
function makeDeps(
|
||||
overrides: Partial<TokenizerServiceDeps> = {},
|
||||
@@ -21,6 +26,27 @@ function makeDeps(
|
||||
};
|
||||
}
|
||||
|
||||
function makeDepsFromMecabTokenizer(
|
||||
tokenize: (text: string) => Promise<import("../../types").Token[] | null>,
|
||||
overrides: Partial<TokenizerDepsRuntimeOptions> = {},
|
||||
): TokenizerServiceDeps {
|
||||
return createTokenizerDepsRuntimeService({
|
||||
getYomitanExt: () => null,
|
||||
getYomitanParserWindow: () => null,
|
||||
setYomitanParserWindow: () => {},
|
||||
getYomitanParserReadyPromise: () => null,
|
||||
setYomitanParserReadyPromise: () => {},
|
||||
getYomitanParserInitPromise: () => null,
|
||||
setYomitanParserInitPromise: () => {},
|
||||
isKnownWord: () => false,
|
||||
getKnownWordMatchMode: () => "headword",
|
||||
getMecabTokenizer: () => ({
|
||||
tokenize,
|
||||
}),
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test("tokenizeSubtitleService returns null tokens for empty normalized text", async () => {
|
||||
const result = await tokenizeSubtitleService(" \\n ", makeDeps());
|
||||
assert.deepEqual(result, { text: " \\n ", tokens: null });
|
||||
@@ -136,20 +162,22 @@ test("tokenizeSubtitleService uses Yomitan parser result when available", async
|
||||
test("tokenizeSubtitleService marks tokens as known using callback", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
makeDepsFromMecabTokenizer(async () => [
|
||||
{
|
||||
word: "猫",
|
||||
partOfSpeech: PartOfSpeech.noun,
|
||||
pos1: "",
|
||||
pos2: "",
|
||||
pos3: "",
|
||||
pos4: "",
|
||||
inflectionType: "",
|
||||
inflectionForm: "",
|
||||
headword: "猫",
|
||||
katakanaReading: "ネコ",
|
||||
pronunciation: "ネコ",
|
||||
},
|
||||
], {
|
||||
isKnownWord: (text) => text === "猫",
|
||||
tokenizeWithMecab: async () => [
|
||||
{
|
||||
surface: "猫",
|
||||
reading: "ネコ",
|
||||
headword: "猫",
|
||||
startPos: 0,
|
||||
endPos: 1,
|
||||
partOfSpeech: PartOfSpeech.noun,
|
||||
isMerged: false,
|
||||
isKnown: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -160,20 +188,22 @@ test("tokenizeSubtitleService marks tokens as known using callback", async () =>
|
||||
test("tokenizeSubtitleService checks known words by headword, not surface", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
makeDepsFromMecabTokenizer(async () => [
|
||||
{
|
||||
word: "猫",
|
||||
partOfSpeech: PartOfSpeech.noun,
|
||||
pos1: "",
|
||||
pos2: "",
|
||||
pos3: "",
|
||||
pos4: "",
|
||||
inflectionType: "",
|
||||
inflectionForm: "",
|
||||
headword: "猫です",
|
||||
katakanaReading: "ネコ",
|
||||
pronunciation: "ネコ",
|
||||
},
|
||||
], {
|
||||
isKnownWord: (text) => text === "猫です",
|
||||
tokenizeWithMecab: async () => [
|
||||
{
|
||||
surface: "猫",
|
||||
reading: "ネコ",
|
||||
headword: "猫です",
|
||||
startPos: 0,
|
||||
endPos: 1,
|
||||
partOfSpeech: PartOfSpeech.noun,
|
||||
isMerged: false,
|
||||
isKnown: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -184,21 +214,23 @@ test("tokenizeSubtitleService checks known words by headword, not surface", asyn
|
||||
test("tokenizeSubtitleService checks known words by surface when configured", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
makeDepsFromMecabTokenizer(async () => [
|
||||
{
|
||||
word: "猫",
|
||||
partOfSpeech: PartOfSpeech.noun,
|
||||
pos1: "",
|
||||
pos2: "",
|
||||
pos3: "",
|
||||
pos4: "",
|
||||
inflectionType: "",
|
||||
inflectionForm: "",
|
||||
headword: "猫です",
|
||||
katakanaReading: "ネコ",
|
||||
pronunciation: "ネコ",
|
||||
},
|
||||
], {
|
||||
getKnownWordMatchMode: () => "surface",
|
||||
isKnownWord: (text) => text === "猫",
|
||||
tokenizeWithMecab: async () => [
|
||||
{
|
||||
surface: "猫",
|
||||
reading: "ネコ",
|
||||
headword: "猫です",
|
||||
startPos: 0,
|
||||
endPos: 1,
|
||||
partOfSpeech: PartOfSpeech.noun,
|
||||
isMerged: false,
|
||||
isKnown: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user