mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -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',
|
||||
|
||||
@@ -108,3 +108,49 @@ test('initializeOverlayRuntime starts Anki integration when ankiConnect.enabled
|
||||
assert.equal(startedIntegrations, 1);
|
||||
assert.equal(setIntegrationCalls, 1);
|
||||
});
|
||||
|
||||
test('initializeOverlayRuntime re-syncs overlay shortcuts when tracker focus changes', () => {
|
||||
let syncCalls = 0;
|
||||
const tracker = {
|
||||
onGeometryChange: null as ((...args: unknown[]) => void) | null,
|
||||
onWindowFound: null as ((...args: unknown[]) => void) | null,
|
||||
onWindowLost: null as (() => void) | null,
|
||||
onWindowFocusChange: null as ((focused: boolean) => void) | null,
|
||||
start: () => {},
|
||||
};
|
||||
|
||||
initializeOverlayRuntime({
|
||||
backendOverride: null,
|
||||
createMainWindow: () => {},
|
||||
registerGlobalShortcuts: () => {},
|
||||
updateVisibleOverlayBounds: () => {},
|
||||
isVisibleOverlayVisible: () => false,
|
||||
updateVisibleOverlayVisibility: () => {},
|
||||
getOverlayWindows: () => [],
|
||||
syncOverlayShortcuts: () => {
|
||||
syncCalls += 1;
|
||||
},
|
||||
setWindowTracker: () => {},
|
||||
getMpvSocketPath: () => '/tmp/mpv.sock',
|
||||
createWindowTracker: () => tracker as never,
|
||||
getResolvedConfig: () => ({
|
||||
ankiConnect: { enabled: false } as never,
|
||||
}),
|
||||
getSubtitleTimingTracker: () => null,
|
||||
getMpvClient: () => null,
|
||||
getRuntimeOptionsManager: () => null,
|
||||
setAnkiIntegration: () => {},
|
||||
showDesktopNotification: () => {},
|
||||
createFieldGroupingCallback: () => async () => ({
|
||||
keepNoteId: 1,
|
||||
deleteNoteId: 2,
|
||||
deleteDuplicate: false,
|
||||
cancelled: false,
|
||||
}),
|
||||
getKnownWordCacheStatePath: () => '/tmp/known-words-cache.json',
|
||||
});
|
||||
|
||||
assert.equal(typeof tracker.onWindowFocusChange, 'function');
|
||||
tracker.onWindowFocusChange?.(true);
|
||||
assert.equal(syncCalls, 1);
|
||||
});
|
||||
|
||||
@@ -101,6 +101,9 @@ export function initializeOverlayRuntime(options: {
|
||||
}
|
||||
options.syncOverlayShortcuts();
|
||||
};
|
||||
windowTracker.onWindowFocusChange = () => {
|
||||
options.syncOverlayShortcuts();
|
||||
};
|
||||
windowTracker.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -969,6 +969,8 @@ const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService(
|
||||
appState.shortcutsRegistered = registered;
|
||||
},
|
||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||
isMacOSPlatform: () => process.platform === 'darwin',
|
||||
isTrackedMpvWindowFocused: () => appState.windowTracker?.isFocused() ?? false,
|
||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||
openRuntimeOptionsPalette: () => {
|
||||
openRuntimeOptionsPalette();
|
||||
|
||||
63
src/types.ts
63
src/types.ts
@@ -227,24 +227,7 @@ export interface AnkiConnectConfig {
|
||||
miscInfo?: string;
|
||||
translation?: string;
|
||||
};
|
||||
ai?: {
|
||||
enabled?: boolean;
|
||||
alwaysUseAiTranslation?: boolean;
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
baseUrl?: string;
|
||||
targetLanguage?: string;
|
||||
systemPrompt?: string;
|
||||
};
|
||||
openRouter?: {
|
||||
enabled?: boolean;
|
||||
alwaysUseAiTranslation?: boolean;
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
baseUrl?: string;
|
||||
targetLanguage?: string;
|
||||
systemPrompt?: string;
|
||||
};
|
||||
ai?: boolean | AiFeatureConfig;
|
||||
media?: {
|
||||
generateAudio?: boolean;
|
||||
generateImage?: boolean;
|
||||
@@ -455,12 +438,29 @@ export interface DiscordPresenceConfig {
|
||||
debounceMs?: number;
|
||||
}
|
||||
|
||||
export type YoutubeSubgenMode = 'automatic' | 'preprocess' | 'off';
|
||||
export interface AiFeatureConfig {
|
||||
enabled?: boolean;
|
||||
model?: string;
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
export interface AiConfig {
|
||||
enabled?: boolean;
|
||||
apiKey?: string;
|
||||
apiKeyCommand?: string;
|
||||
baseUrl?: string;
|
||||
model?: string;
|
||||
systemPrompt?: string;
|
||||
requestTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface YoutubeSubgenConfig {
|
||||
mode?: YoutubeSubgenMode;
|
||||
whisperBin?: string;
|
||||
whisperModel?: string;
|
||||
whisperVadModel?: string;
|
||||
whisperThreads?: number;
|
||||
fixWithAi?: boolean;
|
||||
ai?: AiFeatureConfig;
|
||||
primarySubLanguages?: string[];
|
||||
}
|
||||
|
||||
@@ -498,6 +498,7 @@ export interface Config {
|
||||
anilist?: AnilistConfig;
|
||||
jellyfin?: JellyfinConfig;
|
||||
discordPresence?: DiscordPresenceConfig;
|
||||
ai?: AiConfig;
|
||||
youtubeSubgen?: YoutubeSubgenConfig;
|
||||
immersionTracking?: ImmersionTrackingConfig;
|
||||
logging?: {
|
||||
@@ -531,14 +532,8 @@ export interface ResolvedConfig {
|
||||
miscInfo: string;
|
||||
translation: string;
|
||||
};
|
||||
ai: {
|
||||
ai: AiFeatureConfig & {
|
||||
enabled: boolean;
|
||||
alwaysUseAiTranslation: boolean;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
baseUrl: string;
|
||||
targetLanguage: string;
|
||||
systemPrompt: string;
|
||||
};
|
||||
media: {
|
||||
generateAudio: boolean;
|
||||
@@ -649,10 +644,22 @@ export interface ResolvedConfig {
|
||||
updateIntervalMs: number;
|
||||
debounceMs: number;
|
||||
};
|
||||
ai: AiConfig & {
|
||||
enabled: boolean;
|
||||
apiKey: string;
|
||||
apiKeyCommand: string;
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
systemPrompt: string;
|
||||
requestTimeoutMs: number;
|
||||
};
|
||||
youtubeSubgen: YoutubeSubgenConfig & {
|
||||
mode: YoutubeSubgenMode;
|
||||
whisperBin: string;
|
||||
whisperModel: string;
|
||||
whisperVadModel: string;
|
||||
whisperThreads: number;
|
||||
fixWithAi: boolean;
|
||||
ai: AiFeatureConfig;
|
||||
primarySubLanguages: string[];
|
||||
};
|
||||
immersionTracking: {
|
||||
|
||||
Reference in New Issue
Block a user