feat(anki): mirror mpv software volume into generated sentence audio (#155)

This commit is contained in:
2026-07-10 00:44:23 -07:00
committed by GitHub
parent 84c75f50aa
commit 7b1a005a65
31 changed files with 419 additions and 23 deletions
+26
View File
@@ -93,6 +93,7 @@ test('loads defaults when config is missing', () => {
systemPrompt: '',
});
assert.equal(config.ankiConnect.media.normalizeAudio, true);
assert.equal(config.ankiConnect.media.mirrorMpvVolume, true);
assert.equal(config.startupWarmups.lowPowerMode, false);
assert.equal(config.startupWarmups.mecab, true);
assert.equal(config.startupWarmups.yomitanExtension, true);
@@ -168,6 +169,31 @@ test('loads defaults when config is missing', () => {
assert.equal(config.mpv.aniskipButtonKey, 'TAB');
});
test('rejects invalid mpv volume mirroring values', () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, 'config.jsonc'),
`{
"ankiConnect": {
"media": {
"mirrorMpvVolume": "false"
}
}
}`,
'utf-8',
);
const service = new ConfigService(dir);
assert.equal(
service.getConfig().ankiConnect.media.mirrorMpvVolume,
DEFAULT_CONFIG.ankiConnect.media.mirrorMpvVolume,
);
assert.ok(
service.getWarnings().some((warning) => warning.path === 'ankiConnect.media.mirrorMpvVolume'),
);
});
test('parses updates config and warns on invalid values', () => {
const validDir = makeTempDir();
fs.writeFileSync(
@@ -52,6 +52,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
animatedCrf: 35,
syncAnimatedImageToWordAudio: true,
normalizeAudio: true,
mirrorMpvVolume: true,
audioPadding: 0,
fallbackDuration: 3.0,
maxMediaDuration: 30,
@@ -111,6 +111,7 @@ test('config option registry includes critical paths and has unique entries', ()
'ankiConnect.enabled',
'subtitleStyle.nameMatchEnabled',
'ankiConnect.media.normalizeAudio',
'ankiConnect.media.mirrorMpvVolume',
'anilist.characterDictionary.collapsibleSections.description',
'mpv.executablePath',
'mpv.launchMode',
@@ -185,7 +185,15 @@ export function buildIntegrationConfigOptionRegistry(
path: 'ankiConnect.media.normalizeAudio',
kind: 'boolean',
defaultValue: defaultConfig.ankiConnect.media.normalizeAudio,
description: 'Normalize generated sentence audio loudness during media extraction.',
description:
'Normalize generated sentence audio loudness during media extraction. Changes apply live.',
},
{
path: 'ankiConnect.media.mirrorMpvVolume',
kind: 'boolean',
defaultValue: defaultConfig.ankiConnect.media.mirrorMpvVolume,
description:
"Apply mpv's current software volume curve to generated sentence audio. Changes apply live.",
},
{
path: 'ankiConnect.media.generateImage',
+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, knownWords, nPlusOne, fields.word/audio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, and isKiku.fieldGrouping update live while SubMiner is running.',
'Hot-reload: ankiConnect.ai.enabled, media.normalizeAudio/mirrorMpvVolume, knownWords, nPlusOne, fields.word/audio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, and isKiku.fieldGrouping 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.',
],
+16
View File
@@ -106,6 +106,22 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
},
};
if (hasOwn(media, 'mirrorMpvVolume')) {
const parsed = asBoolean(media.mirrorMpvVolume);
if (parsed === undefined) {
context.resolved.ankiConnect.media.mirrorMpvVolume =
DEFAULT_CONFIG.ankiConnect.media.mirrorMpvVolume;
context.warn(
'ankiConnect.media.mirrorMpvVolume',
media.mirrorMpvVolume,
context.resolved.ankiConnect.media.mirrorMpvVolume,
'Expected boolean.',
);
} else {
context.resolved.ankiConnect.media.mirrorMpvVolume = parsed;
}
}
if (hasOwn(behavior, 'notificationType')) {
const parsed = asNotificationType(behavior.notificationType);
if (parsed === undefined) {
+11
View File
@@ -165,6 +165,15 @@ test('settings registry exposes specialized controls for config-assisted inputs'
assert.equal(field('discordPresence.presenceStyle').control, 'select');
});
test('settings registry exposes mpv volume mirroring as a mining toggle', () => {
const volumeField = field('ankiConnect.media.mirrorMpvVolume');
assert.equal(volumeField.category, 'mining-anki');
assert.equal(volumeField.section, 'Media Capture');
assert.equal(volumeField.control, 'boolean');
assert.equal(volumeField.defaultValue, true);
});
test('settings registry exposes YouTube media cache mode as a labeled select', () => {
const mediaCacheMode = field('youtube.mediaCache.mode');
const mediaCacheMaxHeight = field('youtube.mediaCache.maxHeight');
@@ -313,6 +322,8 @@ test('settings registry marks safe live config paths as hot-reloadable', () => {
'subsync.replace',
'ankiConnect.behavior.autoUpdateNewCards',
'ankiConnect.deck',
'ankiConnect.media.normalizeAudio',
'ankiConnect.media.mirrorMpvVolume',
'ankiConnect.knownWords.highlightEnabled',
'ankiConnect.knownWords.refreshMinutes',
'ankiConnect.knownWords.addMinedWordsImmediately',
+3
View File
@@ -236,6 +236,7 @@ const LABEL_OVERRIDES: Record<string, string> = {
'mpv.pauseUntilOverlayReady': 'Pause Until Overlay Ready',
'mpv.aniskipEnabled': 'Enable AniSkip',
'mpv.aniskipButtonKey': 'AniSkip Button Key',
'ankiConnect.media.mirrorMpvVolume': 'Mirror mpv Volume',
'discordPresence.updateIntervalMs': 'Update Interval (ms)',
};
@@ -671,6 +672,8 @@ function restartBehaviorForPath(path: string): ConfigSettingsRestartBehavior {
path === 'secondarySub.defaultMode' ||
path === 'ankiConnect.deck' ||
path === 'ankiConnect.ai.enabled' ||
path === 'ankiConnect.media.normalizeAudio' ||
path === 'ankiConnect.media.mirrorMpvVolume' ||
path === 'ankiConnect.behavior.autoUpdateNewCards' ||
path === 'ankiConnect.knownWords.highlightEnabled' ||
path === 'ankiConnect.knownWords.refreshMinutes' ||