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
+73
View File
@@ -11,6 +11,78 @@ export type CardKind = 'sentence' | 'audio' | 'word-and-sentence' | 'click';
/** Card kind SubMiner flags on word cards; 'none' leaves the flag fields untouched. */
export type WordCardKind = CardKind | 'none';
export type MediaTimingReviewKind = 'word' | 'sentence' | 'audio';
export interface MediaTimingReviewRequest {
kind: MediaTimingReviewKind;
text: string;
startTime: number;
endTime: number;
noteId?: number;
audioPadding: number;
maxMediaDuration: number;
}
/** A subtitle line adjacent to the mined one that the review can pull onto the card. */
export interface MediaTimingReviewContextLine {
text: string;
startTime: number;
endTime: number;
}
export type MediaTimingReviewDecision =
/** `text` is set when the review combined adjacent lines into the card sentence. */
| { action: 'confirm'; startTime: number; endTime: number; text?: string }
| { action: 'use-original' }
| { action: 'skip-media' }
| { action: 'discard' };
export interface MediaTimingReviewOpenPayload {
reviewId: string;
kind: MediaTimingReviewKind;
text: string;
/** Lines before/after the mined one, both chronological: nearest previous line is last, nearest next line is first. */
previousLines: MediaTimingReviewContextLine[];
nextLines: MediaTimingReviewContextLine[];
noteId?: number;
originalStartTime: number;
originalEndTime: number;
selectionStartTime: number;
selectionEndTime: number;
timelineStartTime: number;
timelineEndTime: number;
mediaDuration?: number;
maxMediaDuration: number;
}
export interface MediaTimingReviewPreviewRequest {
reviewId: string;
startTime: number;
endTime: number;
}
export interface MediaTimingReviewWaveformRequest {
reviewId: string;
startTime: number;
endTime: number;
}
export interface MediaTimingReviewWaveformResult extends MediaTimingReviewActionResult {
peaks?: number[];
}
export interface MediaTimingReviewResolveRequest {
reviewId: string;
decision: MediaTimingReviewDecision;
}
export interface MediaTimingReviewActionResult {
ok: boolean;
message?: string;
/** The review this request targeted has already ended; the renderer should close. */
stale?: boolean;
}
export interface NotificationOptions {
body?: string;
icon?: string;
@@ -85,6 +157,7 @@ export interface AnkiConnectConfig {
syncAnimatedImageToWordAudio?: boolean;
normalizeAudio?: boolean;
mirrorMpvVolume?: boolean;
reviewTiming?: boolean;
audioPadding?: number;
fallbackDuration?: number;
maxMediaDuration?: number;
+5
View File
@@ -1,4 +1,5 @@
import type { AnkiConnectConfig, WordCardKind } from './anki';
import type { SubtitleGenerationConfig } from '../shared/subtitle-generation';
import type {
AiConfig,
AiFeatureConfig,
@@ -125,6 +126,7 @@ export interface ShortcutsConfig {
openRuntimeOptions?: string | null;
openJimaku?: string | null;
openTsukihime?: string | null;
openSubtitleGeneration?: string | null;
openSessionHelp?: string | null;
openControllerSelect?: string | null;
openControllerDebug?: string | null;
@@ -150,6 +152,7 @@ export interface Config {
shortcuts?: RawShortcutsConfig;
secondarySub?: SecondarySubConfig;
subsync?: SubsyncConfig;
subtitleGeneration?: Partial<SubtitleGenerationConfig>;
startupWarmups?: StartupWarmupsConfig;
subtitleStyle?: SubtitleStyleConfig;
subtitleSidebar?: SubtitleSidebarConfig;
@@ -250,6 +253,7 @@ export interface ResolvedConfig {
syncAnimatedImageToWordAudio: boolean;
normalizeAudio: boolean;
mirrorMpvVolume: boolean;
reviewTiming: boolean;
audioPadding: number;
fallbackDuration: number;
maxMediaDuration: number;
@@ -299,6 +303,7 @@ export interface ResolvedConfig {
shortcuts: Required<ShortcutsConfig>;
secondarySub: Required<SecondarySubConfig>;
subsync: Required<SubsyncConfig>;
subtitleGeneration: SubtitleGenerationConfig;
startupWarmups: {
lowPowerMode: boolean;
mecab: boolean;
+1
View File
@@ -1,5 +1,6 @@
export type RuntimeOptionId =
| 'anki.autoUpdateNewCards'
| 'anki.mediaReviewTiming'
| 'subtitle.annotation.knownWords.highlightEnabled'
| 'subtitle.annotation.knownWords.maturityEnabled'
| 'subtitle.annotation.nPlusOne'
+41
View File
@@ -1,11 +1,24 @@
import type { MpvInputBindingsSnapshot } from './session-bindings';
import type {
KikuFieldGroupingChoice,
KikuFieldGroupingRequestData,
KikuMergePreviewRequest,
KikuMergePreviewResponse,
MediaTimingReviewActionResult,
MediaTimingReviewOpenPayload,
MediaTimingReviewPreviewRequest,
MediaTimingReviewResolveRequest,
MediaTimingReviewWaveformRequest,
MediaTimingReviewWaveformResult,
} from './anki';
import type { ChangelogSnapshot } from './changelog';
import type { OverlayHostedModal } from '../shared/ipc/contracts';
import type { SubtitleGenerationProgress } from '../shared/subtitle-generation';
import type { SubtitleGenerationModelId } from '../shared/subtitle-generation-model-catalog';
import type {
SubtitleGenerationResult,
SubtitleGenerationStatus,
} from '../shared/subtitle-generation-ipc';
import type { ResolvedConfig, ShortcutsConfig } from './config';
import type {
CompiledSessionBinding,
@@ -425,6 +438,20 @@ export interface SessionNumericSelectionStartPayload {
}
export interface ElectronAPI {
requestSubtitleGenerationOpen: () => Promise<boolean>;
onSubtitleGenerationOpen: (callback: () => void) => void;
getSubtitleGenerationStatus: () => Promise<SubtitleGenerationStatus>;
selectSubtitleGenerationModel: (
model: SubtitleGenerationModelId,
) => Promise<SubtitleGenerationStatus>;
startSubtitleGeneration: () => Promise<SubtitleGenerationResult>;
downloadSubtitleGenerationModel: () => Promise<SubtitleGenerationResult>;
downloadSubtitleGenerationVadModel: () => Promise<SubtitleGenerationResult>;
setSubtitleGenerationVadEnabled: (enabled: boolean) => Promise<SubtitleGenerationStatus>;
cancelSubtitleGeneration: () => Promise<void>;
onSubtitleGenerationProgress: (
callback: (progress: SubtitleGenerationProgress) => void,
) => () => void;
getOverlayLayer: () => 'visible' | 'modal' | null;
getPathForFile: (file: File) => string;
onSubtitle: (callback: (data: SubtitleData) => void) => void;
@@ -443,6 +470,7 @@ export interface ElectronAPI {
getCurrentSubtitleRaw: () => Promise<string>;
getCurrentSubtitleAss: () => Promise<string>;
getSubtitleSidebarSnapshot: () => Promise<SubtitleSidebarSnapshot>;
copySubtitleSidebarSelection: (text: string) => Promise<void>;
getSubtitleSidebarOpen: () => Promise<boolean>;
getPlaybackPaused: () => Promise<boolean | null>;
onSubtitleAss: (callback: (assText: string) => void) => void;
@@ -456,6 +484,7 @@ export interface ElectronAPI {
setMecabEnabled: (enabled: boolean) => void;
sendMpvCommand: (command: (string | number)[]) => void;
getKeybindings: () => Promise<Keybinding[]>;
getMpvInputBindings: () => Promise<MpvInputBindingsSnapshot>;
getSessionBindings: () => Promise<CompiledSessionBinding[]>;
getConfiguredShortcuts: () => Promise<Required<ShortcutsConfig>>;
dispatchSessionAction: (
@@ -519,6 +548,18 @@ export interface ElectronAPI {
onOpenJimaku: (callback: () => void) => void;
onOpenTsukihime: (callback: () => void) => void;
onOpenYoutubeTrackPicker: (callback: (payload: YoutubePickerOpenPayload) => void) => void;
onOpenMediaTimingReview: (callback: (payload: MediaTimingReviewOpenPayload) => void) => void;
onMediaTimingReviewPreviewEnded: (callback: (reviewId: string) => void) => void;
previewMediaTimingReview: (
request: MediaTimingReviewPreviewRequest,
) => Promise<MediaTimingReviewActionResult>;
getMediaTimingReviewWaveform: (
request: MediaTimingReviewWaveformRequest,
) => Promise<MediaTimingReviewWaveformResult>;
stopMediaTimingReviewPreview: (reviewId: string) => Promise<MediaTimingReviewActionResult>;
resolveMediaTimingReview: (
request: MediaTimingReviewResolveRequest,
) => Promise<MediaTimingReviewActionResult>;
onOpenPlaylistBrowser: (callback: () => void) => void;
onOpenCharacterDictionaryManager: (callback: () => void) => void;
onSubtitleSidebarToggle: (callback: () => void) => void;
+6
View File
@@ -23,6 +23,7 @@ export type SessionActionId =
| 'openControllerDebug'
| 'openJimaku'
| 'openTsukihime'
| 'openSubtitleGeneration'
| 'openYoutubePicker'
| 'openPlaylistBrowser'
| 'openAnimeBrowser'
@@ -35,6 +36,11 @@ export interface SessionKeySpec {
modifiers: SessionKeyModifier[];
}
export interface MpvInputBindingsSnapshot {
keys: string[];
blockedKeys: SessionKeySpec[];
}
export interface SessionBindingWarning {
kind: 'unsupported' | 'conflict' | 'deprecated-config';
path: string;
+3
View File
@@ -227,6 +227,7 @@ export interface SubtitleData {
}
export interface SubtitleSidebarSnapshot {
sourceKey: string | null;
cues: SubtitleCue[];
currentTimeSec?: number | null;
currentSubtitle: {
@@ -243,6 +244,8 @@ export interface SubtitleMiningContext {
startTime: number;
endTime: number;
capturedAtMs?: number;
/** Explicit generator padding. Confirmed timing-review ranges set this to zero. */
mediaPaddingSeconds?: number;
}
export interface SubtitleHoverTokenPayload {