mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-27 06:12:05 -07:00
fix(launcher): remove youtube subtitle mode
This commit is contained in:
@@ -34,6 +34,13 @@ test('loads defaults when config is missing', () => {
|
||||
assert.equal(config.jellyfin.remoteControlAutoConnect, true);
|
||||
assert.equal(config.jellyfin.autoAnnounce, false);
|
||||
assert.equal(config.jellyfin.remoteControlDeviceName, 'SubMiner');
|
||||
assert.equal(config.ai.enabled, false);
|
||||
assert.equal(config.ai.apiKeyCommand, '');
|
||||
assert.deepEqual(config.ankiConnect.ai, {
|
||||
enabled: false,
|
||||
model: '',
|
||||
systemPrompt: '',
|
||||
});
|
||||
assert.equal(config.startupWarmups.lowPowerMode, false);
|
||||
assert.equal(config.startupWarmups.mecab, true);
|
||||
assert.equal(config.startupWarmups.yomitanExtension, true);
|
||||
@@ -1068,12 +1075,20 @@ test('parses global shortcuts and startup settings', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ai": {
|
||||
"enabled": true,
|
||||
"apiKeyCommand": "pass show subminer/ai",
|
||||
"model": "openai/gpt-4o-mini"
|
||||
},
|
||||
"shortcuts": {
|
||||
"toggleVisibleOverlayGlobal": "Alt+Shift+U",
|
||||
"openJimaku": "Ctrl+Alt+J"
|
||||
},
|
||||
"youtubeSubgen": {
|
||||
"primarySubLanguages": ["ja", "jpn", "jp"]
|
||||
"primarySubLanguages": ["ja", "jpn", "jp"],
|
||||
"whisperVadModel": "/models/vad.bin",
|
||||
"whisperThreads": 12,
|
||||
"fixWithAi": true
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
@@ -1081,9 +1096,14 @@ test('parses global shortcuts and startup settings', () => {
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
assert.equal(config.ai.enabled, true);
|
||||
assert.equal(config.ai.apiKeyCommand, 'pass show subminer/ai');
|
||||
assert.equal(config.shortcuts.toggleVisibleOverlayGlobal, 'Alt+Shift+U');
|
||||
assert.equal(config.shortcuts.openJimaku, 'Ctrl+Alt+J');
|
||||
assert.deepEqual(config.youtubeSubgen.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('runtime options registry is centralized', () => {
|
||||
@@ -1324,14 +1344,86 @@ test('supports legacy ankiConnect.behavior N+1 settings as fallback', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('warns when ankiConnect.openRouter is used and migrates to ai', () => {
|
||||
test('accepts top-level ai config', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ai": {
|
||||
"enabled": true,
|
||||
"apiKey": "abc123",
|
||||
"apiKeyCommand": "pass show subminer/ai",
|
||||
"baseUrl": "https://openrouter.ai/api",
|
||||
"model": "openrouter/test-model",
|
||||
"systemPrompt": "Return only fixed subtitles.",
|
||||
"requestTimeoutMs": 20000
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
assert.equal(config.ai.enabled, true);
|
||||
assert.equal(config.ai.apiKey, 'abc123');
|
||||
assert.equal(config.ai.apiKeyCommand, 'pass show subminer/ai');
|
||||
assert.equal(config.ai.baseUrl, 'https://openrouter.ai/api');
|
||||
assert.equal(config.ai.model, 'openrouter/test-model');
|
||||
assert.equal(config.ai.systemPrompt, 'Return only fixed subtitles.');
|
||||
assert.equal(config.ai.requestTimeoutMs, 20000);
|
||||
});
|
||||
|
||||
test('accepts per-feature ai overrides for anki and youtube subtitle generation', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ai": {
|
||||
"enabled": true,
|
||||
"apiKeyCommand": "pass show subminer/ai",
|
||||
"baseUrl": "https://openrouter.ai/api",
|
||||
"model": "openrouter/shared-model",
|
||||
"systemPrompt": "Legacy shared prompt."
|
||||
},
|
||||
"ankiConnect": {
|
||||
"ai": {
|
||||
"enabled": true,
|
||||
"model": "openrouter/anki-model",
|
||||
"systemPrompt": "Translate mined sentence text."
|
||||
}
|
||||
},
|
||||
"youtubeSubgen": {
|
||||
"ai": {
|
||||
"model": "openrouter/subgen-model",
|
||||
"systemPrompt": "Fix subtitle mistakes only."
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.equal(config.ai.enabled, true);
|
||||
assert.equal(config.ai.model, 'openrouter/shared-model');
|
||||
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', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"openRouter": {
|
||||
"model": "openrouter/test-model"
|
||||
"ai": {
|
||||
"enabled": "yes",
|
||||
"model": 123,
|
||||
"systemPrompt": true
|
||||
}
|
||||
}
|
||||
}`,
|
||||
@@ -1342,13 +1434,10 @@ test('warns when ankiConnect.openRouter is used and migrates to ai', () => {
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal((config.ankiConnect.ai as Record<string, unknown>).model, 'openrouter/test-model');
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) =>
|
||||
warning.path === 'ankiConnect.openRouter' && warning.message.includes('ankiConnect.ai'),
|
||||
),
|
||||
);
|
||||
assert.deepEqual(config.ankiConnect.ai, DEFAULT_CONFIG.ankiConnect.ai);
|
||||
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.ai.enabled'));
|
||||
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.ai.model'));
|
||||
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.ai.systemPrompt'));
|
||||
});
|
||||
|
||||
test('falls back and warns when legacy ankiConnect migration values are invalid', () => {
|
||||
@@ -1547,6 +1636,7 @@ test('falls back to default when ankiConnect n+1 deck list is invalid', () => {
|
||||
|
||||
test('template generator includes known keys', () => {
|
||||
const output = generateConfigTemplate(DEFAULT_CONFIG);
|
||||
assert.match(output, /"ai":/);
|
||||
assert.match(output, /"ankiConnect":/);
|
||||
assert.match(output, /"logging":/);
|
||||
assert.match(output, /"websocket":/);
|
||||
@@ -1577,6 +1667,31 @@ test('template generator includes known keys', () => {
|
||||
output,
|
||||
/"enabled": false,? \/\/ Enable AnkiConnect integration\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"enabled": false,? \/\/ Enable AI provider usage for Anki translation\/enrichment flows\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"model": "",? \/\/ Optional model override for Anki AI translation\/enrichment flows\./,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"enabled": false,? \/\/ Enable shared OpenAI-compatible AI provider features\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"fixWithAi": false,? \/\/ Use shared AI provider to post-process whisper-generated YouTube subtitles\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"systemPrompt": "",? \/\/ Optional system prompt override for YouTube subtitle AI post-processing\./,
|
||||
);
|
||||
assert.doesNotMatch(output, /"mode": "automatic"/);
|
||||
assert.match(
|
||||
output,
|
||||
/"whisperThreads": 4,? \/\/ Thread count passed to whisper\.cpp subtitle generation runs\./,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"launchAtStartup": true,? \/\/ Launch texthooker server automatically when SubMiner starts\. Values: true \| false/,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ResolvedConfig } from '../../types';
|
||||
|
||||
export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
ResolvedConfig,
|
||||
'ankiConnect' | 'jimaku' | 'anilist' | 'jellyfin' | 'discordPresence' | 'youtubeSubgen'
|
||||
'ankiConnect' | 'jimaku' | 'anilist' | 'jellyfin' | 'discordPresence' | 'ai' | 'youtubeSubgen'
|
||||
> = {
|
||||
ankiConnect: {
|
||||
enabled: false,
|
||||
@@ -24,13 +24,8 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
},
|
||||
ai: {
|
||||
enabled: false,
|
||||
alwaysUseAiTranslation: false,
|
||||
apiKey: '',
|
||||
model: 'openai/gpt-4o-mini',
|
||||
baseUrl: 'https://openrouter.ai/api',
|
||||
targetLanguage: 'English',
|
||||
systemPrompt:
|
||||
'You are a translation engine. Return only the translated text with no explanations.',
|
||||
model: '',
|
||||
systemPrompt: '',
|
||||
},
|
||||
media: {
|
||||
generateAudio: true,
|
||||
@@ -122,10 +117,26 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
updateIntervalMs: 3_000,
|
||||
debounceMs: 750,
|
||||
},
|
||||
ai: {
|
||||
enabled: false,
|
||||
apiKey: '',
|
||||
apiKeyCommand: '',
|
||||
model: 'openai/gpt-4o-mini',
|
||||
baseUrl: 'https://openrouter.ai/api',
|
||||
systemPrompt:
|
||||
'You are a translation engine. Return only the translated text with no explanations.',
|
||||
requestTimeoutMs: 15_000,
|
||||
},
|
||||
youtubeSubgen: {
|
||||
mode: 'automatic',
|
||||
whisperBin: '',
|
||||
whisperModel: '',
|
||||
whisperVadModel: '',
|
||||
whisperThreads: 4,
|
||||
fixWithAi: false,
|
||||
ai: {
|
||||
model: '',
|
||||
systemPrompt: '',
|
||||
},
|
||||
primarySubLanguages: ['ja', 'jpn'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,6 +51,24 @@ export function buildIntegrationConfigOptionRegistry(
|
||||
description:
|
||||
'Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.',
|
||||
},
|
||||
{
|
||||
path: 'ankiConnect.ai.enabled',
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.ankiConnect.ai.enabled,
|
||||
description: 'Enable AI provider usage for Anki translation/enrichment flows.',
|
||||
},
|
||||
{
|
||||
path: 'ankiConnect.ai.model',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.ankiConnect.ai.model,
|
||||
description: 'Optional model override for Anki AI translation/enrichment flows.',
|
||||
},
|
||||
{
|
||||
path: 'ankiConnect.ai.systemPrompt',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.ankiConnect.ai.systemPrompt,
|
||||
description: 'Optional system prompt override for Anki AI translation/enrichment flows.',
|
||||
},
|
||||
{
|
||||
path: 'ankiConnect.behavior.autoUpdateNewCards',
|
||||
kind: 'boolean',
|
||||
@@ -291,11 +309,34 @@ export function buildIntegrationConfigOptionRegistry(
|
||||
description: 'Debounce delay used to collapse bursty presence updates.',
|
||||
},
|
||||
{
|
||||
path: 'youtubeSubgen.mode',
|
||||
kind: 'enum',
|
||||
enumValues: ['automatic', 'preprocess', 'off'],
|
||||
defaultValue: defaultConfig.youtubeSubgen.mode,
|
||||
description: 'YouTube subtitle generation mode for the launcher script.',
|
||||
path: 'ai.enabled',
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.ai.enabled,
|
||||
description: 'Enable shared OpenAI-compatible AI provider features.',
|
||||
},
|
||||
{
|
||||
path: 'ai.apiKey',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.ai.apiKey,
|
||||
description: 'Static API key for the shared OpenAI-compatible AI provider.',
|
||||
},
|
||||
{
|
||||
path: 'ai.apiKeyCommand',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.ai.apiKeyCommand,
|
||||
description: 'Shell command used to resolve the shared AI provider API key.',
|
||||
},
|
||||
{
|
||||
path: 'ai.baseUrl',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.ai.baseUrl,
|
||||
description: 'Base URL for the shared OpenAI-compatible AI provider.',
|
||||
},
|
||||
{
|
||||
path: 'ai.requestTimeoutMs',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.ai.requestTimeoutMs,
|
||||
description: 'Timeout in milliseconds for shared AI provider requests.',
|
||||
},
|
||||
{
|
||||
path: 'youtubeSubgen.whisperBin',
|
||||
@@ -309,6 +350,36 @@ export function buildIntegrationConfigOptionRegistry(
|
||||
defaultValue: defaultConfig.youtubeSubgen.whisperModel,
|
||||
description: 'Path to whisper model used for fallback transcription.',
|
||||
},
|
||||
{
|
||||
path: 'youtubeSubgen.whisperVadModel',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.youtubeSubgen.whisperVadModel,
|
||||
description: 'Path to optional whisper VAD model used for subtitle generation.',
|
||||
},
|
||||
{
|
||||
path: 'youtubeSubgen.whisperThreads',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.youtubeSubgen.whisperThreads,
|
||||
description: 'Thread count passed to whisper.cpp subtitle generation runs.',
|
||||
},
|
||||
{
|
||||
path: 'youtubeSubgen.fixWithAi',
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.youtubeSubgen.fixWithAi,
|
||||
description: 'Use shared AI provider to post-process whisper-generated YouTube subtitles.',
|
||||
},
|
||||
{
|
||||
path: 'youtubeSubgen.ai.model',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.youtubeSubgen.ai.model,
|
||||
description: 'Optional model override for YouTube subtitle AI post-processing.',
|
||||
},
|
||||
{
|
||||
path: 'youtubeSubgen.ai.systemPrompt',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.youtubeSubgen.ai.systemPrompt,
|
||||
description: 'Optional system prompt override for YouTube subtitle AI post-processing.',
|
||||
},
|
||||
{
|
||||
path: 'youtubeSubgen.primarySubLanguages',
|
||||
kind: 'string',
|
||||
|
||||
@@ -91,11 +91,19 @@ const SUBTITLE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
];
|
||||
|
||||
const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
{
|
||||
title: 'Shared AI Provider',
|
||||
description: [
|
||||
'Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.',
|
||||
],
|
||||
key: 'ai',
|
||||
},
|
||||
{
|
||||
title: 'AnkiConnect Integration',
|
||||
description: ['Automatic Anki updates and media generation options.'],
|
||||
notes: [
|
||||
'Hot-reload: AI translation settings update live while SubMiner is running.',
|
||||
'Hot-reload: ankiConnect.ai.enabled updates 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.',
|
||||
],
|
||||
key: 'ankiConnect',
|
||||
@@ -107,7 +115,7 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
},
|
||||
{
|
||||
title: 'YouTube Subtitle Generation',
|
||||
description: ['Defaults for subminer YouTube subtitle extraction/transcription mode.'],
|
||||
description: ['Defaults for SubMiner YouTube subtitle generation.'],
|
||||
key: 'youtubeSubgen',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -46,18 +46,6 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
||||
}
|
||||
|
||||
if (isObject(src.youtubeSubgen)) {
|
||||
const mode = src.youtubeSubgen.mode;
|
||||
if (mode === 'automatic' || mode === 'preprocess' || mode === 'off') {
|
||||
resolved.youtubeSubgen.mode = mode;
|
||||
} else if (mode !== undefined) {
|
||||
warn(
|
||||
'youtubeSubgen.mode',
|
||||
mode,
|
||||
resolved.youtubeSubgen.mode,
|
||||
'Expected automatic, preprocess, or off.',
|
||||
);
|
||||
}
|
||||
|
||||
const whisperBin = asString(src.youtubeSubgen.whisperBin);
|
||||
if (whisperBin !== undefined) {
|
||||
resolved.youtubeSubgen.whisperBin = whisperBin;
|
||||
@@ -82,6 +70,75 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
||||
);
|
||||
}
|
||||
|
||||
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 (Array.isArray(src.youtubeSubgen.primarySubLanguages)) {
|
||||
resolved.youtubeSubgen.primarySubLanguages = src.youtubeSubgen.primarySubLanguages.filter(
|
||||
(item): item is string => typeof item === 'string',
|
||||
|
||||
Reference in New Issue
Block a user