mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-01 19:21:34 -07:00
feat(anki): add configurable word card type for Kiku/Lapis (#175)
This commit is contained in:
@@ -2454,6 +2454,80 @@ Aligned English subtitle
|
||||
});
|
||||
});
|
||||
|
||||
it('POST /api/stats/mine-card marks the configured Kiku word card kind', async () => {
|
||||
await withTempDir(async (dir) => {
|
||||
const sourcePath = path.join(dir, 'episode.mkv');
|
||||
fs.writeFileSync(sourcePath, 'fake media');
|
||||
|
||||
await withFakeAnkiConnect(
|
||||
async (requests, url) => {
|
||||
const app = createStatsApp(createMockTracker(), {
|
||||
addYomitanNote: async () => 777,
|
||||
createMediaGenerator: () => ({
|
||||
generateAudio: async () => null,
|
||||
generateScreenshot: async () => null,
|
||||
generateAnimatedImage: async () => null,
|
||||
}),
|
||||
ankiConnectConfig: {
|
||||
url,
|
||||
deck: 'Mining',
|
||||
fields: {
|
||||
image: 'Picture',
|
||||
sentence: 'Sentence',
|
||||
},
|
||||
media: {
|
||||
generateAudio: false,
|
||||
generateImage: false,
|
||||
},
|
||||
isKiku: {
|
||||
enabled: true,
|
||||
fieldGrouping: 'disabled',
|
||||
deleteDuplicateInAuto: true,
|
||||
},
|
||||
lapisKiku: {
|
||||
wordCardKind: 'click',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const res = await app.request('/api/stats/mine-card?mode=word', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sourcePath,
|
||||
startMs: 1_000,
|
||||
endMs: 2_000,
|
||||
sentence: '猫を見た',
|
||||
word: '猫',
|
||||
videoTitle: 'Episode 1',
|
||||
}),
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
assert.equal(res.status, 200, JSON.stringify(body));
|
||||
|
||||
const updateRequest = requests.find((request) => request.action === 'updateNoteFields');
|
||||
const fields = updateRequest?.params?.note?.fields ?? {};
|
||||
assert.equal(fields.IsClickCard, 'x');
|
||||
assert.equal(fields.IsWordAndSentenceCard, '');
|
||||
assert.equal(fields.IsSentenceCard, '');
|
||||
assert.equal(fields.IsAudioCard, '');
|
||||
},
|
||||
{
|
||||
notesInfoFields: {
|
||||
Expression: { value: '猫' },
|
||||
Sentence: { value: '' },
|
||||
Picture: { value: '' },
|
||||
IsWordAndSentenceCard: { value: '' },
|
||||
IsClickCard: { value: '' },
|
||||
IsSentenceCard: { value: '' },
|
||||
IsAudioCard: { value: '' },
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('POST /api/stats/mine-card writes word mining sentence audio and image together', async () => {
|
||||
await withTempDir(async (dir) => {
|
||||
const sourcePath = path.join(dir, 'episode.mkv');
|
||||
|
||||
@@ -85,6 +85,7 @@ const HOT_RELOAD_EXACT_OR_PREFIX_PATHS = [
|
||||
'ankiConnect.fields.miscInfo',
|
||||
'ankiConnect.isLapis.sentenceCardModel',
|
||||
'ankiConnect.isKiku.fieldGrouping',
|
||||
'ankiConnect.lapisKiku.wordCardKind',
|
||||
] as const;
|
||||
|
||||
function hotReloadFieldForChangedPath(path: string): string | null {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
resolveSecondarySubtitleTextFromSidecar,
|
||||
} from '../secondary-subtitle-sidecar.js';
|
||||
import {
|
||||
applyStatsWordAndSentenceCardFields,
|
||||
applyStatsWordCardFields,
|
||||
createStatsMiningContext,
|
||||
getStatsDirectMiningAudioFieldNames,
|
||||
getStatsWordMiningAudioFieldName,
|
||||
@@ -272,7 +272,7 @@ export function registerStatsMiningRoutes(app: Hono, options?: StatsMiningRouteO
|
||||
const imageFieldName = ankiConfig.fields?.image ?? 'Picture';
|
||||
|
||||
mediaFields[sentenceFieldName] = highlightedSentence;
|
||||
applyStatsWordAndSentenceCardFields(mediaFields, noteInfo, ankiConfig);
|
||||
applyStatsWordCardFields(mediaFields, noteInfo, ankiConfig);
|
||||
|
||||
if (audioBuffer) {
|
||||
const audioFilename = `subminer_audio_${timestamp}_${noteId}.mp3`;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { MediaGenerator } from '../../../media-generator.js';
|
||||
import type { AnkiConnectConfig } from '../../../types.js';
|
||||
import { applyCardKindFlagFields } from '../../../anki-integration/card-kinds.js';
|
||||
import { resolveWordCardKind } from '../../../anki-integration/note-field-utils.js';
|
||||
import { createLogger } from '../../../logger.js';
|
||||
import type { RetimedSecondarySubtitleInput } from '../secondary-subtitle-sidecar.js';
|
||||
|
||||
@@ -94,20 +96,22 @@ export function shouldUseStatsLapisKikuCardFields(ankiConfig: AnkiConnectConfig)
|
||||
return ankiConfig.isLapis?.enabled === true || ankiConfig.isKiku?.enabled === true;
|
||||
}
|
||||
|
||||
export function applyStatsWordAndSentenceCardFields(
|
||||
export function applyStatsWordCardFields(
|
||||
fields: Record<string, string>,
|
||||
noteInfo: StatsServerNoteInfo | null,
|
||||
ankiConfig: AnkiConnectConfig,
|
||||
): void {
|
||||
if (!shouldUseStatsLapisKikuCardFields(ankiConfig) || !noteInfo) return;
|
||||
const wordAndSentenceFlag = resolveStatsNoteFieldName(noteInfo, 'IsWordAndSentenceCard');
|
||||
if (!wordAndSentenceFlag) return;
|
||||
if (!noteInfo) return;
|
||||
const cardKind = resolveWordCardKind(noteInfo, {
|
||||
lapisEnabled: ankiConfig.isLapis?.enabled === true,
|
||||
kikuEnabled: ankiConfig.isKiku?.enabled === true,
|
||||
wordCardKind: ankiConfig.lapisKiku?.wordCardKind,
|
||||
});
|
||||
if (!cardKind) return;
|
||||
|
||||
fields[wordAndSentenceFlag] = 'x';
|
||||
for (const flagName of ['IsSentenceCard', 'IsAudioCard']) {
|
||||
const resolved = resolveStatsNoteFieldName(noteInfo, flagName);
|
||||
if (resolved && resolved !== wordAndSentenceFlag) fields[resolved] = '';
|
||||
}
|
||||
applyCardKindFlagFields(fields, cardKind, (preferredName) =>
|
||||
resolveStatsNoteFieldName(noteInfo, preferredName),
|
||||
);
|
||||
}
|
||||
|
||||
export function getStatsDirectMiningAudioFieldNames(
|
||||
|
||||
Reference in New Issue
Block a user