fix(anki): separate word audio mapping for animation sync (#256)

This commit is contained in:
2026-09-20 22:07:30 -07:00
committed by GitHub
parent 8097ed27e5
commit 05425cc5db
17 changed files with 97 additions and 10 deletions
@@ -29,6 +29,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
fields: {
word: 'Expression',
audio: 'ExpressionAudio',
wordAudio: 'ExpressionAudio',
image: 'Picture',
sentence: 'Sentence',
miscInfo: 'MiscInfo',
@@ -82,6 +82,13 @@ export function buildIntegrationConfigOptionRegistry(
defaultValue: defaultConfig.ankiConnect.fields.audio,
description: 'Card field that receives generated sentence audio.',
},
{
path: 'ankiConnect.fields.wordAudio',
kind: 'string',
defaultValue: defaultConfig.ankiConnect.fields.wordAudio,
description:
'Existing word-audio field read to time the frozen first frame of animated images. This mapping is only used for synchronization.',
},
{
path: 'ankiConnect.fields.image',
kind: 'string',
+1 -1
View File
@@ -144,7 +144,7 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
title: 'AnkiConnect Integration',
description: ['Automatic Anki updates and media generation options.'],
notes: [
'Hot-reload: ankiConnect.ai.enabled, media.normalizeAudio/mirrorMpvVolume/reviewTiming, knownWords, nPlusOne, fields.word/audio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, isKiku.fieldGrouping, isSenren.fieldGrouping, and lapisKiku.wordCardKind update live while SubMiner is running.',
'Hot-reload: ankiConnect.ai.enabled, media.normalizeAudio/mirrorMpvVolume/reviewTiming, knownWords, nPlusOne, fields.word/audio/wordAudio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, isKiku.fieldGrouping, isSenren.fieldGrouping, and lapisKiku.wordCardKind update live while SubMiner is running.',
'Shared AI provider transport settings are read from top-level ai and typically require restart.',
'Most other AnkiConnect settings still require restart.',
],
+1
View File
@@ -29,6 +29,7 @@ const HOT_RELOAD_EXACT_OR_PREFIX_PATHS = [
'ankiConnect.nPlusOne.minSentenceWords',
'ankiConnect.fields.word',
'ankiConnect.fields.audio',
'ankiConnect.fields.wordAudio',
'ankiConnect.fields.image',
'ankiConnect.fields.sentence',
'ankiConnect.fields.miscInfo',
+22
View File
@@ -290,6 +290,28 @@ test('accepts ankiConnect.media.syncAnimatedImageToWordAudio override', () => {
);
});
test('word audio mapping defaults and validates independently of sentence audio', () => {
for (const wordAudio of [undefined, 'Pronunciation', 7]) {
const { context, warnings } = makeContext({
fields: {
audio: 'SentenceAudio',
...(wordAudio !== undefined ? { wordAudio } : {}),
},
});
applyAnkiConnectResolution(context);
assert.equal(context.resolved.ankiConnect.fields.audio, 'SentenceAudio');
assert.equal(
context.resolved.ankiConnect.fields.wordAudio,
typeof wordAudio === 'string' ? wordAudio : DEFAULT_CONFIG.ankiConnect.fields.wordAudio,
);
assert.deepEqual(
warnings.map((warning) => warning.path),
typeof wordAudio === 'number' ? ['ankiConnect.fields.wordAudio'] : [],
);
}
});
test('invalid modern Anki subtrees warn and keep resolved defaults', () => {
const { context, warnings } = makeContext({
fields: { word: 7 },
@@ -7,7 +7,15 @@ export function applyModernFieldsResolution(
context: ResolveContext,
fields: Record<string, unknown>,
): void {
for (const key of ['word', 'audio', 'image', 'sentence', 'miscInfo', 'translation'] as const) {
for (const key of [
'word',
'audio',
'wordAudio',
'image',
'sentence',
'miscInfo',
'translation',
] as const) {
applyModernValue(
context,
fields,
+1
View File
@@ -364,6 +364,7 @@ test('settings registry marks safe live config paths as hot-reloadable', () => {
'ankiConnect.nPlusOne.minSentenceWords',
'ankiConnect.fields.word',
'ankiConnect.fields.audio',
'ankiConnect.fields.wordAudio',
'ankiConnect.fields.image',
'ankiConnect.fields.sentence',
'ankiConnect.fields.miscInfo',