This commit is contained in:
2026-02-17 22:50:57 -08:00
parent ffeef9c136
commit f20d019c11
315 changed files with 9876 additions and 12537 deletions

View File

@@ -1,94 +1,91 @@
const repositoryName = process.env.GITHUB_REPOSITORY?.split("/")[1];
const base =
process.env.GITHUB_ACTIONS && repositoryName ? `/${repositoryName}/` : "/";
const repositoryName = process.env.GITHUB_REPOSITORY?.split('/')[1];
const base = process.env.GITHUB_ACTIONS && repositoryName ? `/${repositoryName}/` : '/';
export default {
title: "SubMiner Docs",
title: 'SubMiner Docs',
description:
"SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.",
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.',
base,
head: [
["link", { rel: "icon", href: "/favicon.ico", sizes: "any" }],
['link', { rel: 'icon', href: '/favicon.ico', sizes: 'any' }],
[
"link",
'link',
{
rel: "icon",
type: "image/png",
href: "/favicon-32x32.png",
sizes: "32x32",
rel: 'icon',
type: 'image/png',
href: '/favicon-32x32.png',
sizes: '32x32',
},
],
[
"link",
'link',
{
rel: "icon",
type: "image/png",
href: "/favicon-16x16.png",
sizes: "16x16",
rel: 'icon',
type: 'image/png',
href: '/favicon-16x16.png',
sizes: '16x16',
},
],
[
"link",
'link',
{
rel: "apple-touch-icon",
href: "/apple-touch-icon.png",
sizes: "180x180",
rel: 'apple-touch-icon',
href: '/apple-touch-icon.png',
sizes: '180x180',
},
],
],
appearance: "dark",
appearance: 'dark',
cleanUrls: true,
lastUpdated: true,
markdown: {
theme: {
light: "catppuccin-latte",
dark: "catppuccin-macchiato",
light: 'catppuccin-latte',
dark: 'catppuccin-macchiato',
},
},
themeConfig: {
logo: {
light: "/assets/SubMiner.png",
dark: "/assets/SubMiner.png",
light: '/assets/SubMiner.png',
dark: '/assets/SubMiner.png',
},
siteTitle: "SubMiner Docs",
siteTitle: 'SubMiner Docs',
nav: [
{ text: "Home", link: "/" },
{ text: "Get Started", link: "/installation" },
{ text: "Mining", link: "/mining-workflow" },
{ text: "Configuration", link: "/configuration" },
{ text: "Troubleshooting", link: "/troubleshooting" },
{ text: 'Home', link: '/' },
{ text: 'Get Started', link: '/installation' },
{ text: 'Mining', link: '/mining-workflow' },
{ text: 'Configuration', link: '/configuration' },
{ text: 'Troubleshooting', link: '/troubleshooting' },
],
sidebar: [
{
text: "Getting Started",
text: 'Getting Started',
items: [
{ text: "Overview", link: "/" },
{ text: "Installation", link: "/installation" },
{ text: "Usage", link: "/usage" },
{ text: "Mining Workflow", link: "/mining-workflow" },
{ text: 'Overview', link: '/' },
{ text: 'Installation', link: '/installation' },
{ text: 'Usage', link: '/usage' },
{ text: 'Mining Workflow', link: '/mining-workflow' },
],
},
{
text: "Reference",
text: 'Reference',
items: [
{ text: "Configuration", link: "/configuration" },
{ text: "Immersion Tracking", link: "/immersion-tracking" },
{ text: "Anki Integration", link: "/anki-integration" },
{ text: "Jellyfin Integration", link: "/jellyfin-integration" },
{ text: "MPV Plugin", link: "/mpv-plugin" },
{ text: "Troubleshooting", link: "/troubleshooting" },
{ text: 'Configuration', link: '/configuration' },
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
{ text: 'Anki Integration', link: '/anki-integration' },
{ text: 'Jellyfin Integration', link: '/jellyfin-integration' },
{ text: 'MPV Plugin', link: '/mpv-plugin' },
{ text: 'Troubleshooting', link: '/troubleshooting' },
],
},
{
text: "Development",
text: 'Development',
items: [
{ text: "Building & Testing", link: "/development" },
{ text: "Architecture", link: "/architecture" },
{ text: 'Building & Testing', link: '/development' },
{ text: 'Architecture', link: '/architecture' },
],
},
],
socialLinks: [
{ icon: "github", link: "https://github.com/ksyasuda/SubMiner" },
],
socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }],
},
};

View File

@@ -143,9 +143,7 @@ async function renderMermaidBlocks() {
if (typeof document === 'undefined') {
return;
}
const blocks = Array.from(
document.querySelectorAll<HTMLElement>('div.language-mermaid'),
);
const blocks = Array.from(document.querySelectorAll<HTMLElement>('div.language-mermaid'));
if (blocks.length === 0) {
return;
}

View File

@@ -194,19 +194,19 @@ When you mine the same word multiple times, SubMiner can merge the cards instead
### What Gets Merged
| Field | Merge behavior |
| --- | --- |
| Field | Merge behavior |
| -------- | -------------------------------------------------------------- |
| Sentence | Both sentences preserved, labeled `[Original]` / `[Duplicate]` |
| Audio | Both `[sound:...]` entries kept |
| Image | Both images kept |
| Audio | Both `[sound:...]` entries kept |
| Image | Both images kept |
### Keyboard Shortcuts in the Modal
| Key | Action |
| --- | --- |
| `1` / `2` | Select card 1 or card 2 to keep |
| `Enter` | Confirm selection |
| `Esc` | Cancel (keep both cards unchanged) |
| Key | Action |
| --------- | ---------------------------------- |
| `1` / `2` | Select card 1 or card 2 to keep |
| `Enter` | Confirm selection |
| `Esc` | Cancel (keep both cards unchanged) |
## Full Config Example
@@ -221,7 +221,7 @@ When you mine the same word multiple times, SubMiner can merge the cards instead
"image": "Picture",
"sentence": "Sentence",
"miscInfo": "MiscInfo",
"translation": "SelectionText"
"translation": "SelectionText",
},
"media": {
"generateAudio": true,
@@ -230,33 +230,33 @@ When you mine the same word multiple times, SubMiner can merge the cards instead
"imageFormat": "jpg",
"imageQuality": 92,
"audioPadding": 0.5,
"maxMediaDuration": 30
"maxMediaDuration": 30,
},
"behavior": {
"overwriteAudio": true,
"overwriteImage": true,
"mediaInsertMode": "append",
"autoUpdateNewCards": true,
"notificationType": "osd"
"notificationType": "osd",
},
"ai": {
"enabled": false,
"apiKey": "",
"model": "openai/gpt-4o-mini",
"baseUrl": "https://openrouter.ai/api",
"targetLanguage": "English"
"targetLanguage": "English",
},
"isKiku": {
"enabled": false,
"fieldGrouping": "disabled",
"deleteDuplicateInAuto": true
"deleteDuplicateInAuto": true,
},
"isLapis": {
"enabled": false,
"sentenceCardModel": "Japanese sentences",
"sentenceCardSentenceField": "Sentence",
"sentenceCardAudioField": "SentenceAudio"
}
}
"sentenceCardAudioField": "SentenceAudio",
},
},
}
```

View File

@@ -155,6 +155,7 @@ Most runtime code follows a dependency-injection pattern:
4. Call the service from lifecycle/command wiring points.
The composition root (`src/main.ts`) delegates to focused modules in `src/main/`:
- `startup.ts` — argv/env processing and bootstrap flow
- `app-lifecycle.ts` — Electron lifecycle event registration
- `startup-lifecycle.ts` — app-ready initialization sequence

View File

@@ -135,55 +135,55 @@ This example is intentionally compact. The option table below documents availabl
**Requirements:** [AnkiConnect](https://github.com/FooSoft/anki-connect) plugin must be installed and running in Anki. ffmpeg must be installed for media generation.
| Option | Values | Description |
| -------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `false`) |
| `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) |
| `pollingRate` | number (ms) | How often to check for new cards (default: `3000`) |
| `deck` | string | Anki deck to monitor for new cards |
| `ankiConnect.nPlusOne.decks` | array of strings | Decks used for N+1 known-word cache lookups. When omitted/empty, falls back to `ankiConnect.deck`. |
| `fields.audio` | string | Card field for audio files (default: `ExpressionAudio`) |
| `fields.image` | string | Card field for images (default: `Picture`) |
| `fields.sentence` | string | Card field for sentences (default: `Sentence`) |
| `fields.miscInfo` | string | Card field for metadata (default: `"MiscInfo"`, set to `null` to disable) |
| `fields.translation` | string | Card field for sentence-card translation/back text (default: `SelectionText`) |
| `ai.enabled` | `true`, `false` | Use AI translation for sentence cards. Also auto-attempted when secondary subtitle is missing. |
| `ai.alwaysUseAiTranslation` | `true`, `false` | When `true`, always use AI translation even if secondary subtitles exist. When `false`, AI is used only when no secondary subtitle exists. |
| `ai.apiKey` | string | API key for your OpenAI-compatible endpoint (required for translation). |
| `ai.model` | string | Model id for your OpenAI-compatible endpoint (default: `openai/gpt-4o-mini`). |
| `ai.baseUrl` | string (URL) | OpenAI-compatible API base URL; accepts with or without `/v1`. |
| `ai.targetLanguage` | string | Target language name used in translation prompt (default: `English`). |
| `ai.systemPrompt` | string | System prompt used for translation (default returns translation text only). |
| `media.generateAudio` | `true`, `false` | Generate audio clips from video (default: `true`) |
| `media.generateImage` | `true`, `false` | Generate image/animation screenshots (default: `true`) |
| `media.imageType` | `"static"`, `"avif"` | Image type: static screenshot or animated AVIF (default: `"static"`) |
| `media.imageFormat` | `"jpg"`, `"png"`, `"webp"` | Image format (default: `"jpg"`) |
| `media.imageQuality` | number (1-100) | Image quality for JPG/WebP; PNG ignores this (default: `92`) |
| `media.imageMaxWidth` | number (px) | Optional max width for static screenshots. Unset keeps source width. |
| `media.imageMaxHeight` | number (px) | Optional max height for static screenshots. Unset keeps source height. |
| `media.animatedFps` | number (1-60) | FPS for animated AVIF (default: `10`) |
| `media.animatedMaxWidth` | number (px) | Max width for animated AVIF (default: `640`) |
| `media.animatedMaxHeight` | number (px) | Optional max height for animated AVIF. Unset keeps source aspect-constrained height. |
| `media.animatedCrf` | number (0-63) | CRF quality for AVIF; lower = higher quality (default: `35`) |
| `media.audioPadding` | number (seconds) | Padding around audio clip timing (default: `0.5`) |
| `media.fallbackDuration` | number (seconds) | Default duration if timing unavailable (default: `3.0`) |
| `media.maxMediaDuration` | number (seconds) | Max duration for generated media from multi-line copy (default: `30`, `0` to disable) |
| `behavior.overwriteAudio` | `true`, `false` | Replace existing audio on updates; when `false`, new audio is appended/prepended per `behavior.mediaInsertMode` (default: `true`) |
| `behavior.overwriteImage` | `true`, `false` | Replace existing images on updates; when `false`, new images are appended/prepended per `behavior.mediaInsertMode` (default: `true`) |
| `behavior.mediaInsertMode` | `"append"`, `"prepend"` | Where to insert new media when overwrite is off (default: `"append"`) |
| `behavior.highlightWord` | `true`, `false` | Highlight the word in sentence context (default: `true`) |
| `ankiConnect.nPlusOne.highlightEnabled` | `true`, `false` | Enable fast local highlighting for words already known in Anki (default: `false`) |
| `ankiConnect.nPlusOne.nPlusOne` | hex color string | Text color for the single target token to study when exactly one unknown candidate exists in a sentence (default: `"#c6a0f6"`). |
| `ankiConnect.nPlusOne.knownWord` | hex color string | Legacy known-word color kept for backward compatibility (default: `"#a6da95"`). |
| `ankiConnect.nPlusOne.matchMode` | `"headword"`, `"surface"` | Matching strategy for known-word highlighting (default: `"headword"`). `headword` uses token headwords; `surface` uses visible subtitle text. |
| `ankiConnect.nPlusOne.minSentenceWords` | number | Minimum number of words required in a sentence before single unknown-word N+1 highlighting can trigger (default: `3`). |
| `ankiConnect.nPlusOne.refreshMinutes` | number | Minutes between known-word cache refreshes (default: `1440`) |
| `ankiConnect.nPlusOne.decks` | array of strings | Decks used by known-word cache refresh. Leave empty for compatibility with legacy `deck` scope. |
| `behavior.notificationType` | `"osd"`, `"system"`, `"both"`, `"none"` | Notification type on card update (default: `"osd"`) |
| `behavior.autoUpdateNewCards` | `true`, `false` | Automatically update cards on creation (default: `true`) |
| `metadata.pattern` | string | Format pattern for metadata: `%f`=filename, `%F`=filename+ext, `%t`=time |
| `isLapis` | object | Lapis/shared sentence-card config: `{ enabled, sentenceCardModel, sentenceCardSentenceField, sentenceCardAudioField }` |
| `isKiku` | object | Kiku-only config: `{ enabled, fieldGrouping, deleteDuplicateInAuto }` (shared sentence/audio/model settings are inherited from `isLapis`) |
| Option | Values | Description |
| --------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `false`) |
| `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) |
| `pollingRate` | number (ms) | How often to check for new cards (default: `3000`) |
| `deck` | string | Anki deck to monitor for new cards |
| `ankiConnect.nPlusOne.decks` | array of strings | Decks used for N+1 known-word cache lookups. When omitted/empty, falls back to `ankiConnect.deck`. |
| `fields.audio` | string | Card field for audio files (default: `ExpressionAudio`) |
| `fields.image` | string | Card field for images (default: `Picture`) |
| `fields.sentence` | string | Card field for sentences (default: `Sentence`) |
| `fields.miscInfo` | string | Card field for metadata (default: `"MiscInfo"`, set to `null` to disable) |
| `fields.translation` | string | Card field for sentence-card translation/back text (default: `SelectionText`) |
| `ai.enabled` | `true`, `false` | Use AI translation for sentence cards. Also auto-attempted when secondary subtitle is missing. |
| `ai.alwaysUseAiTranslation` | `true`, `false` | When `true`, always use AI translation even if secondary subtitles exist. When `false`, AI is used only when no secondary subtitle exists. |
| `ai.apiKey` | string | API key for your OpenAI-compatible endpoint (required for translation). |
| `ai.model` | string | Model id for your OpenAI-compatible endpoint (default: `openai/gpt-4o-mini`). |
| `ai.baseUrl` | string (URL) | OpenAI-compatible API base URL; accepts with or without `/v1`. |
| `ai.targetLanguage` | string | Target language name used in translation prompt (default: `English`). |
| `ai.systemPrompt` | string | System prompt used for translation (default returns translation text only). |
| `media.generateAudio` | `true`, `false` | Generate audio clips from video (default: `true`) |
| `media.generateImage` | `true`, `false` | Generate image/animation screenshots (default: `true`) |
| `media.imageType` | `"static"`, `"avif"` | Image type: static screenshot or animated AVIF (default: `"static"`) |
| `media.imageFormat` | `"jpg"`, `"png"`, `"webp"` | Image format (default: `"jpg"`) |
| `media.imageQuality` | number (1-100) | Image quality for JPG/WebP; PNG ignores this (default: `92`) |
| `media.imageMaxWidth` | number (px) | Optional max width for static screenshots. Unset keeps source width. |
| `media.imageMaxHeight` | number (px) | Optional max height for static screenshots. Unset keeps source height. |
| `media.animatedFps` | number (1-60) | FPS for animated AVIF (default: `10`) |
| `media.animatedMaxWidth` | number (px) | Max width for animated AVIF (default: `640`) |
| `media.animatedMaxHeight` | number (px) | Optional max height for animated AVIF. Unset keeps source aspect-constrained height. |
| `media.animatedCrf` | number (0-63) | CRF quality for AVIF; lower = higher quality (default: `35`) |
| `media.audioPadding` | number (seconds) | Padding around audio clip timing (default: `0.5`) |
| `media.fallbackDuration` | number (seconds) | Default duration if timing unavailable (default: `3.0`) |
| `media.maxMediaDuration` | number (seconds) | Max duration for generated media from multi-line copy (default: `30`, `0` to disable) |
| `behavior.overwriteAudio` | `true`, `false` | Replace existing audio on updates; when `false`, new audio is appended/prepended per `behavior.mediaInsertMode` (default: `true`) |
| `behavior.overwriteImage` | `true`, `false` | Replace existing images on updates; when `false`, new images are appended/prepended per `behavior.mediaInsertMode` (default: `true`) |
| `behavior.mediaInsertMode` | `"append"`, `"prepend"` | Where to insert new media when overwrite is off (default: `"append"`) |
| `behavior.highlightWord` | `true`, `false` | Highlight the word in sentence context (default: `true`) |
| `ankiConnect.nPlusOne.highlightEnabled` | `true`, `false` | Enable fast local highlighting for words already known in Anki (default: `false`) |
| `ankiConnect.nPlusOne.nPlusOne` | hex color string | Text color for the single target token to study when exactly one unknown candidate exists in a sentence (default: `"#c6a0f6"`). |
| `ankiConnect.nPlusOne.knownWord` | hex color string | Legacy known-word color kept for backward compatibility (default: `"#a6da95"`). |
| `ankiConnect.nPlusOne.matchMode` | `"headword"`, `"surface"` | Matching strategy for known-word highlighting (default: `"headword"`). `headword` uses token headwords; `surface` uses visible subtitle text. |
| `ankiConnect.nPlusOne.minSentenceWords` | number | Minimum number of words required in a sentence before single unknown-word N+1 highlighting can trigger (default: `3`). |
| `ankiConnect.nPlusOne.refreshMinutes` | number | Minutes between known-word cache refreshes (default: `1440`) |
| `ankiConnect.nPlusOne.decks` | array of strings | Decks used by known-word cache refresh. Leave empty for compatibility with legacy `deck` scope. |
| `behavior.notificationType` | `"osd"`, `"system"`, `"both"`, `"none"` | Notification type on card update (default: `"osd"`) |
| `behavior.autoUpdateNewCards` | `true`, `false` | Automatically update cards on creation (default: `true`) |
| `metadata.pattern` | string | Format pattern for metadata: `%f`=filename, `%F`=filename+ext, `%t`=time |
| `isLapis` | object | Lapis/shared sentence-card config: `{ enabled, sentenceCardModel, sentenceCardSentenceField, sentenceCardAudioField }` |
| `isKiku` | object | Kiku-only config: `{ enabled, fieldGrouping, deleteDuplicateInAuto }` (shared sentence/audio/model settings are inherited from `isLapis`) |
**Kiku / Lapis Note Type Support:**
@@ -252,17 +252,17 @@ To refresh roughly once per day, set:
When `behavior.autoUpdateNewCards` is set to `false`, new cards are detected but not automatically updated. Use these keyboard shortcuts for manual control:
| Shortcut | Action |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| `Ctrl+C` | Copy the current subtitle line to clipboard (preserves line breaks) |
| `Ctrl+Shift+C` | Enter multi-copy mode. Press `1-9` to copy that many recent lines, or `Esc` to cancel. Timeout: 3 seconds |
| `Ctrl+V` | Update the last added Anki card using subtitles from clipboard |
| `Ctrl+G` | Trigger Kiku duplicate field grouping for the last added card (only when `behavior.autoUpdateNewCards` is `false`) |
| `Ctrl+S` | Create a sentence card from the current subtitle line |
| `Ctrl+Shift+S` | Enter multi-mine mode. Press `1-9` to create a sentence card from that many recent lines, or `Esc` to cancel |
| `Ctrl+Shift+V` | Cycle secondary subtitle display mode (hidden → visible → hover) |
| `Ctrl+Shift+A` | Mark the last added Anki card as an audio card (sets IsAudioCard, SentenceAudio, Sentence, Picture) |
| `Ctrl+Shift+O` | Open runtime options palette (session-only live toggles) |
| Shortcut | Action |
| -------------- | ------------------------------------------------------------------------------------------------------------------ |
| `Ctrl+C` | Copy the current subtitle line to clipboard (preserves line breaks) |
| `Ctrl+Shift+C` | Enter multi-copy mode. Press `1-9` to copy that many recent lines, or `Esc` to cancel. Timeout: 3 seconds |
| `Ctrl+V` | Update the last added Anki card using subtitles from clipboard |
| `Ctrl+G` | Trigger Kiku duplicate field grouping for the last added card (only when `behavior.autoUpdateNewCards` is `false`) |
| `Ctrl+S` | Create a sentence card from the current subtitle line |
| `Ctrl+Shift+S` | Enter multi-mine mode. Press `1-9` to create a sentence card from that many recent lines, or `Esc` to cancel |
| `Ctrl+Shift+V` | Cycle secondary subtitle display mode (hidden → visible → hover) |
| `Ctrl+Shift+A` | Mark the last added Anki card as an audio card (sets IsAudioCard, SentenceAudio, Sentence, Picture) |
| `Ctrl+Shift+O` | Open runtime options palette (session-only live toggles) |
**Multi-line copy workflow:**
@@ -321,8 +321,8 @@ Control whether toggling the visible overlay also toggles MPV subtitle visibilit
}
```
| Option | Values | Description |
| --------------------------------------------- | --------------- | ----------- |
| Option | Values | Description |
| -------------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bind_visible_overlay_to_mpv_sub_visibility` | `true`, `false` | When `true` (default), visible overlay hides MPV primary/secondary subtitles and restores them when hidden. When `false`, visible overlay toggles do not change MPV subtitle visibility. |
### Auto Subtitle Sync
@@ -340,12 +340,12 @@ Sync the active subtitle track using `alass` (preferred) or `ffsubsync`:
}
```
| Option | Values | Description |
| ---------------- | -------------------- | ----------- |
| Option | Values | Description |
| ---------------- | -------------------- | ----------------------------------------------------------------------------------------------------------- |
| `defaultMode` | `"auto"`, `"manual"` | `auto`: try `alass` against secondary subtitle, then fallback to `ffsubsync`; `manual`: open overlay picker |
| `alass_path` | string path | Path to `alass` executable. Empty or `null` falls back to `/usr/bin/alass`. |
| `ffsubsync_path` | string path | Path to `ffsubsync` executable. Empty or `null` falls back to `/usr/bin/ffsubsync`. |
| `ffmpeg_path` | string path | Path to `ffmpeg` (used for internal subtitle extraction). Empty or `null` falls back to `/usr/bin/ffmpeg`. |
| `alass_path` | string path | Path to `alass` executable. Empty or `null` falls back to `/usr/bin/alass`. |
| `ffsubsync_path` | string path | Path to `ffsubsync` executable. Empty or `null` falls back to `/usr/bin/ffsubsync`. |
| `ffmpeg_path` | string path | Path to `ffmpeg` (used for internal subtitle extraction). Empty or `null` falls back to `/usr/bin/ffmpeg`. |
Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`.
Customize it there, or set it to `null` to disable.
@@ -355,6 +355,7 @@ Customize it there, or set it to `null` to disable.
SubMiner includes a second subtitle mining layer that can be visually invisible while still interactive for Yomitan lookups.
- `invisibleOverlay.startupVisibility` values:
1. `"platform-default"`: hidden on Wayland, visible on Windows/macOS/other sessions.
2. `"visible"`: always shown on startup.
3. `"hidden"`: always hidden on startup.
@@ -399,10 +400,10 @@ AniList integration is opt-in and disabled by default. Enable it and provide an
}
```
| Option | Values | Description |
| ------ | ------ | ----------- |
| `enabled` | `true`, `false` | Enable AniList post-watch progress updates (default: `false`) |
| `accessToken` | string | AniList access token used for authenticated GraphQL updates (default: empty string) |
| Option | Values | Description |
| ------------- | --------------- | ----------------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable AniList post-watch progress updates (default: `false`) |
| `accessToken` | string | AniList access token used for authenticated GraphQL updates (default: empty string) |
When `enabled` is `true` and `accessToken` is empty, SubMiner opens an AniList setup helper window. Keep `enabled` as `false` to disable all AniList setup/update behavior.
@@ -467,26 +468,26 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner
}
```
| Option | Values | Description |
| ------ | ------ | ----------- |
| `enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) |
| `serverUrl` | string (URL) | Jellyfin server base URL |
| `username` | string | Default username used by `--jellyfin-login` |
| `accessToken` | string | Stored Jellyfin access token (treat as secret) |
| `userId` | string | Jellyfin user id bound to token/session |
| `deviceId` | string | Client device id sent in auth headers (default: `subminer`) |
| `clientName` | string | Client name sent in auth headers (default: `SubMiner`) |
| `clientVersion` | string | Client version sent in auth headers (default: `0.1.0`) |
| `defaultLibraryId` | string | Default library id for `--jellyfin-items` when CLI value is omitted |
| `remoteControlEnabled` | `true`, `false` | Enable Jellyfin cast/remote-control session support |
| `remoteControlAutoConnect` | `true`, `false` | Auto-connect Jellyfin remote session on app startup |
| `autoAnnounce` | `true`, `false` | Auto-run cast-target visibility announce check on connect (default: `false`) |
| `remoteControlDeviceName` | string | Device name shown in Jellyfin cast/device lists |
| `pullPictures` | `true`, `false` | Enable poster/icon fetching for launcher Jellyfin pickers |
| `iconCacheDir` | string | Cache directory for launcher-fetched Jellyfin poster icons |
| `directPlayPreferred` | `true`, `false` | Prefer direct stream URLs before transcoding |
| `directPlayContainers` | string[] | Container allowlist for direct play decisions |
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
| Option | Values | Description |
| -------------------------- | --------------- | ---------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) |
| `serverUrl` | string (URL) | Jellyfin server base URL |
| `username` | string | Default username used by `--jellyfin-login` |
| `accessToken` | string | Stored Jellyfin access token (treat as secret) |
| `userId` | string | Jellyfin user id bound to token/session |
| `deviceId` | string | Client device id sent in auth headers (default: `subminer`) |
| `clientName` | string | Client name sent in auth headers (default: `SubMiner`) |
| `clientVersion` | string | Client version sent in auth headers (default: `0.1.0`) |
| `defaultLibraryId` | string | Default library id for `--jellyfin-items` when CLI value is omitted |
| `remoteControlEnabled` | `true`, `false` | Enable Jellyfin cast/remote-control session support |
| `remoteControlAutoConnect` | `true`, `false` | Auto-connect Jellyfin remote session on app startup |
| `autoAnnounce` | `true`, `false` | Auto-run cast-target visibility announce check on connect (default: `false`) |
| `remoteControlDeviceName` | string | Device name shown in Jellyfin cast/device lists |
| `pullPictures` | `true`, `false` | Enable poster/icon fetching for launcher Jellyfin pickers |
| `iconCacheDir` | string | Cache directory for launcher-fetched Jellyfin poster icons |
| `directPlayPreferred` | `true`, `false` | Prefer direct stream URLs before transcoding |
| `directPlayContainers` | string[] | Container allowlist for direct play decisions |
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
Jellyfin direct app CLI commands (`SubMiner.AppImage ...`):
@@ -630,22 +631,22 @@ See `config.example.jsonc` for detailed configuration options.
}
```
| Option | Values | Description |
| ----------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `toggleVisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling visible subtitle overlay (default: `"Alt+Shift+O"`) |
| `toggleInvisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling invisible interactive overlay (default: `"Alt+Shift+I"`) |
| `copySubtitle` | string \| `null` | Accelerator for copying current subtitle (default: `"CommandOrControl+C"`) |
| `copySubtitleMultiple` | string \| `null` | Accelerator for multi-copy mode (default: `"CommandOrControl+Shift+C"`) |
| `updateLastCardFromClipboard` | string \| `null` | Accelerator for updating card from clipboard (default: `"CommandOrControl+V"`) |
| `triggerFieldGrouping` | string \| `null` | Accelerator for Kiku field grouping on last card (default: `"CommandOrControl+G"`; only active when `behavior.autoUpdateNewCards` is `false`) |
| `triggerSubsync` | string \| `null` | Accelerator for running Subsync (default: `"Ctrl+Alt+S"`) |
| `mineSentence` | string \| `null` | Accelerator for creating sentence card from current subtitle (default: `"CommandOrControl+S"`) |
| `mineSentenceMultiple` | string \| `null` | Accelerator for multi-mine sentence card mode (default: `"CommandOrControl+Shift+S"`) |
| `multiCopyTimeoutMs` | number | Timeout in ms for multi-copy/mine digit input (default: `3000`) |
| `toggleSecondarySub` | string \| `null` | Accelerator for cycling secondary subtitle mode (default: `"CommandOrControl+Shift+V"`) |
| `markAudioCard` | string \| `null` | Accelerator for marking last card as audio card (default: `"CommandOrControl+Shift+A"`) |
| `openRuntimeOptions` | string \| `null` | Opens runtime options palette for live session-only toggles (default: `"CommandOrControl+Shift+O"`) |
| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Shift+J"`) |
| Option | Values | Description |
| ------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `toggleVisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling visible subtitle overlay (default: `"Alt+Shift+O"`) |
| `toggleInvisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling invisible interactive overlay (default: `"Alt+Shift+I"`) |
| `copySubtitle` | string \| `null` | Accelerator for copying current subtitle (default: `"CommandOrControl+C"`) |
| `copySubtitleMultiple` | string \| `null` | Accelerator for multi-copy mode (default: `"CommandOrControl+Shift+C"`) |
| `updateLastCardFromClipboard` | string \| `null` | Accelerator for updating card from clipboard (default: `"CommandOrControl+V"`) |
| `triggerFieldGrouping` | string \| `null` | Accelerator for Kiku field grouping on last card (default: `"CommandOrControl+G"`; only active when `behavior.autoUpdateNewCards` is `false`) |
| `triggerSubsync` | string \| `null` | Accelerator for running Subsync (default: `"Ctrl+Alt+S"`) |
| `mineSentence` | string \| `null` | Accelerator for creating sentence card from current subtitle (default: `"CommandOrControl+S"`) |
| `mineSentenceMultiple` | string \| `null` | Accelerator for multi-mine sentence card mode (default: `"CommandOrControl+Shift+S"`) |
| `multiCopyTimeoutMs` | number | Timeout in ms for multi-copy/mine digit input (default: `3000`) |
| `toggleSecondarySub` | string \| `null` | Accelerator for cycling secondary subtitle mode (default: `"CommandOrControl+Shift+V"`) |
| `markAudioCard` | string \| `null` | Accelerator for marking last card as audio card (default: `"CommandOrControl+Shift+A"`) |
| `openRuntimeOptions` | string \| `null` | Opens runtime options palette for live session-only toggles (default: `"CommandOrControl+Shift+O"`) |
| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Shift+J"`) |
**See `config.example.jsonc`** for the complete list of shortcut configuration options.
@@ -663,8 +664,8 @@ Set the initial vertical subtitle position (measured from the bottom of the scre
}
```
| Option | Values | Description |
| ---------- | --------------- | ------------------------------------------------------------------ |
| Option | Values | Description |
| ---------- | ---------------- | ---------------------------------------------------------------------- |
| `yPercent` | number (0 - 100) | Distance from the bottom as a percent of screen height (default: `10`) |
### Subtitle Style
@@ -691,25 +692,25 @@ See `config.example.jsonc` for detailed configuration options.
}
```
| Option | Values | Description |
| ----------------- | ----------- | ----------------------------------------------------------------------------- |
| `fontFamily` | string | CSS font-family value (default: `"Noto Sans CJK JP Regular, ..."`) |
| `fontSize` | number (px) | Font size in pixels (default: `35`) |
| `fontColor` | string | Any CSS color value (default: `"#cad3f5"`) |
| `fontWeight` | string | CSS font-weight, e.g. `"bold"`, `"normal"`, `"600"` (default: `"normal"`) |
| `fontStyle` | string | `"normal"` or `"italic"` (default: `"normal"`) |
| `backgroundColor` | string | Any CSS color, including `"transparent"` (default: `"rgba(54, 58, 79, 0.5)"`) |
| `enableJlpt` | boolean | Enable JLPT level underline styling (`false` by default) |
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
| `frequencyDictionary.sourcePath` | string | Path to a local frequency dictionary root. Leave empty or omit to use the built-in bundled dictionary search paths. |
| `frequencyDictionary.topX` | number | Only color tokens whose frequency rank is `<= topX` (`1000` by default) |
| `frequencyDictionary.mode` | string | `"single"` or `"banded"` (`"single"` by default) |
| `frequencyDictionary.singleColor` | string | Color used for all highlighted tokens in single mode |
| `frequencyDictionary.bandedColors` | string[] | Array of five hex colors used for ranked bands in banded mode |
| `nPlusOneColor` | string | Existing n+1 highlight color (default: `#c6a0f6`) |
| `knownWordColor` | string | Existing known-word highlight color (default: `#a6da95`) |
| `jlptColors` | object | JLPT level underline colors object (`N1`..`N5`) |
| `secondary` | object | Override any of the above for secondary subtitles (optional) |
| Option | Values | Description |
| ---------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------- |
| `fontFamily` | string | CSS font-family value (default: `"Noto Sans CJK JP Regular, ..."`) |
| `fontSize` | number (px) | Font size in pixels (default: `35`) |
| `fontColor` | string | Any CSS color value (default: `"#cad3f5"`) |
| `fontWeight` | string | CSS font-weight, e.g. `"bold"`, `"normal"`, `"600"` (default: `"normal"`) |
| `fontStyle` | string | `"normal"` or `"italic"` (default: `"normal"`) |
| `backgroundColor` | string | Any CSS color, including `"transparent"` (default: `"rgba(54, 58, 79, 0.5)"`) |
| `enableJlpt` | boolean | Enable JLPT level underline styling (`false` by default) |
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
| `frequencyDictionary.sourcePath` | string | Path to a local frequency dictionary root. Leave empty or omit to use the built-in bundled dictionary search paths. |
| `frequencyDictionary.topX` | number | Only color tokens whose frequency rank is `<= topX` (`1000` by default) |
| `frequencyDictionary.mode` | string | `"single"` or `"banded"` (`"single"` by default) |
| `frequencyDictionary.singleColor` | string | Color used for all highlighted tokens in single mode |
| `frequencyDictionary.bandedColors` | string[] | Array of five hex colors used for ranked bands in banded mode |
| `nPlusOneColor` | string | Existing n+1 highlight color (default: `#c6a0f6`) |
| `knownWordColor` | string | Existing known-word highlight color (default: `#a6da95`) |
| `jlptColors` | object | JLPT level underline colors object (`N1`..`N5`) |
| `secondary` | object | Override any of the above for secondary subtitles (optional) |
JLPT underlining is powered by offline term-meta bank files at runtime. See [`docs/jlpt-vocab-bundle.md`](jlpt-vocab-bundle.md) for required files, source/version refresh steps, and deterministic fallback behavior.
@@ -729,13 +730,13 @@ Secondary subtitle defaults: `fontSize: 24`, `fontColor: "#ffffff"`, `background
`jlptColors` keys are:
| Key | Default | Description |
| ---- | --------- | ---------------------------------------- |
| `N1` | `#ed8796` | JLPT N1 underline color |
| `N2` | `#f5a97f` | JLPT N2 underline color |
| `N3` | `#f9e2af` | JLPT N3 underline color |
| `N4` | `#a6e3a1` | JLPT N4 underline color |
| `N5` | `#8aadf4` | JLPT N5 underline color |
| Key | Default | Description |
| ---- | --------- | ----------------------- |
| `N1` | `#ed8796` | JLPT N1 underline color |
| `N2` | `#f5a97f` | JLPT N2 underline color |
| `N3` | `#f9e2af` | JLPT N3 underline color |
| `N4` | `#a6e3a1` | JLPT N4 underline color |
| `N5` | `#8aadf4` | JLPT N5 underline color |
### Texthooker
@@ -798,20 +799,20 @@ Enable or disable local immersion analytics stored in SQLite for mined subtitles
}
```
| Option | Values | Description |
| --- | --- | --- |
| `enabled` | `true`, `false` | Enable immersion tracking. Defaults to `true`. |
| `dbPath` | string | Optional SQLite database path. Leave empty to use default app-data path at `<config dir>/immersion.sqlite`. |
| `batchSize` | integer (`1`-`10000`) | Buffered writes per transaction. Default `25`. |
| `flushIntervalMs` | integer (`50`-`60000`) | Maximum queue delay before flush. Default `500ms`. |
| `queueCap` | integer (`100`-`100000`) | In-memory queue cap. Overflow drops oldest writes. Default `1000`. |
| `payloadCapBytes` | integer (`64`-`8192`) | Event payload byte cap before truncation marker. Default `256`. |
| `maintenanceIntervalMs` | integer (`60000`-`604800000`) | Prune + rollup maintenance cadence. Default `86400000` (24h). |
| `retention.eventsDays` | integer (`1`-`3650`) | Raw event retention window. Default `7` days. |
| `retention.telemetryDays` | integer (`1`-`3650`) | Telemetry retention window. Default `30` days. |
| `retention.dailyRollupsDays` | integer (`1`-`36500`) | Daily rollup retention window. Default `365` days. |
| `retention.monthlyRollupsDays` | integer (`1`-`36500`) | Monthly rollup retention window. Default `1825` days (~5 years). |
| `retention.vacuumIntervalDays` | integer (`1`-`3650`) | Minimum spacing between `VACUUM` passes. Default `7` days. |
| Option | Values | Description |
| ------------------------------ | ----------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable immersion tracking. Defaults to `true`. |
| `dbPath` | string | Optional SQLite database path. Leave empty to use default app-data path at `<config dir>/immersion.sqlite`. |
| `batchSize` | integer (`1`-`10000`) | Buffered writes per transaction. Default `25`. |
| `flushIntervalMs` | integer (`50`-`60000`) | Maximum queue delay before flush. Default `500ms`. |
| `queueCap` | integer (`100`-`100000`) | In-memory queue cap. Overflow drops oldest writes. Default `1000`. |
| `payloadCapBytes` | integer (`64`-`8192`) | Event payload byte cap before truncation marker. Default `256`. |
| `maintenanceIntervalMs` | integer (`60000`-`604800000`) | Prune + rollup maintenance cadence. Default `86400000` (24h). |
| `retention.eventsDays` | integer (`1`-`3650`) | Raw event retention window. Default `7` days. |
| `retention.telemetryDays` | integer (`1`-`3650`) | Telemetry retention window. Default `30` days. |
| `retention.dailyRollupsDays` | integer (`1`-`36500`) | Daily rollup retention window. Default `365` days. |
| `retention.monthlyRollupsDays` | integer (`1`-`36500`) | Monthly rollup retention window. Default `1825` days (~5 years). |
| `retention.vacuumIntervalDays` | integer (`1`-`3650`) | Minimum spacing between `VACUUM` passes. Default `7` days. |
When `dbPath` is blank or omitted, SubMiner writes telemetry and session summaries to the default app-data location:
@@ -838,14 +839,15 @@ Set defaults used by the `subminer` launcher for YouTube subtitle extraction/tra
}
```
| Option | Values | Description |
| -------------- | --------------------------------------- | ----------- |
| `mode` | `"automatic"`, `"preprocess"`, `"off"` | `automatic`: play immediately and load generated subtitles in background; `preprocess`: generate before playback; `off`: disable launcher generation. |
| `whisperBin` | string path | Path to `whisper.cpp` CLI binary used as fallback transcription engine. |
| `whisperModel` | string path | Path to whisper model used by fallback transcription. |
| `primarySubLanguages` | string[] | Primary subtitle language priority for YouTube subtitle generation (default `["ja", "jpn"]`). |
| Option | Values | Description |
| --------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mode` | `"automatic"`, `"preprocess"`, `"off"` | `automatic`: play immediately and load generated subtitles in background; `preprocess`: generate before playback; `off`: disable launcher generation. |
| `whisperBin` | string path | Path to `whisper.cpp` CLI binary used as fallback transcription engine. |
| `whisperModel` | string path | Path to whisper model used by fallback transcription. |
| `primarySubLanguages` | string[] | Primary subtitle language priority for YouTube subtitle generation (default `["ja", "jpn"]`). |
YouTube language targets are derived from subtitle config:
- primary track: `youtubeSubgen.primarySubLanguages` (falls back to `["ja","jpn"]`)
- secondary track: `secondarySub.secondarySubLanguages` (falls back to English when empty)

View File

@@ -83,14 +83,14 @@ make docs-preview # Preview built site at http://localhost:4173
Run `make help` for a full list of targets. Key ones:
| Target | Description |
| --- | --- |
| `make build` | Build platform package for detected OS |
| `make install` | Install platform artifacts (wrapper, theme, AppImage/app bundle) |
| `make install-plugin` | Install mpv Lua plugin and config |
| `make deps` | Install JS dependencies (root + texthooker-ui) |
| `make generate-config` | Generate default config from centralized registry |
| `make docs-dev` | Run VitePress dev server |
| Target | Description |
| ---------------------- | ---------------------------------------------------------------- |
| `make build` | Build platform package for detected OS |
| `make install` | Install platform artifacts (wrapper, theme, AppImage/app bundle) |
| `make install-plugin` | Install mpv Lua plugin and config |
| `make deps` | Install JS dependencies (root + texthooker-ui) |
| `make generate-config` | Generate default config from centralized registry |
| `make docs-dev` | Run VitePress dev server |
## Contributor Notes
@@ -104,13 +104,13 @@ Run `make help` for a full list of targets. Key ones:
## Environment Variables
| Variable | Description |
| --- | --- |
| `SUBMINER_APPIMAGE_PATH` | Override AppImage location for subminer script |
| `SUBMINER_BINARY_PATH` | Alias for `SUBMINER_APPIMAGE_PATH` |
| `SUBMINER_YT_SUBGEN_MODE` | Override `youtubeSubgen.mode` for launcher |
| `SUBMINER_WHISPER_BIN` | Override `youtubeSubgen.whisperBin` for launcher |
| `SUBMINER_WHISPER_MODEL` | Override `youtubeSubgen.whisperModel` for launcher |
| `SUBMINER_YT_SUBGEN_OUT_DIR` | Override generated subtitle output directory |
| `SUBMINER_YT_SUBGEN_AUDIO_FORMAT` | Override extraction format used for whisper fallback |
| `SUBMINER_YT_SUBGEN_KEEP_TEMP` | Set to `1` to keep temporary subtitle-generation workspace |
| Variable | Description |
| --------------------------------- | ---------------------------------------------------------- |
| `SUBMINER_APPIMAGE_PATH` | Override AppImage location for subminer script |
| `SUBMINER_BINARY_PATH` | Alias for `SUBMINER_APPIMAGE_PATH` |
| `SUBMINER_YT_SUBGEN_MODE` | Override `youtubeSubgen.mode` for launcher |
| `SUBMINER_WHISPER_BIN` | Override `youtubeSubgen.whisperBin` for launcher |
| `SUBMINER_WHISPER_MODEL` | Override `youtubeSubgen.whisperModel` for launcher |
| `SUBMINER_YT_SUBGEN_OUT_DIR` | Override generated subtitle output directory |
| `SUBMINER_YT_SUBGEN_AUDIO_FORMAT` | Override extraction format used for whisper fallback |
| `SUBMINER_YT_SUBGEN_KEEP_TEMP` | Set to `1` to keep temporary subtitle-generation workspace |

View File

@@ -4,13 +4,13 @@
### System Dependencies
| Dependency | Required | Notes |
| --- | --- | --- |
| mpv | Yes | Must support IPC sockets (`--input-ipc-server`) |
| ffmpeg | For media | Audio extraction and screenshot generation |
| MeCab + mecab-ipadic | No | Optional fallback tokenizer for Japanese |
| fuse2 | Linux only | Required for AppImage |
| yt-dlp | No | Recommended for YouTube playback and subtitle extraction |
| Dependency | Required | Notes |
| -------------------- | ---------- | -------------------------------------------------------- |
| mpv | Yes | Must support IPC sockets (`--input-ipc-server`) |
| ffmpeg | For media | Audio extraction and screenshot generation |
| MeCab + mecab-ipadic | No | Optional fallback tokenizer for Japanese |
| fuse2 | Linux only | Required for AppImage |
| yt-dlp | No | Recommended for YouTube playback and subtitle extraction |
### Platform-Specific
@@ -24,15 +24,15 @@
### Optional Tools
| Tool | Purpose |
| --- | --- |
| fzf | Terminal-based video picker (default) |
| rofi | GUI-based video picker |
| chafa | Thumbnail previews in fzf |
| ffmpegthumbnailer | Generate video thumbnails for picker |
| alass | Subtitle sync engine (preferred) |
| ffsubsync | Subtitle sync engine (fallback) |
| Bun | Required for the `subminer` wrapper script |
| Tool | Purpose |
| ----------------- | ------------------------------------------ |
| fzf | Terminal-based video picker (default) |
| rofi | GUI-based video picker |
| chafa | Thumbnail previews in fzf |
| ffmpegthumbnailer | Generate video thumbnails for picker |
| alass | Subtitle sync engine (preferred) |
| ffsubsync | Subtitle sync engine (fallback) |
| Bun | Required for the `subminer` wrapper script |
## Linux
@@ -162,18 +162,18 @@ cp plugin/subminer.conf ~/.config/mpv/script-opts/
All keybindings use a `y` chord prefix — press `y`, then the second key:
| Chord | Action |
| --- | --- |
| Chord | Action |
| ----- | ------------------------------------- |
| `y-y` | Open SubMiner menu (fuzzy-searchable) |
| `y-s` | Start overlay |
| `y-S` | Stop overlay |
| `y-t` | Toggle visible overlay |
| `y-i` | Toggle invisible overlay |
| `y-I` | Show invisible overlay |
| `y-u` | Hide invisible overlay |
| `y-o` | Open Yomitan settings |
| `y-r` | Restart overlay |
| `y-c` | Check overlay status |
| `y-s` | Start overlay |
| `y-S` | Stop overlay |
| `y-t` | Toggle visible overlay |
| `y-i` | Toggle invisible overlay |
| `y-I` | Show invisible overlay |
| `y-u` | Hide invisible overlay |
| `y-o` | Open Yomitan settings |
| `y-r` | Restart overlay |
| `y-c` | Check overlay status |
See [MPV Plugin](/mpv-plugin) for the full configuration reference, script messages, and binary auto-detection details.

View File

@@ -32,8 +32,8 @@ SubMiner includes an optional Jellyfin CLI integration for:
"iconCacheDir": "/tmp/subminer-jellyfin-icons",
"directPlayPreferred": true,
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"],
"transcodeVideoCodec": "h264"
}
"transcodeVideoCodec": "h264",
},
}
```

View File

@@ -179,13 +179,13 @@ When enabled, SubMiner highlights words you already know in your Anki deck, maki
}
```
| Option | Description |
|--------|-------------|
| `highlightEnabled` | Turn on/off the highlighting feature |
| `refreshMinutes` | How often to refresh the known-word cache (default: 1440 = daily) |
| `matchMode` | `"headword"` (dictionary form) or `"surface"` (exact text match) |
| Option | Description |
| ------------------ | ----------------------------------------------------------------------------------- |
| `highlightEnabled` | Turn on/off the highlighting feature |
| `refreshMinutes` | How often to refresh the known-word cache (default: 1440 = daily) |
| `matchMode` | `"headword"` (dictionary form) or `"surface"` (exact text match) |
| `minSentenceWords` | Minimum sentence length in tokens required to allow N+1 highlighting (default: `3`) |
| `decks` | Which Anki decks to consider "known" (empty = uses `ankiConnect.deck`) |
| `decks` | Which Anki decks to consider "known" (empty = uses `ankiConnect.deck`) |
### Use Cases

View File

@@ -21,18 +21,18 @@ input-ipc-server=/tmp/subminer-socket
All keybindings use a `y` chord prefix — press `y`, then the second key:
| Chord | Action |
| --- | --- |
| `y-y` | Open menu |
| `y-s` | Start overlay |
| `y-S` | Stop overlay |
| `y-t` | Toggle visible overlay |
| Chord | Action |
| ----- | ------------------------ |
| `y-y` | Open menu |
| `y-s` | Start overlay |
| `y-S` | Stop overlay |
| `y-t` | Toggle visible overlay |
| `y-i` | Toggle invisible overlay |
| `y-I` | Show invisible overlay |
| `y-u` | Hide invisible overlay |
| `y-o` | Open settings window |
| `y-r` | Restart overlay |
| `y-c` | Check status |
| `y-I` | Show invisible overlay |
| `y-u` | Hide invisible overlay |
| `y-o` | Open settings window |
| `y-r` | Restart overlay |
| `y-c` | Check status |
## Menu
@@ -90,34 +90,37 @@ log_level=info
### Option Reference
| Option | Default | Values | Description |
| --- | --- | --- | --- |
| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary |
| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path |
| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server |
| `texthooker_port` | `5174` | 165535 | Texthooker server port |
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
| `auto_start` | `no` | `yes` / `no` | Auto-start overlay on file load |
| `auto_start_visible_overlay` | `no` | `yes` / `no` | Show visible layer on auto-start |
| `auto_start_invisible_overlay` | `platform-default` | `platform-default`, `visible`, `hidden` | Invisible layer on auto-start |
| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
| Option | Default | Values | Description |
| ------------------------------ | ---------------------- | ------------------------------------------ | -------------------------------- |
| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary |
| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path |
| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server |
| `texthooker_port` | `5174` | 165535 | Texthooker server port |
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
| `auto_start` | `no` | `yes` / `no` | Auto-start overlay on file load |
| `auto_start_visible_overlay` | `no` | `yes` / `no` | Show visible layer on auto-start |
| `auto_start_invisible_overlay` | `platform-default` | `platform-default`, `visible`, `hidden` | Invisible layer on auto-start |
| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
## Binary Auto-Detection
When `binary_path` is empty, the plugin searches platform-specific locations:
**Linux:**
1. `~/.local/bin/SubMiner.AppImage`
2. `/opt/SubMiner/SubMiner.AppImage`
3. `/usr/local/bin/SubMiner`
4. `/usr/bin/SubMiner`
**macOS:**
1. `/Applications/SubMiner.app/Contents/MacOS/SubMiner`
2. `~/Applications/SubMiner.app/Contents/MacOS/SubMiner`
**Windows:**
1. `C:\Program Files\SubMiner\SubMiner.exe`
2. `C:\Program Files (x86)\SubMiner\SubMiner.exe`
3. `C:\SubMiner\SubMiner.exe`

View File

@@ -5,7 +5,6 @@
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
*/
{
// ==========================================
// Overlay Auto-Start
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
@@ -24,7 +23,7 @@
// Control whether browser opens automatically for texthooker.
// ==========================================
"texthooker": {
"openBrowser": true
"openBrowser": true,
},
// ==========================================
@@ -34,7 +33,7 @@
// ==========================================
"websocket": {
"enabled": "auto",
"port": 6677
"port": 6677,
},
// ==========================================
@@ -43,7 +42,7 @@
// Set to debug for full runtime diagnostics.
// ==========================================
"logging": {
"level": "info"
"level": "info",
},
// ==========================================
@@ -59,7 +58,7 @@
"image": "Picture",
"sentence": "Sentence",
"miscInfo": "MiscInfo",
"translation": "SelectionText"
"translation": "SelectionText",
},
"ai": {
"enabled": false,
@@ -68,7 +67,7 @@
"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."
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.",
},
"media": {
"generateAudio": true,
@@ -81,7 +80,7 @@
"animatedCrf": 35,
"audioPadding": 0.5,
"fallbackDuration": 3,
"maxMediaDuration": 30
"maxMediaDuration": 30,
},
"behavior": {
"overwriteAudio": true,
@@ -89,7 +88,7 @@
"mediaInsertMode": "append",
"highlightWord": true,
"notificationType": "osd",
"autoUpdateNewCards": true
"autoUpdateNewCards": true,
},
"nPlusOne": {
"highlightEnabled": false,
@@ -98,22 +97,22 @@
"decks": [],
"minSentenceWords": 3,
"nPlusOne": "#c6a0f6",
"knownWord": "#a6da95"
"knownWord": "#a6da95",
},
"metadata": {
"pattern": "[SubMiner] %f (%t)"
"pattern": "[SubMiner] %f (%t)",
},
"isLapis": {
"enabled": false,
"sentenceCardModel": "Japanese sentences",
"sentenceCardSentenceField": "Sentence",
"sentenceCardAudioField": "SentenceAudio"
"sentenceCardAudioField": "SentenceAudio",
},
"isKiku": {
"enabled": false,
"fieldGrouping": "disabled",
"deleteDuplicateInAuto": true
}
"deleteDuplicateInAuto": true,
},
},
// ==========================================
@@ -134,7 +133,7 @@
"toggleSecondarySub": "CommandOrControl+Shift+V",
"markAudioCard": "CommandOrControl+Shift+A",
"openRuntimeOptions": "CommandOrControl+Shift+O",
"openJimaku": "Ctrl+Shift+J"
"openJimaku": "Ctrl+Shift+J",
},
// ==========================================
@@ -144,7 +143,7 @@
// This edit-mode shortcut is fixed and is not currently configurable.
// ==========================================
"invisibleOverlay": {
"startupVisibility": "platform-default"
"startupVisibility": "platform-default",
},
// ==========================================
@@ -173,7 +172,7 @@
"N2": "#f5a97f",
"N3": "#f9e2af",
"N4": "#a6e3a1",
"N5": "#8aadf4"
"N5": "#8aadf4",
},
"frequencyDictionary": {
"enabled": false,
@@ -181,13 +180,7 @@
"topX": 1000,
"mode": "single",
"singleColor": "#f5a97f",
"bandedColors": [
"#ed8796",
"#f5a97f",
"#f9e2af",
"#a6e3a1",
"#8aadf4"
]
"bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"],
},
"secondary": {
"fontSize": 24,
@@ -195,8 +188,8 @@
"backgroundColor": "transparent",
"fontWeight": "normal",
"fontStyle": "normal",
"fontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif"
}
"fontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif",
},
},
// ==========================================
@@ -207,7 +200,7 @@
"secondarySub": {
"secondarySubLanguages": [],
"autoLoadSecondarySub": false,
"defaultMode": "hover"
"defaultMode": "hover",
},
// ==========================================
@@ -218,7 +211,7 @@
"defaultMode": "auto",
"alass_path": "",
"ffsubsync_path": "",
"ffmpeg_path": ""
"ffmpeg_path": "",
},
// ==========================================
@@ -226,7 +219,7 @@
// Initial vertical subtitle position from the bottom.
// ==========================================
"subtitlePosition": {
"yPercent": 10
"yPercent": 10,
},
// ==========================================
@@ -236,7 +229,7 @@
"jimaku": {
"apiBaseUrl": "https://jimaku.cc",
"languagePreference": "ja",
"maxEntryResults": 10
"maxEntryResults": 10,
},
// ==========================================
@@ -247,10 +240,7 @@
"mode": "automatic",
"whisperBin": "",
"whisperModel": "",
"primarySubLanguages": [
"ja",
"jpn"
]
"primarySubLanguages": ["ja", "jpn"],
},
// ==========================================
@@ -259,7 +249,7 @@
// ==========================================
"anilist": {
"enabled": false,
"accessToken": ""
"accessToken": "",
},
// ==========================================
@@ -284,16 +274,8 @@
"pullPictures": false,
"iconCacheDir": "/tmp/subminer-jellyfin-icons",
"directPlayPreferred": true,
"directPlayContainers": [
"mkv",
"mp4",
"webm",
"mov",
"flac",
"mp3",
"aac"
],
"transcodeVideoCodec": "h264"
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"],
"transcodeVideoCodec": "h264",
},
// ==========================================
@@ -315,7 +297,7 @@
"telemetryDays": 30,
"dailyRollupsDays": 365,
"monthlyRollupsDays": 1825,
"vacuumIntervalDays": 7
}
}
"vacuumIntervalDays": 7,
},
},
}

View File

@@ -4,17 +4,17 @@ Date: 2026-02-14
## 1) Oversized refactor candidates (>=400 LOC)
| File | Concern | Status | Reason |
| --- | --- | --- | --- |
| `src/main.ts` | Bootstrap / composition root / orchestration | Active | Main entrypoint owns startup, lifecycle orchestration, service construction, state mutation surfaces, and IPC bindings |
| `src/anki-integration.ts` | Domain service orchestration / integrations | Active | 2.6k+ LOC, high cyclomatic coupling to mpv/subtitle timing and mining flows |
| `src/core/services/mpv.ts` | MPV protocol + app state bridge | Active | ~780 LOC, large protocol and behavior mix, 22-entry dep interface |
| `src/core/services/subsync.ts` | Subsync orchestration (ffsubsync/alass workflows) | Active | ~494 LOC, file IO + mpv command orchestration + result shaping |
| `src/renderer/positioning.ts` | Renderer positioning layout policy | Active (downstream of TASK-27.5) | 513 LOC, layout/rules + platform-specific behavior in one module |
| `src/config/service.ts` | Config load/validation | Active support | ~601 LOC, central schema validation + mutation APIs |
| `src/types.ts` | Shared contract surface | Active support | ~640 LOC, foundational type exports driving module boundaries |
| `src/config/definitions.ts` | Config schema/registry definitions | Active support | ~480 LOC, dense constants/interfaces used by config runtime and docs |
| `src/media-generator.ts` | Media generation helpers | Active support | ~431 LOC, utility-heavy with multiple generation flows |
| File | Concern | Status | Reason |
| ------------------------------ | ------------------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `src/main.ts` | Bootstrap / composition root / orchestration | Active | Main entrypoint owns startup, lifecycle orchestration, service construction, state mutation surfaces, and IPC bindings |
| `src/anki-integration.ts` | Domain service orchestration / integrations | Active | 2.6k+ LOC, high cyclomatic coupling to mpv/subtitle timing and mining flows |
| `src/core/services/mpv.ts` | MPV protocol + app state bridge | Active | ~780 LOC, large protocol and behavior mix, 22-entry dep interface |
| `src/core/services/subsync.ts` | Subsync orchestration (ffsubsync/alass workflows) | Active | ~494 LOC, file IO + mpv command orchestration + result shaping |
| `src/renderer/positioning.ts` | Renderer positioning layout policy | Active (downstream of TASK-27.5) | 513 LOC, layout/rules + platform-specific behavior in one module |
| `src/config/service.ts` | Config load/validation | Active support | ~601 LOC, central schema validation + mutation APIs |
| `src/types.ts` | Shared contract surface | Active support | ~640 LOC, foundational type exports driving module boundaries |
| `src/config/definitions.ts` | Config schema/registry definitions | Active support | ~480 LOC, dense constants/interfaces used by config runtime and docs |
| `src/media-generator.ts` | Media generation helpers | Active support | ~431 LOC, utility-heavy with multiple generation flows |
## 2) API contracts by target file
@@ -102,24 +102,29 @@ Adopted sequence (from TASK-27 parent):
## 4) Compatibility and migration risks per split
### TASK-27.3 / anki integration surface
- Risk: interface breakage in field-grouping preview/build/enable flow
- Mitigation: keep constructor params and public methods stable; preserve IPC payload shapes
### TASK-27.2 (composition root)
- Risk: lifecycle/cleanup regressions from moving startup hooks and shutdown behavior
- Mitigation: preserve service construction order and keep existing event registration boundaries
Migration note:
- `src/main.ts` now delegates composition edges to `src/main/startup.ts`, `src/main/app-lifecycle.ts`, `src/main/startup-lifecycle.ts`, `src/main/ipc-runtime.ts`, `src/main/cli-runtime.ts`, and `src/main/subsync-runtime.ts`.
- `src/main.ts` now delegates composition edges to `src/main/startup.ts`, `src/main/app-lifecycle.ts`, `src/main/startup-lifecycle.ts`, `src/main/ipc-runtime.ts`, `src/main/cli-runtime.ts`, and `src/main/subsync-runtime.ts`.
- Overlay/modal interaction has been moved into `src/main/overlay-runtime.ts` (window selection, modal restore set tracking, runtime-options palette/modal close handling) so `main.ts` now uses a dedicated runtime service for modal routing instead of inline window bookkeeping.
- Stateful runtime session data has moved to `src/main/state.ts` via `createAppState()` so `main.ts` no longer owns the `AppState` shape inline, only importing and mutating the shared state instance.
- Behavioral contract remains stable: startup flow, CLI dispatch, IPC handlers, and subsync orchestration keep existing external behavior; only dependency wiring moved out of `main.ts`.
### TASK-27.4 (mpv-service)
- Risk: request/deps interface changes could break control and subsync interactions
- Mitigation: preserve public `MpvClient` methods, request semantics, and reconnect events while splitting internal modules
### TASK-27.4 expected event flow snapshot (current)
- `connect()` establishes socket and starts `observe_property` subscriptions via `subscribeToProperties()`.
- `processBuffer()` uses `splitMpvMessagesFromBuffer()` to parse JSON lines and route each message to `handleMessage()`.
- `dispatchMpvProtocolMessage()` now owns protocol-level handling for:
@@ -132,6 +137,7 @@ Migration note:
- timer callback calls `connect()` after exponential-ish delay
### TASK-27.5 (renderer positioning)
- Risk: UI placement drift on platform edge cases
- Mitigation: keep existing DOM state updates and geometry math in place while refactoring module boundaries

View File

@@ -2,9 +2,9 @@
There are two ways to use SubMiner — the `subminer` wrapper script or the mpv plugin:
| Approach | Best For |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **subminer script** | All-in-one solution. Handles video selection, launches MPV with the correct socket, starts the overlay automatically, and cleans up on exit. |
| Approach | Best For |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **subminer script** | All-in-one solution. Handles video selection, launches MPV with the correct socket, starts the overlay automatically, and cleans up on exit. |
| **MPV plugin** | When you launch MPV yourself or from other tools. Provides in-MPV chord keybindings (e.g. `y-y` for menu) to control visible and invisible overlay layers. Requires `--input-ipc-server=/tmp/subminer-socket`. |
You can use both together—install the plugin for on-demand control, but use `subminer` when you want the streamlined workflow.
@@ -145,11 +145,11 @@ Notes:
### Global Shortcuts
| Keybind | Action |
| ------------- | ------------------------- |
| `Alt+Shift+O` | Toggle visible overlay |
| `Alt+Shift+I` | Toggle invisible overlay |
| `Alt+Shift+Y` | Open Yomitan settings |
| Keybind | Action |
| ------------- | ------------------------ |
| `Alt+Shift+O` | Toggle visible overlay |
| `Alt+Shift+I` | Toggle invisible overlay |
| `Alt+Shift+Y` | Open Yomitan settings |
`Alt+Shift+Y` is a fixed global shortcut; it is not part of `shortcuts` config.