mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-29 17:40:50 -07:00
feat(youtube): add YouTube browser window and Whisper subtitle source (#273)
This commit is contained in:
@@ -19,30 +19,16 @@ function withTempDir<T>(fn: (dir: string) => T): T {
|
||||
}
|
||||
}
|
||||
|
||||
test('createDefaultArgs normalizes configured language codes and env thread override', () => {
|
||||
const originalThreads = process.env.SUBMINER_WHISPER_THREADS;
|
||||
process.env.SUBMINER_WHISPER_THREADS = '7';
|
||||
test('createDefaultArgs normalizes configured language codes', () => {
|
||||
const parsed = createDefaultArgs({
|
||||
primarySubLanguages: [' JA ', 'jpn', 'ja'],
|
||||
secondarySubLanguages: ['en', 'ENG', ''],
|
||||
});
|
||||
|
||||
try {
|
||||
const parsed = createDefaultArgs({
|
||||
primarySubLanguages: [' JA ', 'jpn', 'ja'],
|
||||
secondarySubLanguages: ['en', 'ENG', ''],
|
||||
whisperThreads: 2,
|
||||
});
|
||||
|
||||
assert.deepEqual(parsed.youtubePrimarySubLangs, ['ja', 'jpn']);
|
||||
assert.deepEqual(parsed.youtubeSecondarySubLangs, ['en', 'eng']);
|
||||
assert.deepEqual(parsed.youtubeAudioLangs, ['ja', 'jpn', 'en', 'eng']);
|
||||
assert.equal(parsed.whisperThreads, 7);
|
||||
assert.equal(parsed.youtubeWhisperSourceLanguage, 'ja');
|
||||
assert.equal(parsed.profile, '');
|
||||
} finally {
|
||||
if (originalThreads === undefined) {
|
||||
delete process.env.SUBMINER_WHISPER_THREADS;
|
||||
} else {
|
||||
process.env.SUBMINER_WHISPER_THREADS = originalThreads;
|
||||
}
|
||||
}
|
||||
assert.deepEqual(parsed.youtubePrimarySubLangs, ['ja', 'jpn']);
|
||||
assert.deepEqual(parsed.youtubeSecondarySubLangs, ['en', 'eng']);
|
||||
assert.deepEqual(parsed.youtubeAudioLangs, ['ja', 'jpn', 'en', 'eng']);
|
||||
assert.equal(parsed.profile, '');
|
||||
});
|
||||
|
||||
test('createDefaultArgs seeds mpv profile from launcher config', () => {
|
||||
@@ -120,6 +106,7 @@ test('applyInvocationsToArgs maps config and jellyfin invocation state', () => {
|
||||
logLevel: 'warn',
|
||||
},
|
||||
settingsInvocation: null,
|
||||
youtubeBrowserInvocation: null,
|
||||
mpvInvocation: null,
|
||||
appInvocation: null,
|
||||
dictionaryTriggered: false,
|
||||
@@ -174,6 +161,7 @@ test('applyInvocationsToArgs maps settings invocation to settings window', () =>
|
||||
settingsInvocation: {
|
||||
logLevel: undefined,
|
||||
},
|
||||
youtubeBrowserInvocation: null,
|
||||
mpvInvocation: null,
|
||||
appInvocation: null,
|
||||
dictionaryTriggered: false,
|
||||
@@ -221,6 +209,7 @@ test('applyInvocationsToArgs fails when config invocation has no action', () =>
|
||||
action: undefined,
|
||||
},
|
||||
settingsInvocation: null,
|
||||
youtubeBrowserInvocation: null,
|
||||
mpvInvocation: null,
|
||||
appInvocation: null,
|
||||
dictionaryTriggered: false,
|
||||
@@ -266,6 +255,7 @@ test('applyInvocationsToArgs maps texthooker browser-open request', () => {
|
||||
jellyfinInvocation: null,
|
||||
configInvocation: null,
|
||||
settingsInvocation: null,
|
||||
youtubeBrowserInvocation: null,
|
||||
mpvInvocation: null,
|
||||
appInvocation: null,
|
||||
dictionaryTriggered: false,
|
||||
|
||||
@@ -13,14 +13,8 @@ import {
|
||||
DEFAULT_JIMAKU_API_BASE_URL,
|
||||
DEFAULT_YOUTUBE_PRIMARY_SUB_LANGS,
|
||||
DEFAULT_YOUTUBE_SECONDARY_SUB_LANGS,
|
||||
DEFAULT_YOUTUBE_SUBGEN_OUT_DIR,
|
||||
} from '../types.js';
|
||||
import {
|
||||
inferWhisperLanguage,
|
||||
isUrlTarget,
|
||||
resolvePathMaybe,
|
||||
uniqueNormalizedLangCodes,
|
||||
} from '../util.js';
|
||||
import { isUrlTarget, resolvePathMaybe, uniqueNormalizedLangCodes } from '../util.js';
|
||||
import type { CliInvocations } from './cli-parser-builder.js';
|
||||
|
||||
function ensureTarget(target: string, parsed: Args): void {
|
||||
@@ -131,18 +125,6 @@ export function createDefaultArgs(
|
||||
recursive: false,
|
||||
profile: mpvConfig.profile ?? '',
|
||||
startOverlay: false,
|
||||
whisperBin: process.env.SUBMINER_WHISPER_BIN || launcherConfig.whisperBin || '',
|
||||
whisperModel: process.env.SUBMINER_WHISPER_MODEL || launcherConfig.whisperModel || '',
|
||||
whisperVadModel: process.env.SUBMINER_WHISPER_VAD_MODEL || launcherConfig.whisperVadModel || '',
|
||||
whisperThreads: (() => {
|
||||
const envValue = Number.parseInt(process.env.SUBMINER_WHISPER_THREADS || '', 10);
|
||||
if (Number.isInteger(envValue) && envValue > 0) return envValue;
|
||||
return launcherConfig.whisperThreads || 4;
|
||||
})(),
|
||||
youtubeSubgenOutDir: process.env.SUBMINER_YT_SUBGEN_OUT_DIR || DEFAULT_YOUTUBE_SUBGEN_OUT_DIR,
|
||||
youtubeSubgenAudioFormat: process.env.SUBMINER_YT_SUBGEN_AUDIO_FORMAT || 'm4a',
|
||||
youtubeSubgenKeepTemp: process.env.SUBMINER_YT_SUBGEN_KEEP_TEMP === '1',
|
||||
youtubeFixWithAi: launcherConfig.fixWithAi === true,
|
||||
jimakuApiKey: process.env.SUBMINER_JIMAKU_API_KEY || '',
|
||||
jimakuApiKeyCommand: process.env.SUBMINER_JIMAKU_API_KEY_COMMAND || '',
|
||||
jimakuApiBaseUrl: process.env.SUBMINER_JIMAKU_API_BASE_URL || DEFAULT_JIMAKU_API_BASE_URL,
|
||||
@@ -170,6 +152,7 @@ export function createDefaultArgs(
|
||||
version: false,
|
||||
update: false,
|
||||
settings: false,
|
||||
youtubeBrowser: false,
|
||||
configPath: false,
|
||||
configShow: false,
|
||||
mpvIdle: false,
|
||||
@@ -185,16 +168,6 @@ export function createDefaultArgs(
|
||||
youtubePrimarySubLangs: primarySubLangs,
|
||||
youtubeSecondarySubLangs: secondarySubLangs,
|
||||
youtubeAudioLangs,
|
||||
youtubeWhisperSourceLanguage: inferWhisperLanguage(primarySubLangs, 'ja'),
|
||||
aiConfig: {
|
||||
enabled: launcherConfig.ai?.enabled,
|
||||
apiKey: launcherConfig.ai?.apiKey,
|
||||
apiKeyCommand: launcherConfig.ai?.apiKeyCommand,
|
||||
baseUrl: launcherConfig.ai?.baseUrl,
|
||||
model: launcherConfig.ai?.model,
|
||||
systemPrompt: launcherConfig.ai?.systemPrompt,
|
||||
requestTimeoutMs: launcherConfig.ai?.requestTimeoutMs,
|
||||
},
|
||||
useTexthooker: true,
|
||||
autoStartOverlay: false,
|
||||
texthookerOnly: false,
|
||||
@@ -363,6 +336,13 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations
|
||||
parsed.settings = true;
|
||||
}
|
||||
|
||||
if (invocations.youtubeBrowserInvocation) {
|
||||
if (invocations.youtubeBrowserInvocation.logLevel) {
|
||||
parsed.logLevel = parseLogLevel(invocations.youtubeBrowserInvocation.logLevel);
|
||||
}
|
||||
parsed.youtubeBrowser = true;
|
||||
}
|
||||
|
||||
if (invocations.mpvInvocation) {
|
||||
if (invocations.mpvInvocation.logLevel) {
|
||||
parsed.logLevel = parseLogLevel(invocations.mpvInvocation.logLevel);
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface CliInvocations {
|
||||
jellyfinInvocation: JellyfinInvocation | null;
|
||||
configInvocation: CommandActionInvocation | null;
|
||||
settingsInvocation: CommandActionInvocation | null;
|
||||
youtubeBrowserInvocation: CommandActionInvocation | null;
|
||||
mpvInvocation: CommandActionInvocation | null;
|
||||
appInvocation: { appArgs: string[] } | null;
|
||||
dictionaryTriggered: boolean;
|
||||
@@ -121,6 +122,8 @@ function getTopLevelCommand(argv: string[]): { name: string; index: number } | n
|
||||
'doctor',
|
||||
'config',
|
||||
'settings',
|
||||
'youtube',
|
||||
'yt',
|
||||
'mpv',
|
||||
'logs',
|
||||
'dictionary',
|
||||
@@ -175,6 +178,7 @@ export function parseCliPrograms(
|
||||
let jellyfinInvocation: JellyfinInvocation | null = null;
|
||||
let configInvocation: CommandActionInvocation | null = null;
|
||||
let settingsInvocation: CommandActionInvocation | null = null;
|
||||
let youtubeBrowserInvocation: CommandActionInvocation | null = null;
|
||||
let mpvInvocation: CommandActionInvocation | null = null;
|
||||
let appInvocation: { appArgs: string[] } | null = null;
|
||||
let dictionaryTriggered = false;
|
||||
@@ -512,6 +516,17 @@ export function parseCliPrograms(
|
||||
};
|
||||
});
|
||||
|
||||
commandProgram
|
||||
.command('youtube')
|
||||
.alias('yt')
|
||||
.description('Open the YouTube browser (videos play in mpv)')
|
||||
.option('--log-level <level>', 'Log level')
|
||||
.action((options: Record<string, unknown>) => {
|
||||
youtubeBrowserInvocation = {
|
||||
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
commandProgram
|
||||
.command('mpv')
|
||||
.description('MPV helpers')
|
||||
@@ -567,6 +582,7 @@ export function parseCliPrograms(
|
||||
jellyfinInvocation,
|
||||
configInvocation,
|
||||
settingsInvocation,
|
||||
youtubeBrowserInvocation,
|
||||
mpvInvocation,
|
||||
appInvocation,
|
||||
dictionaryTriggered,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { LauncherYoutubeSubgenConfig } from '../types.js';
|
||||
import { mergeAiConfig } from '../../src/ai/config.js';
|
||||
|
||||
function asStringArray(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) return undefined;
|
||||
@@ -9,11 +8,6 @@ function asStringArray(value: unknown): string[] | undefined {
|
||||
export function parseLauncherYoutubeSubgenConfig(
|
||||
root: Record<string, unknown>,
|
||||
): LauncherYoutubeSubgenConfig {
|
||||
const youtubeSubgenRaw = root.youtubeSubgen;
|
||||
const youtubeSubgen =
|
||||
youtubeSubgenRaw && typeof youtubeSubgenRaw === 'object'
|
||||
? (youtubeSubgenRaw as Record<string, unknown>)
|
||||
: null;
|
||||
const youtubeRaw = root.youtube;
|
||||
const youtube =
|
||||
youtubeRaw && typeof youtubeRaw === 'object' ? (youtubeRaw as Record<string, unknown>) : null;
|
||||
@@ -25,58 +19,11 @@ export function parseLauncherYoutubeSubgenConfig(
|
||||
const jimakuRaw = root.jimaku;
|
||||
const jimaku =
|
||||
jimakuRaw && typeof jimakuRaw === 'object' ? (jimakuRaw as Record<string, unknown>) : null;
|
||||
const aiRaw = root.ai;
|
||||
const ai = aiRaw && typeof aiRaw === 'object' ? (aiRaw as Record<string, unknown>) : null;
|
||||
const youtubeAiRaw = youtubeSubgen?.ai;
|
||||
const youtubeAi =
|
||||
youtubeAiRaw && typeof youtubeAiRaw === 'object'
|
||||
? (youtubeAiRaw as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
const jimakuLanguagePreference = jimaku?.languagePreference;
|
||||
const jimakuMaxEntryResults = jimaku?.maxEntryResults;
|
||||
|
||||
return {
|
||||
whisperBin:
|
||||
typeof youtubeSubgen?.whisperBin === 'string' ? youtubeSubgen.whisperBin : undefined,
|
||||
whisperModel:
|
||||
typeof youtubeSubgen?.whisperModel === 'string' ? youtubeSubgen.whisperModel : undefined,
|
||||
whisperVadModel:
|
||||
typeof youtubeSubgen?.whisperVadModel === 'string'
|
||||
? youtubeSubgen.whisperVadModel
|
||||
: undefined,
|
||||
whisperThreads:
|
||||
typeof youtubeSubgen?.whisperThreads === 'number' &&
|
||||
Number.isFinite(youtubeSubgen.whisperThreads) &&
|
||||
youtubeSubgen.whisperThreads > 0
|
||||
? Math.floor(youtubeSubgen.whisperThreads)
|
||||
: undefined,
|
||||
fixWithAi: typeof youtubeSubgen?.fixWithAi === 'boolean' ? youtubeSubgen.fixWithAi : undefined,
|
||||
ai: mergeAiConfig(
|
||||
ai
|
||||
? {
|
||||
enabled: typeof ai.enabled === 'boolean' ? ai.enabled : undefined,
|
||||
apiKey: typeof ai.apiKey === 'string' ? ai.apiKey : undefined,
|
||||
apiKeyCommand: typeof ai.apiKeyCommand === 'string' ? ai.apiKeyCommand : undefined,
|
||||
baseUrl: typeof ai.baseUrl === 'string' ? ai.baseUrl : undefined,
|
||||
model: typeof ai.model === 'string' ? ai.model : undefined,
|
||||
systemPrompt: typeof ai.systemPrompt === 'string' ? ai.systemPrompt : undefined,
|
||||
requestTimeoutMs:
|
||||
typeof ai.requestTimeoutMs === 'number' &&
|
||||
Number.isFinite(ai.requestTimeoutMs) &&
|
||||
ai.requestTimeoutMs > 0
|
||||
? Math.floor(ai.requestTimeoutMs)
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
youtubeAi
|
||||
? {
|
||||
model: typeof youtubeAi.model === 'string' ? youtubeAi.model : undefined,
|
||||
systemPrompt:
|
||||
typeof youtubeAi.systemPrompt === 'string' ? youtubeAi.systemPrompt : undefined,
|
||||
}
|
||||
: undefined,
|
||||
),
|
||||
primarySubLanguages: asStringArray(youtube?.primarySubLanguages),
|
||||
secondarySubLanguages: asStringArray(secondarySub?.secondarySubLanguages),
|
||||
jimakuApiKey: typeof jimaku?.apiKey === 'string' ? jimaku.apiKey : undefined,
|
||||
|
||||
Reference in New Issue
Block a user