feat: add subtitle generation and bundle Bun launcher runtime

- Add local subtitle generation and card timing review workflows
- Package cross-platform Bun runtimes, launchers, licenses, and source
- Consolidate release packaging and refresh v0.19.6 documentation
This commit is contained in:
2026-09-15 21:59:10 -07:00
279 changed files with 23054 additions and 1603 deletions
+57
View File
@@ -1,4 +1,9 @@
import { Command } from 'commander';
import type { Args } from '../types.js';
import {
isSubtitleGenerationModelId,
SUBTITLE_GENERATION_MODELS,
} from '../../src/shared/subtitle-generation-model-catalog.js';
export interface JellyfinInvocation {
action?: string;
@@ -20,6 +25,7 @@ export interface CommandActionInvocation {
}
export interface CliInvocations {
generateSubtitles?: Args['generateSubtitles'];
jellyfinInvocation: JellyfinInvocation | null;
configInvocation: CommandActionInvocation | null;
settingsInvocation: CommandActionInvocation | null;
@@ -120,6 +126,7 @@ function getTopLevelCommand(argv: string[]): { name: string; index: number } | n
'mpv',
'logs',
'dictionary',
'generate-subs',
'dict',
'stats',
'sync',
@@ -202,6 +209,7 @@ export function parseCliPrograms(
let texthookerOpenBrowser = false;
let doctorTriggered = false;
let texthookerTriggered = false;
let generateSubtitles: Args['generateSubtitles'];
const commandProgram = new Command();
commandProgram
@@ -226,6 +234,50 @@ export function parseCliPrograms(
.argument('[target]', 'file, directory, or URL');
applyRootOptions(rootProgram);
commandProgram
.command('generate-subs')
.description('Generate Japanese subtitles locally with whisper.cpp')
.argument('[video]', 'Local media file, or the current mpv file if omitted')
.option('--download-model', 'Download the selected managed model if missing')
.option('--model-path <path>', 'Use an existing whisper.cpp model file')
.option(
'--model <name>',
`Managed model: ${SUBTITLE_GENERATION_MODELS.map((model) => model.id).join(', ')}`,
)
.option('--output <path>', 'Save subtitles to this SRT path')
.option('--audio-stream <index>', 'Absolute audio stream index from ffprobe')
.action((mediaPath: string | undefined, options: Record<string, unknown>) => {
const model = options.model;
if (model !== undefined && !isSubtitleGenerationModelId(model)) {
throw new Error(
`Generation --model must be one of: ${SUBTITLE_GENERATION_MODELS.map((entry) => entry.id).join(', ')}.`,
);
}
if (
options.modelPath !== undefined &&
(model !== undefined || options.downloadModel === true)
) {
throw new Error(
'Generation --model-path cannot be combined with --model or --download-model.',
);
}
let audioStreamIndex: number | undefined;
if (typeof options.audioStream === 'string') {
audioStreamIndex = Number(options.audioStream);
if (!/^\d+$/.test(options.audioStream) || !Number.isSafeInteger(audioStreamIndex)) {
throw new Error('Generation --audio-stream must be a non-negative integer stream index.');
}
}
generateSubtitles = {
mediaPath,
downloadModel: options.downloadModel === true,
modelPath: typeof options.modelPath === 'string' ? options.modelPath : undefined,
managedModel: model,
outputPath: typeof options.output === 'string' ? options.output : undefined,
audioStreamIndex,
};
});
commandProgram
.command('jellyfin')
.alias('jf')
@@ -363,6 +415,7 @@ export function parseCliPrograms(
.option('--json', 'Emit machine-readable NDJSON progress output')
.option('--make-temp', 'Create a sync temp directory and print its path (used over SSH)')
.option('--remove-temp <dir>', 'Remove a sync temp directory created by --make-temp')
.option('--transfer-cache <key>', 'Reuse/save a received snapshot with temp helpers (internal)')
.option('--ui', 'Open the SubMiner sync window')
.option('--log-level <level>', 'Log level')
.action((rawHost: string | undefined, options: Record<string, unknown>) => {
@@ -384,6 +437,7 @@ export function parseCliPrograms(
check ||
makeTemp ||
removeTemp ||
options.transferCache !== undefined ||
options.remoteCmd !== undefined ||
options.db !== undefined ||
options.json === true ||
@@ -405,6 +459,8 @@ export function parseCliPrograms(
if (merge) tokens.push('--merge', merge);
if (makeTemp) tokens.push('--make-temp');
if (removeTemp) tokens.push('--remove-temp', removeTemp);
if (typeof options.transferCache === 'string')
tokens.push('--transfer-cache', options.transferCache);
if (push) tokens.push('--push');
if (pull) tokens.push('--pull');
if (check) tokens.push('--check');
@@ -520,6 +576,7 @@ export function parseCliPrograms(
options: selectedProgram.opts<Record<string, unknown>>(),
rootTarget: rootProgram.processedArgs[0],
invocations: {
generateSubtitles,
jellyfinInvocation,
configInvocation,
settingsInvocation,