mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-23 00:11:28 -07:00
400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import { CardCreationService } from './card-creation';
|
|
import type { AnkiConnectConfig } from '../types';
|
|
|
|
test('CardCreationService counts locally created sentence cards', async () => {
|
|
const minedCards: Array<{ count: number; noteIds?: number[] }> = [];
|
|
const service = new CardCreationService({
|
|
getConfig: () =>
|
|
({
|
|
deck: 'Mining',
|
|
fields: {
|
|
sentence: 'Sentence',
|
|
audio: 'SentenceAudio',
|
|
},
|
|
media: {
|
|
generateAudio: false,
|
|
generateImage: false,
|
|
},
|
|
behavior: {},
|
|
ai: false,
|
|
}) as AnkiConnectConfig,
|
|
getAiConfig: () => ({}),
|
|
getTimingTracker: () => ({}) as never,
|
|
getMpvClient: () =>
|
|
({
|
|
currentVideoPath: '/video.mp4',
|
|
currentSubText: '字幕',
|
|
currentSubStart: 1,
|
|
currentSubEnd: 2,
|
|
currentTimePos: 1.5,
|
|
currentAudioStreamIndex: 0,
|
|
}) as never,
|
|
client: {
|
|
addNote: async () => 42,
|
|
addTags: async () => undefined,
|
|
notesInfo: async () => [],
|
|
updateNoteFields: async () => undefined,
|
|
storeMediaFile: async () => undefined,
|
|
findNotes: async () => [],
|
|
retrieveMediaFile: async () => '',
|
|
},
|
|
mediaGenerator: {
|
|
generateAudio: async () => null,
|
|
generateScreenshot: async () => null,
|
|
generateAnimatedImage: async () => null,
|
|
},
|
|
showOsdNotification: () => undefined,
|
|
showUpdateResult: () => undefined,
|
|
showStatusNotification: () => undefined,
|
|
showNotification: async () => undefined,
|
|
beginUpdateProgress: () => undefined,
|
|
endUpdateProgress: () => undefined,
|
|
withUpdateProgress: async (_message, action) => action(),
|
|
resolveConfiguredFieldName: () => null,
|
|
resolveNoteFieldName: () => null,
|
|
getAnimatedImageLeadInSeconds: async () => 0,
|
|
extractFields: () => ({}),
|
|
processSentence: (sentence) => sentence,
|
|
setCardTypeFields: () => undefined,
|
|
mergeFieldValue: (_existing, newValue) => newValue,
|
|
formatMiscInfoPattern: () => '',
|
|
getEffectiveSentenceCardConfig: () => ({
|
|
model: 'Sentence',
|
|
sentenceField: 'Sentence',
|
|
audioField: 'SentenceAudio',
|
|
lapisEnabled: false,
|
|
kikuEnabled: false,
|
|
kikuFieldGrouping: 'disabled',
|
|
kikuDeleteDuplicateInAuto: false,
|
|
}),
|
|
getFallbackDurationSeconds: () => 10,
|
|
appendKnownWordsFromNoteInfo: () => undefined,
|
|
isUpdateInProgress: () => false,
|
|
setUpdateInProgress: () => undefined,
|
|
trackLastAddedNoteId: () => undefined,
|
|
recordCardsMinedCallback: (count, noteIds) => {
|
|
minedCards.push({ count, noteIds });
|
|
},
|
|
});
|
|
|
|
const created = await service.createSentenceCard('テスト', 0, 1);
|
|
|
|
assert.equal(created, true);
|
|
assert.deepEqual(minedCards, [{ count: 1, noteIds: [42] }]);
|
|
});
|
|
|
|
test('CardCreationService keeps updating after trackLastAddedNoteId throws', async () => {
|
|
const calls = {
|
|
notesInfo: 0,
|
|
updateNoteFields: 0,
|
|
};
|
|
const service = new CardCreationService({
|
|
getConfig: () =>
|
|
({
|
|
deck: 'Mining',
|
|
fields: {
|
|
sentence: 'Sentence',
|
|
audio: 'SentenceAudio',
|
|
},
|
|
media: {
|
|
generateAudio: false,
|
|
generateImage: false,
|
|
},
|
|
behavior: {},
|
|
ai: false,
|
|
}) as AnkiConnectConfig,
|
|
getAiConfig: () => ({}),
|
|
getTimingTracker: () => ({}) as never,
|
|
getMpvClient: () =>
|
|
({
|
|
currentVideoPath: '/video.mp4',
|
|
currentSubText: '字幕',
|
|
currentSubStart: 1,
|
|
currentSubEnd: 2,
|
|
currentTimePos: 1.5,
|
|
currentAudioStreamIndex: 0,
|
|
}) as never,
|
|
client: {
|
|
addNote: async () => 42,
|
|
addTags: async () => undefined,
|
|
notesInfo: async () => {
|
|
calls.notesInfo += 1;
|
|
return [
|
|
{
|
|
noteId: 42,
|
|
fields: {
|
|
Sentence: { value: 'existing' },
|
|
},
|
|
},
|
|
];
|
|
},
|
|
updateNoteFields: async () => {
|
|
calls.updateNoteFields += 1;
|
|
},
|
|
storeMediaFile: async () => undefined,
|
|
findNotes: async () => [],
|
|
retrieveMediaFile: async () => '',
|
|
},
|
|
mediaGenerator: {
|
|
generateAudio: async () => null,
|
|
generateScreenshot: async () => null,
|
|
generateAnimatedImage: async () => null,
|
|
},
|
|
showOsdNotification: () => undefined,
|
|
showUpdateResult: () => undefined,
|
|
showStatusNotification: () => undefined,
|
|
showNotification: async () => undefined,
|
|
beginUpdateProgress: () => undefined,
|
|
endUpdateProgress: () => undefined,
|
|
withUpdateProgress: async (_message, action) => action(),
|
|
resolveConfiguredFieldName: () => null,
|
|
resolveNoteFieldName: () => null,
|
|
getAnimatedImageLeadInSeconds: async () => 0,
|
|
extractFields: () => ({}),
|
|
processSentence: (sentence) => sentence,
|
|
setCardTypeFields: (updatedFields) => {
|
|
updatedFields.CardType = 'sentence';
|
|
},
|
|
mergeFieldValue: (_existing, newValue) => newValue,
|
|
formatMiscInfoPattern: () => '',
|
|
getEffectiveSentenceCardConfig: () => ({
|
|
model: 'Sentence',
|
|
sentenceField: 'Sentence',
|
|
audioField: 'SentenceAudio',
|
|
lapisEnabled: false,
|
|
kikuEnabled: false,
|
|
kikuFieldGrouping: 'disabled',
|
|
kikuDeleteDuplicateInAuto: false,
|
|
}),
|
|
getFallbackDurationSeconds: () => 10,
|
|
appendKnownWordsFromNoteInfo: () => undefined,
|
|
isUpdateInProgress: () => false,
|
|
setUpdateInProgress: () => undefined,
|
|
trackLastAddedNoteId: () => {
|
|
throw new Error('track failed');
|
|
},
|
|
});
|
|
|
|
const created = await service.createSentenceCard('テスト', 0, 1);
|
|
|
|
assert.equal(created, true);
|
|
assert.equal(calls.notesInfo, 1);
|
|
assert.equal(calls.updateNoteFields, 1);
|
|
});
|
|
|
|
test('CardCreationService keeps updating after recordCardsMinedCallback throws', async () => {
|
|
const calls = {
|
|
notesInfo: 0,
|
|
updateNoteFields: 0,
|
|
};
|
|
const service = new CardCreationService({
|
|
getConfig: () =>
|
|
({
|
|
deck: 'Mining',
|
|
fields: {
|
|
sentence: 'Sentence',
|
|
audio: 'SentenceAudio',
|
|
},
|
|
media: {
|
|
generateAudio: false,
|
|
generateImage: false,
|
|
},
|
|
behavior: {},
|
|
ai: false,
|
|
}) as AnkiConnectConfig,
|
|
getAiConfig: () => ({}),
|
|
getTimingTracker: () => ({}) as never,
|
|
getMpvClient: () =>
|
|
({
|
|
currentVideoPath: '/video.mp4',
|
|
currentSubText: '字幕',
|
|
currentSubStart: 1,
|
|
currentSubEnd: 2,
|
|
currentTimePos: 1.5,
|
|
currentAudioStreamIndex: 0,
|
|
}) as never,
|
|
client: {
|
|
addNote: async () => 42,
|
|
addTags: async () => undefined,
|
|
notesInfo: async () => {
|
|
calls.notesInfo += 1;
|
|
return [
|
|
{
|
|
noteId: 42,
|
|
fields: {
|
|
Sentence: { value: 'existing' },
|
|
},
|
|
},
|
|
];
|
|
},
|
|
updateNoteFields: async () => {
|
|
calls.updateNoteFields += 1;
|
|
},
|
|
storeMediaFile: async () => undefined,
|
|
findNotes: async () => [],
|
|
retrieveMediaFile: async () => '',
|
|
},
|
|
mediaGenerator: {
|
|
generateAudio: async () => null,
|
|
generateScreenshot: async () => null,
|
|
generateAnimatedImage: async () => null,
|
|
},
|
|
showOsdNotification: () => undefined,
|
|
showUpdateResult: () => undefined,
|
|
showStatusNotification: () => undefined,
|
|
showNotification: async () => undefined,
|
|
beginUpdateProgress: () => undefined,
|
|
endUpdateProgress: () => undefined,
|
|
withUpdateProgress: async (_message, action) => action(),
|
|
resolveConfiguredFieldName: () => null,
|
|
resolveNoteFieldName: () => null,
|
|
getAnimatedImageLeadInSeconds: async () => 0,
|
|
extractFields: () => ({}),
|
|
processSentence: (sentence) => sentence,
|
|
setCardTypeFields: (updatedFields) => {
|
|
updatedFields.CardType = 'sentence';
|
|
},
|
|
mergeFieldValue: (_existing, newValue) => newValue,
|
|
formatMiscInfoPattern: () => '',
|
|
getEffectiveSentenceCardConfig: () => ({
|
|
model: 'Sentence',
|
|
sentenceField: 'Sentence',
|
|
audioField: 'SentenceAudio',
|
|
lapisEnabled: false,
|
|
kikuEnabled: false,
|
|
kikuFieldGrouping: 'disabled',
|
|
kikuDeleteDuplicateInAuto: false,
|
|
}),
|
|
getFallbackDurationSeconds: () => 10,
|
|
appendKnownWordsFromNoteInfo: () => undefined,
|
|
isUpdateInProgress: () => false,
|
|
setUpdateInProgress: () => undefined,
|
|
recordCardsMinedCallback: () => {
|
|
throw new Error('record failed');
|
|
},
|
|
});
|
|
|
|
const created = await service.createSentenceCard('テスト', 0, 1);
|
|
|
|
assert.equal(created, true);
|
|
assert.equal(calls.notesInfo, 1);
|
|
assert.equal(calls.updateNoteFields, 1);
|
|
});
|
|
|
|
test('CardCreationService uses stream-open-filename for remote media generation', async () => {
|
|
const audioPaths: string[] = [];
|
|
const imagePaths: string[] = [];
|
|
const edlSource = [
|
|
'edl://!new_stream;!no_clip;!no_chapters;%70%https://audio.example/videoplayback?mime=audio%2Fwebm',
|
|
'!new_stream;!no_clip;!no_chapters;%69%https://video.example/videoplayback?mime=video%2Fmp4',
|
|
'!global_tags,title=test',
|
|
].join(';');
|
|
|
|
const service = new CardCreationService({
|
|
getConfig: () =>
|
|
({
|
|
deck: 'Mining',
|
|
fields: {
|
|
sentence: 'Sentence',
|
|
audio: 'SentenceAudio',
|
|
image: 'Picture',
|
|
},
|
|
media: {
|
|
generateAudio: true,
|
|
generateImage: true,
|
|
imageFormat: 'jpg',
|
|
},
|
|
behavior: {},
|
|
ai: false,
|
|
}) as AnkiConnectConfig,
|
|
getAiConfig: () => ({}),
|
|
getTimingTracker: () => ({}) as never,
|
|
getMpvClient: () =>
|
|
({
|
|
currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
|
|
currentSubText: '字幕',
|
|
currentSubStart: 1,
|
|
currentSubEnd: 2,
|
|
currentTimePos: 1.5,
|
|
currentAudioStreamIndex: 0,
|
|
requestProperty: async (name: string) => {
|
|
assert.equal(name, 'stream-open-filename');
|
|
return edlSource;
|
|
},
|
|
}) as never,
|
|
client: {
|
|
addNote: async () => 42,
|
|
addTags: async () => undefined,
|
|
notesInfo: async () => [
|
|
{
|
|
noteId: 42,
|
|
fields: {
|
|
Sentence: { value: '' },
|
|
SentenceAudio: { value: '' },
|
|
Picture: { value: '' },
|
|
},
|
|
},
|
|
],
|
|
updateNoteFields: async () => undefined,
|
|
storeMediaFile: async () => undefined,
|
|
findNotes: async () => [],
|
|
retrieveMediaFile: async () => '',
|
|
},
|
|
mediaGenerator: {
|
|
generateAudio: async (path) => {
|
|
audioPaths.push(path);
|
|
return Buffer.from('audio');
|
|
},
|
|
generateScreenshot: async (path) => {
|
|
imagePaths.push(path);
|
|
return Buffer.from('image');
|
|
},
|
|
generateAnimatedImage: async () => null,
|
|
},
|
|
showOsdNotification: () => undefined,
|
|
showUpdateResult: () => undefined,
|
|
showStatusNotification: () => undefined,
|
|
showNotification: async () => undefined,
|
|
beginUpdateProgress: () => undefined,
|
|
endUpdateProgress: () => undefined,
|
|
withUpdateProgress: async (_message, action) => action(),
|
|
resolveConfiguredFieldName: (noteInfo, preferredName) => {
|
|
if (!preferredName) return null;
|
|
return Object.keys(noteInfo.fields).find((field) => field === preferredName) ?? null;
|
|
},
|
|
resolveNoteFieldName: (noteInfo, preferredName) => {
|
|
if (!preferredName) return null;
|
|
return Object.keys(noteInfo.fields).find((field) => field === preferredName) ?? null;
|
|
},
|
|
getAnimatedImageLeadInSeconds: async () => 0,
|
|
extractFields: () => ({}),
|
|
processSentence: (sentence) => sentence,
|
|
setCardTypeFields: () => undefined,
|
|
mergeFieldValue: (_existing, newValue) => newValue,
|
|
formatMiscInfoPattern: () => '',
|
|
getEffectiveSentenceCardConfig: () => ({
|
|
model: 'Sentence',
|
|
sentenceField: 'Sentence',
|
|
audioField: 'SentenceAudio',
|
|
lapisEnabled: false,
|
|
kikuEnabled: false,
|
|
kikuFieldGrouping: 'disabled',
|
|
kikuDeleteDuplicateInAuto: false,
|
|
}),
|
|
getFallbackDurationSeconds: () => 10,
|
|
appendKnownWordsFromNoteInfo: () => undefined,
|
|
isUpdateInProgress: () => false,
|
|
setUpdateInProgress: () => undefined,
|
|
trackLastAddedNoteId: () => undefined,
|
|
});
|
|
|
|
const created = await service.createSentenceCard('テスト', 0, 1);
|
|
|
|
assert.equal(created, true);
|
|
assert.deepEqual(audioPaths, ['https://audio.example/videoplayback?mime=audio%2Fwebm']);
|
|
assert.deepEqual(imagePaths, ['https://video.example/videoplayback?mime=video%2Fmp4']);
|
|
});
|