mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-21 17:01:25 -07:00
feat(youtube): add YouTube browser window and Whisper subtitle source (#273)
This commit is contained in:
1 parent
c68b73c8c1
commit
07b32626a6
101 files changed
+2269
-726
No files matched your search
+24
-16
@@ -1757,11 +1757,6 @@ test('parses global shortcuts and startup settings', () => {
|
||||
},
|
||||
"youtube": {
|
||||
"primarySubLanguages": ["ja", "jpn", "jp"]
|
||||
},
|
||||
"youtubeSubgen": {
|
||||
"whisperVadModel": "/models/vad.bin",
|
||||
"whisperThreads": 12,
|
||||
"fixWithAi": true
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
@@ -1774,9 +1769,30 @@ test('parses global shortcuts and startup settings', () => {
|
||||
assert.equal(config.shortcuts.toggleVisibleOverlayGlobal, 'Alt+Shift+U');
|
||||
assert.equal(config.shortcuts.openJimaku, 'Ctrl+Alt+J');
|
||||
assert.deepEqual(config.youtube.primarySubLanguages, ['ja', 'jpn', 'jp']);
|
||||
assert.equal(config.youtubeSubgen.whisperVadModel, '/models/vad.bin');
|
||||
assert.equal(config.youtubeSubgen.whisperThreads, 12);
|
||||
assert.equal(config.youtubeSubgen.fixWithAi, true);
|
||||
});
|
||||
|
||||
test('parses YouTube subtitle source and warns on invalid values', () => {
|
||||
assert.equal(DEFAULT_CONFIG.youtube.subtitleSource, 'youtube');
|
||||
|
||||
const validDir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(validDir, 'config.jsonc'),
|
||||
`{ "youtube": { "subtitleSource": "whisper" } }`,
|
||||
'utf-8',
|
||||
);
|
||||
assert.equal(new ConfigService(validDir).getConfig().youtube.subtitleSource, 'whisper');
|
||||
|
||||
const invalidDir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(invalidDir, 'config.jsonc'),
|
||||
`{ "youtube": { "subtitleSource": "asr" } }`,
|
||||
'utf-8',
|
||||
);
|
||||
const invalidService = new ConfigService(invalidDir);
|
||||
assert.equal(invalidService.getConfig().youtube.subtitleSource, 'youtube');
|
||||
assert.ok(
|
||||
invalidService.getWarnings().some((warning) => warning.path === 'youtube.subtitleSource'),
|
||||
);
|
||||
});
|
||||
|
||||
test('parses YouTube media cache config and warns on invalid values', () => {
|
||||
@@ -2577,12 +2593,6 @@ test('accepts per-feature ai overrides for anki and YouTube subtitles', () => {
|
||||
"model": "openrouter/anki-model",
|
||||
"systemPrompt": "Translate mined sentence text."
|
||||
}
|
||||
},
|
||||
"youtubeSubgen": {
|
||||
"ai": {
|
||||
"model": "openrouter/subgen-model",
|
||||
"systemPrompt": "Fix subtitle mistakes only."
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
@@ -2596,8 +2606,6 @@ test('accepts per-feature ai overrides for anki and YouTube subtitles', () => {
|
||||
assert.equal(config.ankiConnect.ai.enabled, true);
|
||||
assert.equal(config.ankiConnect.ai.model, 'openrouter/anki-model');
|
||||
assert.equal(config.ankiConnect.ai.systemPrompt, 'Translate mined sentence text.');
|
||||
assert.equal(config.youtubeSubgen.ai.model, 'openrouter/subgen-model');
|
||||
assert.equal(config.youtubeSubgen.ai.systemPrompt, 'Fix subtitle mistakes only.');
|
||||
});
|
||||
|
||||
test('warns and falls back when ankiConnect.ai override values are invalid', () => {
|
||||
|
||||
@@ -49,7 +49,6 @@ const {
|
||||
jellyfin,
|
||||
discordPresence,
|
||||
ai,
|
||||
youtubeSubgen,
|
||||
} = INTEGRATIONS_DEFAULT_CONFIG;
|
||||
const { subtitleStyle, subtitleSidebar } = SUBTITLE_DEFAULT_CONFIG;
|
||||
const { immersionTracking } = IMMERSION_DEFAULT_CONFIG;
|
||||
@@ -85,7 +84,6 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
jellyfin,
|
||||
discordPresence,
|
||||
ai,
|
||||
youtubeSubgen,
|
||||
immersionTracking,
|
||||
stats,
|
||||
};
|
||||
|
||||
@@ -115,6 +115,7 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
||||
},
|
||||
youtube: {
|
||||
primarySubLanguages: ['ja', 'jpn'],
|
||||
subtitleSource: 'youtube',
|
||||
mediaCache: {
|
||||
mode: 'direct',
|
||||
maxHeight: 720,
|
||||
|
||||
@@ -13,7 +13,6 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
| 'jellyfin'
|
||||
| 'discordPresence'
|
||||
| 'ai'
|
||||
| 'youtubeSubgen'
|
||||
> = {
|
||||
ankiConnect: {
|
||||
enabled: true,
|
||||
@@ -179,15 +178,4 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
'You are a translation engine. Return only the translated text with no explanations.',
|
||||
requestTimeoutMs: 15_000,
|
||||
},
|
||||
youtubeSubgen: {
|
||||
whisperBin: '',
|
||||
whisperModel: '',
|
||||
whisperVadModel: '',
|
||||
whisperThreads: 4,
|
||||
fixWithAi: false,
|
||||
ai: {
|
||||
model: '',
|
||||
systemPrompt: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -85,13 +85,6 @@ const UNDOCUMENTED_LEAVES: ReadonlySet<string> = new Set([
|
||||
'subtitleStyle.textShadow',
|
||||
'subtitleStyle.WebkitTextStroke',
|
||||
'subtitleStyle.wordSpacing',
|
||||
'youtubeSubgen.ai.model',
|
||||
'youtubeSubgen.ai.systemPrompt',
|
||||
'youtubeSubgen.fixWithAi',
|
||||
'youtubeSubgen.whisperBin',
|
||||
'youtubeSubgen.whisperModel',
|
||||
'youtubeSubgen.whisperThreads',
|
||||
'youtubeSubgen.whisperVadModel',
|
||||
]);
|
||||
|
||||
test('config option registry includes critical paths and has unique entries', () => {
|
||||
|
||||
@@ -119,6 +119,18 @@ export function buildCoreConfigOptionRegistry(
|
||||
description:
|
||||
'Comma-separated primary subtitle language priority for managed subtitle auto-selection.',
|
||||
},
|
||||
{
|
||||
path: 'youtube.subtitleSource',
|
||||
kind: 'enum',
|
||||
enumValues: ['youtube', 'whisper'],
|
||||
enumLabels: {
|
||||
youtube: 'Download YouTube captions',
|
||||
whisper: 'Generate with Whisper',
|
||||
},
|
||||
defaultValue: defaultConfig.youtube.subtitleSource,
|
||||
description:
|
||||
'Where primary YouTube subtitles come from. Whisper transcribes the audio locally using the subtitleGeneration settings.',
|
||||
},
|
||||
{
|
||||
path: 'youtube.mediaCache.mode',
|
||||
kind: 'enum',
|
||||
|
||||
@@ -183,7 +183,9 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
description: [
|
||||
'Defaults for managed subtitle language preferences and YouTube subtitle loading.',
|
||||
],
|
||||
notes: ['Hot-reload: primarySubLanguages applies to the next YouTube subtitle load.'],
|
||||
notes: [
|
||||
'Hot-reload: primarySubLanguages and subtitleSource apply to the next YouTube subtitle load.',
|
||||
],
|
||||
key: 'youtube',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ const HOT_RELOAD_EXACT_OR_PREFIX_PATHS = [
|
||||
'logging.rotation',
|
||||
'logging.files',
|
||||
'youtube.primarySubLanguages',
|
||||
'youtube.subtitleSource',
|
||||
'ankiConnect.deck',
|
||||
'ankiConnect.media.normalizeAudio',
|
||||
'ankiConnect.media.mirrorMpvVolume',
|
||||
|
||||
@@ -339,6 +339,18 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
||||
);
|
||||
}
|
||||
|
||||
const subtitleSource = src.youtube.subtitleSource;
|
||||
if (subtitleSource === 'youtube' || subtitleSource === 'whisper') {
|
||||
resolved.youtube.subtitleSource = subtitleSource;
|
||||
} else if (subtitleSource !== undefined) {
|
||||
warn(
|
||||
'youtube.subtitleSource',
|
||||
subtitleSource,
|
||||
resolved.youtube.subtitleSource,
|
||||
"Expected 'youtube' or 'whisper'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (isObject(src.youtube.mediaCache)) {
|
||||
const mode = src.youtube.mediaCache.mode;
|
||||
if (mode === 'direct' || mode === 'background') {
|
||||
|
||||
@@ -118,105 +118,6 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(src.youtubeSubgen)) {
|
||||
const whisperBin = asString(src.youtubeSubgen.whisperBin);
|
||||
if (whisperBin !== undefined) {
|
||||
resolved.youtubeSubgen.whisperBin = whisperBin;
|
||||
} else if (src.youtubeSubgen.whisperBin !== undefined) {
|
||||
warn(
|
||||
'youtubeSubgen.whisperBin',
|
||||
src.youtubeSubgen.whisperBin,
|
||||
resolved.youtubeSubgen.whisperBin,
|
||||
'Expected string.',
|
||||
);
|
||||
}
|
||||
|
||||
const whisperModel = asString(src.youtubeSubgen.whisperModel);
|
||||
if (whisperModel !== undefined) {
|
||||
resolved.youtubeSubgen.whisperModel = whisperModel;
|
||||
} else if (src.youtubeSubgen.whisperModel !== undefined) {
|
||||
warn(
|
||||
'youtubeSubgen.whisperModel',
|
||||
src.youtubeSubgen.whisperModel,
|
||||
resolved.youtubeSubgen.whisperModel,
|
||||
'Expected string.',
|
||||
);
|
||||
}
|
||||
|
||||
const whisperVadModel = asString(src.youtubeSubgen.whisperVadModel);
|
||||
if (whisperVadModel !== undefined) {
|
||||
resolved.youtubeSubgen.whisperVadModel = whisperVadModel;
|
||||
} else if (src.youtubeSubgen.whisperVadModel !== undefined) {
|
||||
warn(
|
||||
'youtubeSubgen.whisperVadModel',
|
||||
src.youtubeSubgen.whisperVadModel,
|
||||
resolved.youtubeSubgen.whisperVadModel,
|
||||
'Expected string.',
|
||||
);
|
||||
}
|
||||
|
||||
const whisperThreads = asNumber(src.youtubeSubgen.whisperThreads);
|
||||
if (whisperThreads !== undefined && Number.isInteger(whisperThreads) && whisperThreads > 0) {
|
||||
resolved.youtubeSubgen.whisperThreads = whisperThreads;
|
||||
} else if (src.youtubeSubgen.whisperThreads !== undefined) {
|
||||
warn(
|
||||
'youtubeSubgen.whisperThreads',
|
||||
src.youtubeSubgen.whisperThreads,
|
||||
resolved.youtubeSubgen.whisperThreads,
|
||||
'Expected positive integer.',
|
||||
);
|
||||
}
|
||||
|
||||
const fixWithAi = asBoolean(src.youtubeSubgen.fixWithAi);
|
||||
if (fixWithAi !== undefined) {
|
||||
resolved.youtubeSubgen.fixWithAi = fixWithAi;
|
||||
} else if (src.youtubeSubgen.fixWithAi !== undefined) {
|
||||
warn(
|
||||
'youtubeSubgen.fixWithAi',
|
||||
src.youtubeSubgen.fixWithAi,
|
||||
resolved.youtubeSubgen.fixWithAi,
|
||||
'Expected boolean.',
|
||||
);
|
||||
}
|
||||
|
||||
if (isObject(src.youtubeSubgen.ai)) {
|
||||
const aiModel = asString(src.youtubeSubgen.ai.model);
|
||||
if (aiModel !== undefined) {
|
||||
resolved.youtubeSubgen.ai.model = aiModel;
|
||||
} else if (src.youtubeSubgen.ai.model !== undefined) {
|
||||
warn(
|
||||
'youtubeSubgen.ai.model',
|
||||
src.youtubeSubgen.ai.model,
|
||||
resolved.youtubeSubgen.ai.model,
|
||||
'Expected string.',
|
||||
);
|
||||
}
|
||||
|
||||
const aiSystemPrompt = asString(src.youtubeSubgen.ai.systemPrompt);
|
||||
if (aiSystemPrompt !== undefined) {
|
||||
resolved.youtubeSubgen.ai.systemPrompt = aiSystemPrompt;
|
||||
} else if (src.youtubeSubgen.ai.systemPrompt !== undefined) {
|
||||
warn(
|
||||
'youtubeSubgen.ai.systemPrompt',
|
||||
src.youtubeSubgen.ai.systemPrompt,
|
||||
resolved.youtubeSubgen.ai.systemPrompt,
|
||||
'Expected string.',
|
||||
);
|
||||
}
|
||||
} else if (src.youtubeSubgen.ai !== undefined) {
|
||||
warn('youtubeSubgen.ai', src.youtubeSubgen.ai, resolved.youtubeSubgen.ai, 'Expected object.');
|
||||
}
|
||||
|
||||
if (src.youtubeSubgen.primarySubLanguages !== undefined) {
|
||||
warn(
|
||||
'youtubeSubgen.primarySubLanguages',
|
||||
src.youtubeSubgen.primarySubLanguages,
|
||||
undefined,
|
||||
'Removed. Use youtube.primarySubLanguages instead.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(src.subtitleStyle)) {
|
||||
const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt;
|
||||
const fallbackSubtitleStylePrimaryDefaultMode = resolved.subtitleStyle.primaryDefaultMode;
|
||||
|
||||
@@ -328,7 +328,6 @@ test('settings registry hides app-managed and inactive config surfaces', () => {
|
||||
'controller.preferredGamepadId',
|
||||
'controller.preferredGamepadLabel',
|
||||
'controller.profiles',
|
||||
'youtubeSubgen.whisperBin',
|
||||
'jellyfin.defaultLibraryId',
|
||||
'subtitleSidebar.toggleKey',
|
||||
'jellyfin.recentServers',
|
||||
|
||||
@@ -61,7 +61,6 @@ export const LEGACY_HIDDEN_CONFIG_PATHS = [
|
||||
'controller.preferredGamepadId',
|
||||
'controller.preferredGamepadLabel',
|
||||
'controller.profiles',
|
||||
'youtubeSubgen.primarySubLanguages',
|
||||
'anilist.characterDictionary.enabled',
|
||||
'anilist.characterDictionary.refreshTtlHours',
|
||||
'anilist.characterDictionary.evictionPolicy',
|
||||
@@ -76,12 +75,7 @@ export const LEGACY_HIDDEN_CONFIG_PATHS = [
|
||||
'jellyfin.recentServers',
|
||||
] as const;
|
||||
|
||||
const EXCLUDED_PREFIXES = [
|
||||
'ai',
|
||||
'ankiConnect.ai',
|
||||
'controller.buttonIndices',
|
||||
'youtubeSubgen',
|
||||
] as const;
|
||||
const EXCLUDED_PREFIXES = ['ai', 'ankiConnect.ai', 'controller.buttonIndices'] as const;
|
||||
|
||||
const JSON_OBJECT_FIELDS = new Set([
|
||||
'keybindings',
|
||||
@@ -525,7 +519,6 @@ function topSection(path: string): string {
|
||||
websocket: 'WebSocket server',
|
||||
yomitan: 'Yomitan',
|
||||
youtube: 'YouTube Playback Settings',
|
||||
youtubeSubgen: 'YouTube subtitle generation',
|
||||
auto_start_overlay: 'Playback Behavior',
|
||||
};
|
||||
return labels[top] ?? humanizePath(top);
|
||||
@@ -725,9 +718,7 @@ function fieldForLeaf(leaf: Leaf): ConfigSettingsField {
|
||||
...(option?.enumLabels ? { enumLabels: option.enumLabels } : {}),
|
||||
restartBehavior: getConfigHotReloadField(leaf.path) ? 'hot-reload' : 'restart',
|
||||
advanced:
|
||||
leaf.path.startsWith('controller.') ||
|
||||
leaf.path.startsWith('immersionTracking.retention.') ||
|
||||
leaf.path.startsWith('youtubeSubgen.'),
|
||||
leaf.path.startsWith('controller.') || leaf.path.startsWith('immersionTracking.retention.'),
|
||||
secret: SECRET_PATHS.has(leaf.path),
|
||||
settingsHidden: SUBTITLE_CSS_MANAGED_CONFIG_PATHS.has(leaf.path),
|
||||
};
|
||||
|
||||
@@ -21,13 +21,6 @@ const SUBTITLE_CSS_SCOPES: SubtitleCssScope[] = ['primary', 'secondary', 'sideba
|
||||
const HIDDEN_TEMPLATE_PATHS = [
|
||||
'anilist.characterDictionary.evictionPolicy',
|
||||
'anilist.characterDictionary.refreshTtlHours',
|
||||
'youtubeSubgen.ai.model',
|
||||
'youtubeSubgen.ai.systemPrompt',
|
||||
'youtubeSubgen.fixWithAi',
|
||||
'youtubeSubgen.whisperBin',
|
||||
'youtubeSubgen.whisperModel',
|
||||
'youtubeSubgen.whisperThreads',
|
||||
'youtubeSubgen.whisperVadModel',
|
||||
];
|
||||
|
||||
function normalizeCommentText(value: string): string {
|
||||
|
||||
Reference in new issue
Block a user