mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
619 lines
15 KiB
TypeScript
619 lines
15 KiB
TypeScript
/*
|
|
* SubMiner - All-in-one sentence mining overlay
|
|
* Copyright (C) 2024 sudacode
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
export enum PartOfSpeech {
|
|
noun = "noun",
|
|
verb = "verb",
|
|
i_adjective = "i_adjective",
|
|
na_adjective = "na_adjective",
|
|
particle = "particle",
|
|
bound_auxiliary = "bound_auxiliary",
|
|
symbol = "symbol",
|
|
other = "other",
|
|
}
|
|
|
|
export interface Token {
|
|
word: string;
|
|
partOfSpeech: PartOfSpeech;
|
|
pos1: string;
|
|
pos2: string;
|
|
pos3: string;
|
|
pos4: string;
|
|
inflectionType: string;
|
|
inflectionForm: string;
|
|
headword: string;
|
|
katakanaReading: string;
|
|
pronunciation: string;
|
|
}
|
|
|
|
export interface MergedToken {
|
|
surface: string;
|
|
reading: string;
|
|
headword: string;
|
|
startPos: number;
|
|
endPos: number;
|
|
partOfSpeech: PartOfSpeech;
|
|
isMerged: boolean;
|
|
}
|
|
|
|
export interface WindowGeometry {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export interface SubtitlePosition {
|
|
yPercent: number;
|
|
}
|
|
|
|
export interface SubtitleStyle {
|
|
fontSize: number;
|
|
}
|
|
|
|
export interface Keybinding {
|
|
key: string;
|
|
command: (string | number)[] | null;
|
|
}
|
|
|
|
export type SecondarySubMode = "hidden" | "visible" | "hover";
|
|
|
|
export interface SecondarySubConfig {
|
|
secondarySubLanguages?: string[];
|
|
autoLoadSecondarySub?: boolean;
|
|
defaultMode?: SecondarySubMode;
|
|
}
|
|
|
|
export type SubsyncMode = "auto" | "manual";
|
|
|
|
export interface SubsyncConfig {
|
|
defaultMode?: SubsyncMode;
|
|
alass_path?: string;
|
|
ffsubsync_path?: string;
|
|
ffmpeg_path?: string;
|
|
}
|
|
|
|
export interface WebSocketConfig {
|
|
enabled?: boolean | "auto";
|
|
port?: number;
|
|
}
|
|
|
|
export interface TexthookerConfig {
|
|
openBrowser?: boolean;
|
|
}
|
|
|
|
export interface NotificationOptions {
|
|
body?: string;
|
|
icon?: string;
|
|
}
|
|
|
|
export interface MpvClient {
|
|
currentSubText: string;
|
|
currentVideoPath: string;
|
|
currentTimePos: number;
|
|
currentSubStart: number;
|
|
currentSubEnd: number;
|
|
currentAudioStreamIndex: number | null;
|
|
send(command: { command: unknown[]; request_id?: number }): boolean;
|
|
}
|
|
|
|
export interface KikuDuplicateCardInfo {
|
|
noteId: number;
|
|
expression: string;
|
|
sentencePreview: string;
|
|
hasAudio: boolean;
|
|
hasImage: boolean;
|
|
isOriginal: boolean;
|
|
}
|
|
|
|
export interface KikuFieldGroupingRequestData {
|
|
original: KikuDuplicateCardInfo;
|
|
duplicate: KikuDuplicateCardInfo;
|
|
}
|
|
|
|
export interface KikuFieldGroupingChoice {
|
|
keepNoteId: number;
|
|
deleteNoteId: number;
|
|
deleteDuplicate: boolean;
|
|
cancelled: boolean;
|
|
}
|
|
|
|
export interface KikuMergePreviewRequest {
|
|
keepNoteId: number;
|
|
deleteNoteId: number;
|
|
deleteDuplicate: boolean;
|
|
}
|
|
|
|
export interface KikuMergePreviewResponse {
|
|
ok: boolean;
|
|
compact?: Record<string, unknown>;
|
|
full?: Record<string, unknown>;
|
|
error?: string;
|
|
}
|
|
|
|
export type RuntimeOptionId =
|
|
| "anki.autoUpdateNewCards"
|
|
| "anki.kikuFieldGrouping";
|
|
|
|
export type RuntimeOptionScope = "ankiConnect";
|
|
|
|
export type RuntimeOptionValueType = "boolean" | "enum";
|
|
|
|
export type RuntimeOptionValue = boolean | string;
|
|
|
|
export interface RuntimeOptionState {
|
|
id: RuntimeOptionId;
|
|
label: string;
|
|
scope: RuntimeOptionScope;
|
|
valueType: RuntimeOptionValueType;
|
|
value: RuntimeOptionValue;
|
|
allowedValues: RuntimeOptionValue[];
|
|
requiresRestart: boolean;
|
|
}
|
|
|
|
export interface RuntimeOptionApplyResult {
|
|
ok: boolean;
|
|
option?: RuntimeOptionState;
|
|
osdMessage?: string;
|
|
requiresRestart?: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export interface AnkiConnectConfig {
|
|
enabled?: boolean;
|
|
url?: string;
|
|
pollingRate?: number;
|
|
fields?: {
|
|
audio?: string;
|
|
image?: string;
|
|
sentence?: string;
|
|
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;
|
|
};
|
|
media?: {
|
|
generateAudio?: boolean;
|
|
generateImage?: boolean;
|
|
imageType?: "static" | "avif";
|
|
imageFormat?: "jpg" | "png" | "webp";
|
|
imageQuality?: number;
|
|
imageMaxWidth?: number;
|
|
imageMaxHeight?: number;
|
|
animatedFps?: number;
|
|
animatedMaxWidth?: number;
|
|
animatedMaxHeight?: number;
|
|
animatedCrf?: number;
|
|
audioPadding?: number;
|
|
fallbackDuration?: number;
|
|
maxMediaDuration?: number;
|
|
};
|
|
behavior?: {
|
|
overwriteAudio?: boolean;
|
|
overwriteImage?: boolean;
|
|
mediaInsertMode?: "append" | "prepend";
|
|
highlightWord?: boolean;
|
|
notificationType?: "osd" | "system" | "both" | "none";
|
|
autoUpdateNewCards?: boolean;
|
|
};
|
|
metadata?: {
|
|
pattern?: string;
|
|
};
|
|
deck?: string;
|
|
isLapis?: {
|
|
enabled?: boolean;
|
|
sentenceCardModel?: string;
|
|
sentenceCardSentenceField?: string;
|
|
sentenceCardAudioField?: string;
|
|
};
|
|
isKiku?: {
|
|
enabled?: boolean;
|
|
fieldGrouping?: "auto" | "manual" | "disabled";
|
|
deleteDuplicateInAuto?: boolean;
|
|
};
|
|
}
|
|
|
|
export interface SubtitleStyleConfig {
|
|
fontFamily?: string;
|
|
fontSize?: number;
|
|
fontColor?: string;
|
|
fontWeight?: string;
|
|
fontStyle?: string;
|
|
backgroundColor?: string;
|
|
secondary?: {
|
|
fontFamily?: string;
|
|
fontSize?: number;
|
|
fontColor?: string;
|
|
fontWeight?: string;
|
|
fontStyle?: string;
|
|
backgroundColor?: string;
|
|
};
|
|
}
|
|
|
|
export interface ShortcutsConfig {
|
|
toggleVisibleOverlayGlobal?: string | null;
|
|
toggleInvisibleOverlayGlobal?: string | null;
|
|
copySubtitle?: string | null;
|
|
copySubtitleMultiple?: string | null;
|
|
updateLastCardFromClipboard?: string | null;
|
|
triggerFieldGrouping?: string | null;
|
|
triggerSubsync?: string | null;
|
|
mineSentence?: string | null;
|
|
mineSentenceMultiple?: string | null;
|
|
multiCopyTimeoutMs?: number;
|
|
toggleSecondarySub?: string | null;
|
|
markAudioCard?: string | null;
|
|
openRuntimeOptions?: string | null;
|
|
openJimaku?: string | null;
|
|
}
|
|
|
|
export type JimakuLanguagePreference = "ja" | "en" | "none";
|
|
|
|
export interface JimakuConfig {
|
|
apiKey?: string;
|
|
apiKeyCommand?: string;
|
|
apiBaseUrl?: string;
|
|
languagePreference?: JimakuLanguagePreference;
|
|
maxEntryResults?: number;
|
|
}
|
|
|
|
export interface InvisibleOverlayConfig {
|
|
startupVisibility?: "platform-default" | "visible" | "hidden";
|
|
}
|
|
|
|
export type YoutubeSubgenMode = "automatic" | "preprocess" | "off";
|
|
|
|
export interface YoutubeSubgenConfig {
|
|
mode?: YoutubeSubgenMode;
|
|
whisperBin?: string;
|
|
whisperModel?: string;
|
|
primarySubLanguages?: string[];
|
|
}
|
|
|
|
export interface Config {
|
|
subtitlePosition?: SubtitlePosition;
|
|
keybindings?: Keybinding[];
|
|
websocket?: WebSocketConfig;
|
|
texthooker?: TexthookerConfig;
|
|
ankiConnect?: AnkiConnectConfig;
|
|
shortcuts?: ShortcutsConfig;
|
|
secondarySub?: SecondarySubConfig;
|
|
subsync?: SubsyncConfig;
|
|
subtitleStyle?: SubtitleStyleConfig;
|
|
auto_start_overlay?: boolean;
|
|
bind_visible_overlay_to_mpv_sub_visibility?: boolean;
|
|
jimaku?: JimakuConfig;
|
|
invisibleOverlay?: InvisibleOverlayConfig;
|
|
youtubeSubgen?: YoutubeSubgenConfig;
|
|
}
|
|
|
|
export type RawConfig = Config;
|
|
|
|
export interface ResolvedConfig {
|
|
subtitlePosition: SubtitlePosition;
|
|
keybindings: Keybinding[];
|
|
websocket: Required<WebSocketConfig>;
|
|
texthooker: Required<TexthookerConfig>;
|
|
ankiConnect: AnkiConnectConfig & {
|
|
enabled: boolean;
|
|
url: string;
|
|
pollingRate: number;
|
|
fields: {
|
|
audio: string;
|
|
image: string;
|
|
sentence: string;
|
|
miscInfo: string;
|
|
translation: string;
|
|
};
|
|
ai: {
|
|
enabled: boolean;
|
|
alwaysUseAiTranslation: boolean;
|
|
apiKey: string;
|
|
model: string;
|
|
baseUrl: string;
|
|
targetLanguage: string;
|
|
systemPrompt: string;
|
|
};
|
|
media: {
|
|
generateAudio: boolean;
|
|
generateImage: boolean;
|
|
imageType: "static" | "avif";
|
|
imageFormat: "jpg" | "png" | "webp";
|
|
imageQuality: number;
|
|
imageMaxWidth?: number;
|
|
imageMaxHeight?: number;
|
|
animatedFps: number;
|
|
animatedMaxWidth: number;
|
|
animatedMaxHeight?: number;
|
|
animatedCrf: number;
|
|
audioPadding: number;
|
|
fallbackDuration: number;
|
|
maxMediaDuration: number;
|
|
};
|
|
behavior: {
|
|
overwriteAudio: boolean;
|
|
overwriteImage: boolean;
|
|
mediaInsertMode: "append" | "prepend";
|
|
highlightWord: boolean;
|
|
notificationType: "osd" | "system" | "both" | "none";
|
|
autoUpdateNewCards: boolean;
|
|
};
|
|
metadata: {
|
|
pattern: string;
|
|
};
|
|
isLapis: {
|
|
enabled: boolean;
|
|
sentenceCardModel: string;
|
|
sentenceCardSentenceField: string;
|
|
sentenceCardAudioField: string;
|
|
};
|
|
isKiku: {
|
|
enabled: boolean;
|
|
fieldGrouping: "auto" | "manual" | "disabled";
|
|
deleteDuplicateInAuto: boolean;
|
|
};
|
|
};
|
|
shortcuts: Required<ShortcutsConfig>;
|
|
secondarySub: Required<SecondarySubConfig>;
|
|
subsync: Required<SubsyncConfig>;
|
|
subtitleStyle: Required<Omit<SubtitleStyleConfig, "secondary">> & {
|
|
secondary: Required<NonNullable<SubtitleStyleConfig["secondary"]>>;
|
|
};
|
|
auto_start_overlay: boolean;
|
|
bind_visible_overlay_to_mpv_sub_visibility: boolean;
|
|
jimaku: JimakuConfig & {
|
|
apiBaseUrl: string;
|
|
languagePreference: JimakuLanguagePreference;
|
|
maxEntryResults: number;
|
|
};
|
|
invisibleOverlay: Required<InvisibleOverlayConfig>;
|
|
youtubeSubgen: YoutubeSubgenConfig & {
|
|
mode: YoutubeSubgenMode;
|
|
whisperBin: string;
|
|
whisperModel: string;
|
|
primarySubLanguages: string[];
|
|
};
|
|
}
|
|
|
|
export interface ConfigValidationWarning {
|
|
path: string;
|
|
value: unknown;
|
|
fallback: unknown;
|
|
message: string;
|
|
}
|
|
|
|
export interface SubsyncSourceTrack {
|
|
id: number;
|
|
label: string;
|
|
}
|
|
|
|
export interface SubsyncManualPayload {
|
|
sourceTracks: SubsyncSourceTrack[];
|
|
}
|
|
|
|
export interface SubsyncManualRunRequest {
|
|
engine: "alass" | "ffsubsync";
|
|
sourceTrackId?: number | null;
|
|
}
|
|
|
|
export interface SubsyncResult {
|
|
ok: boolean;
|
|
message: string;
|
|
}
|
|
|
|
export interface SubtitleData {
|
|
text: string;
|
|
tokens: MergedToken[] | null;
|
|
}
|
|
|
|
export interface MpvSubtitleRenderMetrics {
|
|
subPos: number;
|
|
subFontSize: number;
|
|
subScale: number;
|
|
subMarginY: number;
|
|
subMarginX: number;
|
|
subFont: string;
|
|
subSpacing: number;
|
|
subBold: boolean;
|
|
subItalic: boolean;
|
|
subBorderSize: number;
|
|
subShadowOffset: number;
|
|
subAssOverride: string;
|
|
subScaleByWindow: boolean;
|
|
subUseMargins: boolean;
|
|
osdHeight: number;
|
|
osdDimensions: {
|
|
w: number;
|
|
h: number;
|
|
ml: number;
|
|
mr: number;
|
|
mt: number;
|
|
mb: number;
|
|
} | null;
|
|
}
|
|
|
|
export interface MecabStatus {
|
|
available: boolean;
|
|
enabled: boolean;
|
|
path: string | null;
|
|
}
|
|
|
|
export type JimakuConfidence = "high" | "medium" | "low";
|
|
|
|
export interface JimakuMediaInfo {
|
|
title: string;
|
|
season: number | null;
|
|
episode: number | null;
|
|
confidence: JimakuConfidence;
|
|
filename: string;
|
|
rawTitle: string;
|
|
}
|
|
|
|
export interface JimakuSearchQuery {
|
|
query: string;
|
|
}
|
|
|
|
export interface JimakuEntryFlags {
|
|
anime?: boolean;
|
|
movie?: boolean;
|
|
adult?: boolean;
|
|
external?: boolean;
|
|
unverified?: boolean;
|
|
}
|
|
|
|
export interface JimakuEntry {
|
|
id: number;
|
|
name: string;
|
|
english_name?: string | null;
|
|
japanese_name?: string | null;
|
|
flags?: JimakuEntryFlags;
|
|
last_modified?: string;
|
|
}
|
|
|
|
export interface JimakuFilesQuery {
|
|
entryId: number;
|
|
episode?: number | null;
|
|
}
|
|
|
|
export interface JimakuFileEntry {
|
|
name: string;
|
|
url: string;
|
|
size: number;
|
|
last_modified: string;
|
|
}
|
|
|
|
export interface JimakuDownloadQuery {
|
|
entryId: number;
|
|
url: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface JimakuApiError {
|
|
error: string;
|
|
code?: number;
|
|
retryAfter?: number;
|
|
}
|
|
|
|
export type JimakuApiResponse<T> =
|
|
| { ok: true; data: T }
|
|
| { ok: false; error: JimakuApiError };
|
|
|
|
export type JimakuDownloadResult =
|
|
| { ok: true; path: string }
|
|
| { ok: false; error: JimakuApiError };
|
|
|
|
export interface ElectronAPI {
|
|
getOverlayLayer: () => "visible" | "invisible" | null;
|
|
onSubtitle: (callback: (data: SubtitleData) => void) => void;
|
|
onVisibility: (callback: (visible: boolean) => void) => void;
|
|
onSubtitlePosition: (
|
|
callback: (position: SubtitlePosition | null) => void,
|
|
) => void;
|
|
getOverlayVisibility: () => Promise<boolean>;
|
|
getCurrentSubtitle: () => Promise<SubtitleData>;
|
|
getCurrentSubtitleAss: () => Promise<string>;
|
|
getMpvSubtitleRenderMetrics: () => Promise<MpvSubtitleRenderMetrics>;
|
|
onMpvSubtitleRenderMetrics: (
|
|
callback: (metrics: MpvSubtitleRenderMetrics) => void,
|
|
) => void;
|
|
onSubtitleAss: (callback: (assText: string) => void) => void;
|
|
onOverlayDebugVisualization: (callback: (enabled: boolean) => void) => void;
|
|
setIgnoreMouseEvents: (
|
|
ignore: boolean,
|
|
options?: { forward?: boolean },
|
|
) => void;
|
|
openYomitanSettings: () => void;
|
|
getSubtitlePosition: () => Promise<SubtitlePosition | null>;
|
|
saveSubtitlePosition: (position: SubtitlePosition) => void;
|
|
getMecabStatus: () => Promise<MecabStatus>;
|
|
setMecabEnabled: (enabled: boolean) => void;
|
|
sendMpvCommand: (command: (string | number)[]) => void;
|
|
getKeybindings: () => Promise<Keybinding[]>;
|
|
getJimakuMediaInfo: () => Promise<JimakuMediaInfo>;
|
|
jimakuSearchEntries: (
|
|
query: JimakuSearchQuery,
|
|
) => Promise<JimakuApiResponse<JimakuEntry[]>>;
|
|
jimakuListFiles: (
|
|
query: JimakuFilesQuery,
|
|
) => Promise<JimakuApiResponse<JimakuFileEntry[]>>;
|
|
jimakuDownloadFile: (
|
|
query: JimakuDownloadQuery,
|
|
) => Promise<JimakuDownloadResult>;
|
|
quitApp: () => void;
|
|
toggleDevTools: () => void;
|
|
toggleOverlay: () => void;
|
|
getAnkiConnectStatus: () => Promise<boolean>;
|
|
setAnkiConnectEnabled: (enabled: boolean) => void;
|
|
clearAnkiConnectHistory: () => void;
|
|
onSecondarySub: (callback: (text: string) => void) => void;
|
|
onSecondarySubMode: (callback: (mode: SecondarySubMode) => void) => void;
|
|
getSecondarySubMode: () => Promise<SecondarySubMode>;
|
|
getCurrentSecondarySub: () => Promise<string>;
|
|
getSubtitleStyle: () => Promise<SubtitleStyleConfig | null>;
|
|
onSubsyncManualOpen: (
|
|
callback: (payload: SubsyncManualPayload) => void,
|
|
) => void;
|
|
runSubsyncManual: (
|
|
request: SubsyncManualRunRequest,
|
|
) => Promise<SubsyncResult>;
|
|
onKikuFieldGroupingRequest: (
|
|
callback: (data: KikuFieldGroupingRequestData) => void,
|
|
) => void;
|
|
kikuBuildMergePreview: (
|
|
request: KikuMergePreviewRequest,
|
|
) => Promise<KikuMergePreviewResponse>;
|
|
kikuFieldGroupingRespond: (choice: KikuFieldGroupingChoice) => void;
|
|
getRuntimeOptions: () => Promise<RuntimeOptionState[]>;
|
|
setRuntimeOptionValue: (
|
|
id: RuntimeOptionId,
|
|
value: RuntimeOptionValue,
|
|
) => Promise<RuntimeOptionApplyResult>;
|
|
cycleRuntimeOption: (
|
|
id: RuntimeOptionId,
|
|
direction: 1 | -1,
|
|
) => Promise<RuntimeOptionApplyResult>;
|
|
onRuntimeOptionsChanged: (
|
|
callback: (options: RuntimeOptionState[]) => void,
|
|
) => void;
|
|
onOpenRuntimeOptions: (callback: () => void) => void;
|
|
onOpenJimaku: (callback: () => void) => void;
|
|
notifyOverlayModalClosed: (modal: "runtime-options" | "subsync") => void;
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
electronAPI: ElectronAPI;
|
|
}
|
|
}
|