feat(anki): add media timing review before card creation (#203)

This commit is contained in:
2026-09-04 01:40:56 -07:00
committed by GitHub
parent 99266294b8
commit 84f718043a
78 changed files with 7022 additions and 135 deletions
+1
View File
@@ -2181,6 +2181,7 @@ test('runtime options registry is centralized', () => {
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
assert.deepEqual(ids, [
'anki.autoUpdateNewCards',
'anki.mediaReviewTiming',
'subtitle.annotation.knownWords.highlightEnabled',
'subtitle.annotation.knownWords.maturityEnabled',
'subtitle.annotation.nPlusOne',
@@ -54,6 +54,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
syncAnimatedImageToWordAudio: true,
normalizeAudio: true,
mirrorMpvVolume: true,
reviewTiming: false,
audioPadding: 0,
fallbackDuration: 3.0,
maxMediaDuration: 30,
@@ -196,6 +196,14 @@ export function buildIntegrationConfigOptionRegistry(
description:
"Apply mpv's current software volume curve to generated sentence audio. Changes apply live.",
},
{
path: 'ankiConnect.media.reviewTiming',
kind: 'boolean',
defaultValue: defaultConfig.ankiConnect.media.reviewTiming,
description:
'Review and preview subtitle media timing before SubMiner creates or enriches a mined card.',
runtime: runtimeOptionById.get('anki.mediaReviewTiming'),
},
{
path: 'ankiConnect.media.generateImage',
kind: 'boolean',
+14
View File
@@ -19,6 +19,20 @@ export function buildRuntimeOptionRegistry(
behavior: { autoUpdateNewCards: value === true },
}),
},
{
id: 'anki.mediaReviewTiming',
path: 'ankiConnect.media.reviewTiming',
label: 'Review Media Timing',
scope: 'ankiConnect',
valueType: 'boolean',
allowedValues: [true, false],
defaultValue: defaultConfig.ankiConnect.media.reviewTiming,
requiresRestart: false,
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
toAnkiPatch: (value) => ({
media: { reviewTiming: value === true },
}),
},
{
id: 'subtitle.annotation.knownWords.highlightEnabled',
path: 'ankiConnect.knownWords.highlightEnabled',
+1 -1
View File
@@ -135,7 +135,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, 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/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.',
],
+28
View File
@@ -21,6 +21,34 @@ function makeContext(ankiConnect: unknown): {
return { context, warnings };
}
test('media timing review is disabled by default and accepts a boolean override', () => {
const defaultContext = makeContext({});
applyAnkiConnectResolution(defaultContext.context);
assert.equal(defaultContext.context.resolved.ankiConnect.media.reviewTiming, false);
const enabledContext = makeContext({ media: { reviewTiming: true } });
applyAnkiConnectResolution(enabledContext.context);
assert.equal(enabledContext.context.resolved.ankiConnect.media.reviewTiming, true);
assert.deepEqual(enabledContext.warnings, []);
});
test('modern media duration accepts zero as the disabled cap sentinel', () => {
const disabledCap = makeContext({ media: { maxMediaDuration: 0 } });
applyAnkiConnectResolution(disabledCap.context);
assert.equal(disabledCap.context.resolved.ankiConnect.media.maxMediaDuration, 0);
assert.deepEqual(disabledCap.warnings, []);
const invalidCap = makeContext({ media: { maxMediaDuration: -1 } });
applyAnkiConnectResolution(invalidCap.context);
assert.equal(
invalidCap.context.resolved.ankiConnect.media.maxMediaDuration,
DEFAULT_CONFIG.ankiConnect.media.maxMediaDuration,
);
assert.ok(
invalidCap.warnings.some((warning) => warning.path === 'ankiConnect.media.maxMediaDuration'),
);
});
test('modern invalid knownWords.highlightEnabled warns modern key and does not fallback to legacy', () => {
const { context, warnings } = makeContext({
nPlusOne: { highlightEnabled: true },
+25 -14
View File
@@ -19,6 +19,7 @@ export function applyModernMediaResolution(
'syncAnimatedImageToWordAudio',
'normalizeAudio',
'mirrorMpvVolume',
'reviewTiming',
] as const) {
applyModernValue(
context,
@@ -128,18 +129,28 @@ export function applyModernMediaResolution(
'Expected non-negative number.',
);
for (const key of ['fallbackDuration', 'maxMediaDuration'] as const) {
applyModernValue(
context,
media,
key,
`ankiConnect.media.${key}`,
asPositiveNumber,
DEFAULT_CONFIG.ankiConnect.media[key],
(value) => {
context.resolved.ankiConnect.media[key] = value;
},
'Expected positive number.',
);
}
applyModernValue(
context,
media,
'fallbackDuration',
'ankiConnect.media.fallbackDuration',
asPositiveNumber,
DEFAULT_CONFIG.ankiConnect.media.fallbackDuration,
(value) => {
context.resolved.ankiConnect.media.fallbackDuration = value;
},
'Expected positive number.',
);
applyModernValue(
context,
media,
'maxMediaDuration',
'ankiConnect.media.maxMediaDuration',
asNonNegativeNumber,
DEFAULT_CONFIG.ankiConnect.media.maxMediaDuration,
(value) => {
context.resolved.ankiConnect.media.maxMediaDuration = value;
},
'Expected non-negative number.',
);
}
+1
View File
@@ -354,6 +354,7 @@ test('settings registry marks safe live config paths as hot-reloadable', () => {
'ankiConnect.deck',
'ankiConnect.media.normalizeAudio',
'ankiConnect.media.mirrorMpvVolume',
'ankiConnect.media.reviewTiming',
'ankiConnect.knownWords.highlightEnabled',
'ankiConnect.knownWords.refreshMinutes',
'ankiConnect.knownWords.addMinedWordsImmediately',
+2
View File
@@ -246,6 +246,7 @@ const LABEL_OVERRIDES: Record<string, string> = {
'mpv.aniskipEnabled': 'Enable AniSkip',
'mpv.aniskipButtonKey': 'AniSkip Button Key',
'ankiConnect.media.mirrorMpvVolume': 'Mirror mpv Volume',
'ankiConnect.media.reviewTiming': 'Review Media Timing',
'discordPresence.updateIntervalMs': 'Update Interval (ms)',
};
@@ -699,6 +700,7 @@ function restartBehaviorForPath(path: string): ConfigSettingsRestartBehavior {
path === 'ankiConnect.ai.enabled' ||
path === 'ankiConnect.media.normalizeAudio' ||
path === 'ankiConnect.media.mirrorMpvVolume' ||
path === 'ankiConnect.media.reviewTiming' ||
path === 'ankiConnect.behavior.autoUpdateNewCards' ||
path === 'ankiConnect.knownWords.highlightEnabled' ||
path === 'ankiConnect.knownWords.refreshMinutes' ||