initial commit

This commit is contained in:
2026-02-09 19:04:19 -08:00
commit f92b57c7b6
531 changed files with 196294 additions and 0 deletions

18
docs/README.md Normal file
View File

@@ -0,0 +1,18 @@
# Documentation
Use this directory for detailed SubMiner documentation.
- [Installation](installation.md)
- Platform requirements
- AppImage / macOS / source installs
- mpv plugin setup
- [Usage](usage.md)
- Script vs plugin workflow
- Running SubMiner with mpv
- Keybindings and runtime behavior
- [Configuration](configuration.md)
- Full config file reference and option details
- [Development](development.md)
- Contributor notes
- Environment variables
- License and acknowledgments

565
docs/configuration.md Normal file
View File

@@ -0,0 +1,565 @@
# Configuration
Settings are stored in `~/.config/SubMiner/config.jsonc`
### Configuration File
See `config.example.jsonc` for a comprehensive example configuration file with all available options, default values, and detailed comments. Only include the options you want to customize in your config file.
Generate a fresh default config from the centralized config registry:
```bash
subminer.AppImage --generate-config
subminer.AppImage --generate-config --config-path /tmp/subminer.jsonc
subminer.AppImage --generate-config --backup-overwrite
```
- `--generate-config` writes a default JSONC config template.
- If the target file exists, SubMiner prompts to create a timestamped backup and overwrite.
- In non-interactive shells, use `--backup-overwrite` to explicitly back up and overwrite.
- `pnpm run generate:config-example` regenerates repository `config.example.jsonc` from the same centralized defaults.
- `make generate-config` builds and runs the same default-config generator via local Electron.
Invalid config values are handled with warn-and-fallback behavior: SubMiner logs the bad key/value and continues with the default for that option.
### Configuration Options Overview
The configuration file includes several main sections:
- [**AnkiConnect**](#ankiconnect) - Automatic Anki card creation with media
- [**Auto-Start Overlay**](#auto-start-overlay) - Automatically show overlay on MPV connection
- [**Visible Overlay Subtitle Binding**](#visible-overlay-subtitle-binding) - Link visible overlay toggles to MPV subtitle visibility
- [**Auto Subtitle Sync**](#auto-subtitle-sync) - Sync current subtitle with `alass`/`ffsubsync`
- [**Invisible Overlay**](#invisible-overlay) - Startup visibility behavior for the invisible mining layer
- [**Jimaku**](#jimaku) - Jimaku API configuration and defaults
- [**Keybindings**](#keybindings) - MPV command shortcuts
- [**Runtime Option Palette**](#runtime-option-palette) - Live, session-only option toggles
- [**Secondary Subtitles**](#secondary-subtitles) - Dual subtitle track support
- [**Shortcuts Configuration**](#shortcuts-configuration) - Overlay keyboard shortcuts
- [**Subtitle Position**](#subtitle-position) - Overlay vertical positioning
- [**Subtitle Style**](#subtitle-style) - Appearance customization
- [**Texthooker**](#texthooker) - Control browser opening behavior
- [**WebSocket Server**](#websocket-server) - Built-in subtitle broadcasting server
- [**YouTube Subtitle Generation**](#youtube-subtitle-generation) - Launcher defaults for yt-dlp + local whisper fallback
### AnkiConnect
Enable automatic Anki card creation and updates with media generation:
```json
{
"ankiConnect": {
"enabled": true,
"url": "http://127.0.0.1:8765",
"pollingRate": 3000,
"deck": "Learning::Japanese",
"fields": {
"audio": "ExpressionAudio",
"image": "Picture",
"sentence": "Sentence",
"miscInfo": "MiscInfo",
"translation": "SelectionText"
},
"ai": {
"enabled": false,
"alwaysUseAiTranslation": false,
"apiKey": "",
"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."
},
"media": {
"generateAudio": true,
"generateImage": true,
"imageType": "static",
"imageFormat": "jpg",
"imageQuality": 92,
"imageMaxWidth": 1280,
"imageMaxHeight": 720,
"animatedFps": 10,
"animatedMaxWidth": 640,
"animatedMaxHeight": 360,
"animatedCrf": 35,
"audioPadding": 0.5,
"fallbackDuration": 3,
"maxMediaDuration": 30
},
"behavior": {
"autoUpdateNewCards": true,
"overwriteAudio": true,
"overwriteImage": true
},
"metadata": {
"pattern": "[SubMiner] %f (%t)"
},
"isLapis": {
"enabled": true,
"sentenceCardModel": "Japanese sentences",
"sentenceCardSentenceField": "Sentence",
"sentenceCardAudioField": "SentenceAudio"
},
"isKiku": {
"enabled": false,
"fieldGrouping": "disabled",
"deleteDuplicateInAuto": true
}
}
}
```
This example is intentionally compact. The option table below documents available `ankiConnect` settings and behavior.
**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 |
| `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`) |
| `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:**
SubMiner supports the [Lapis](https://github.com/donkuri/lapis) and [Kiku](https://kiku.youyoumu.my.id/) note types. Both `isLapis.enabled` and `isKiku.enabled` can be true; Kiku takes precedence for grouping behavior, while sentence-card model/field settings come from `isLapis`.
When enabled, sentence cards automatically set `IsSentenceCard` to `"x"` and populate the `Expression` field. Audio cards set `IsAudioCard` to `"x"`.
Kiku extends Lapis with **field grouping** — when a duplicate card is detected (same Word/Expression), SubMiner merges the two cards' content into one using Kiku's `data-group-id` HTML structure, organizing each mining instance into separate pages within the note.
[![Field Grouping Demo](assets/kiku-integration-poster.jpg)](https://github.com/user-attachments/assets/bf2476cb-2351-4622-8143-c90e59b19213)
| Mode | Behavior |
| ---------- | -------------------------------------------------------------------------------------------------------------------------- |
| `auto` | Automatically merges the new card's content into the original; duplicate deletion is controlled by `deleteDuplicateInAuto` |
| `manual` | Shows an overlay popup to choose which card to keep and whether to delete the duplicate after merge |
| `disabled` | No field grouping; duplicate cards are left as-is |
`deleteDuplicateInAuto` controls whether `auto` mode deletes the duplicate after merge (default: `true`). In `manual` mode, the popup asks each time whether to delete the duplicate.
**Image Quality Notes:**
- `imageQuality` affects JPG and WebP only; PNG is lossless and ignores this setting
- JPG quality is mapped to FFmpeg's scale (2-31, lower = better)
- WebP quality uses FFmpeg's native 0-100 scale
**Manual Card Update:**
When `behavior.autoUpdateNewCards` is set to `false`, new cards are detected but not automatically updated. Instead, you can manually update cards using keyboard shortcuts:
| 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) |
To copy multiple lines (current + previous):
1. Press `Ctrl+Shift+C`
2. Press a number key (`1-9`) within 3 seconds
3. The specified number of most recent subtitle lines are copied
4. Press `Ctrl+V` to update the last added card with the copied lines
These shortcuts are only active when the overlay window is visible. They are automatically disabled when the overlay is hidden to avoid interfering with normal system clipboard operations.
### Auto-Start Overlay
Control whether the overlay automatically becomes visible when it connects to mpv:
```json
{
"auto_start_overlay": false
}
```
| Option | Values | Description |
| -------------------- | --------------- | ------------------------------------------------------ |
| `auto_start_overlay` | `true`, `false` | Auto-show overlay on mpv connection (default: `false`) |
The mpv plugin now controls startup per layer via `auto_start_visible_overlay` and `auto_start_invisible_overlay` in `subminer.conf` (`platform-default` for invisible means hidden on Linux, visible on macOS/Windows).
### Visible Overlay Subtitle Binding
Control whether toggling the visible overlay also toggles MPV subtitle visibility:
```json
{
"bind_visible_overlay_to_mpv_sub_visibility": true
}
```
| 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
Sync the active subtitle track using `alass` (preferred) or `ffsubsync`:
```json
{
"subsync": {
"defaultMode": "auto",
"alass_path": "",
"ffsubsync_path": "",
"ffmpeg_path": ""
}
}
```
| 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`. |
Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`.
Customize it there, or set it to `null` to disable.
### Invisible Overlay
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.
### Jimaku
Configure Jimaku API access and defaults:
```json
{
"jimaku": {
"apiKey": "YOUR_API_KEY",
"apiKeyCommand": "cat ~/.jimaku_key",
"apiBaseUrl": "https://jimaku.cc",
"languagePreference": "ja",
"maxEntryResults": 10
}
}
```
Jimaku is rate limited; if you hit a limit, SubMiner will surface the retry delay from the API response.
Set `openBrowser` to `false` to only print the URL without opening a browser.
### Keybindings
Add a `keybindings` array to configure keyboard shortcuts that send commands to mpv:
See `config.example.jsonc` for detailed configuration options and more examples.
**Default keybindings:**
| Key | Command | Description |
| ----------------- | -------------------------- | ------------------------------------- |
| `Space` | `["cycle", "pause"]` | Toggle pause |
| `ArrowRight` | `["seek", 5]` | Seek forward 5 seconds |
| `ArrowLeft` | `["seek", -5]` | Seek backward 5 seconds |
| `ArrowUp` | `["seek", 60]` | Seek forward 60 seconds |
| `ArrowDown` | `["seek", -60]` | Seek backward 60 seconds |
| `Shift+KeyH` | `["sub-seek", -1]` | Jump to previous subtitle |
| `Shift+KeyL` | `["sub-seek", 1]` | Jump to next subtitle |
| `Ctrl+Shift+KeyH` | `["__replay-subtitle"]` | Replay current subtitle, pause at end |
| `Ctrl+Shift+KeyL` | `["__play-next-subtitle"]` | Play next subtitle, pause at end |
| `KeyQ` | `["quit"]` | Quit mpv |
| `Ctrl+KeyW` | `["quit"]` | Quit mpv |
**Custom keybindings example:**
```json
{
"keybindings": [
{ "key": "ArrowRight", "command": ["seek", 5] },
{ "key": "ArrowLeft", "command": ["seek", -5] },
{ "key": "Shift+ArrowRight", "command": ["seek", 30] },
{ "key": "KeyR", "command": ["script-binding", "immersive/auto-replay"] },
{ "key": "KeyA", "command": ["script-message", "ankiconnect-add-note"] }
]
}
```
**Key format:** Use `KeyboardEvent.code` values (`Space`, `ArrowRight`, `KeyR`, etc.) with optional modifiers (`Ctrl+`, `Alt+`, `Shift+`, `Meta+`).
**Disable a default binding:** Set command to `null`:
```json
{ "key": "Space", "command": null }
```
**Special commands:** Commands prefixed with `__` are handled internally by the overlay rather than sent to mpv. `__replay-subtitle` replays the current subtitle and pauses at its end. `__play-next-subtitle` seeks to the next subtitle, plays it, and pauses at its end. `__runtime-options-open` opens the runtime options palette. `__runtime-option-cycle:<id>[:next|prev]` cycles a runtime option value.
**Supported commands:** Any valid mpv JSON IPC command array (`["cycle", "pause"]`, `["seek", 5]`, `["script-binding", "..."]`, etc.)
**See `config.example.jsonc`** for more keybinding examples and configuration options.
### Runtime Option Palette
Use the runtime options palette to toggle settings live while SubMiner is running. These changes are session-only and reset on restart.
Current runtime options:
- `ankiConnect.behavior.autoUpdateNewCards` (`On` / `Off`)
- `ankiConnect.isKiku.fieldGrouping` (`auto` / `manual` / `disabled`)
Default shortcut: `Ctrl+Shift+O`
Palette controls:
- `Arrow Up/Down`: select option
- `Arrow Left/Right`: change selected value
- `Enter`: apply selected value
- `Esc`: close
### Secondary Subtitles
Display a second subtitle track (e.g., English alongside Japanese) in the overlay:
See `config.example.jsonc` for detailed configuration options.
```json
{
"secondarySub": {
"secondarySubLanguages": ["eng", "en"],
"autoLoadSecondarySub": true,
"defaultMode": "hover"
}
}
```
| Option | Values | Description |
| ----------------------- | ---------------------------------- | ------------------------------------------------------ |
| `secondarySubLanguages` | string[] | Language codes to auto-load (e.g., `["eng", "en"]`) |
| `autoLoadSecondarySub` | `true`, `false` | Auto-detect and load matching secondary subtitle track |
| `defaultMode` | `"hidden"`, `"visible"`, `"hover"` | Initial display mode (default: `"hover"`) |
**Display modes:**
- **hidden** — Secondary subtitles not shown
- **visible** — Always visible at top of overlay
- **hover** — Only visible when hovering over the subtitle area (default)
**See `config.example.jsonc`** for additional secondary subtitle configuration options.
### Shortcuts Configuration
Customize or disable the overlay keyboard shortcuts:
See `config.example.jsonc` for detailed configuration options.
```json
{
"shortcuts": {
"toggleVisibleOverlayGlobal": "Alt+Shift+O",
"toggleInvisibleOverlayGlobal": "Alt+Shift+I",
"copySubtitle": "CommandOrControl+C",
"copySubtitleMultiple": "CommandOrControl+Shift+C",
"updateLastCardFromClipboard": "CommandOrControl+V",
"triggerFieldGrouping": "CommandOrControl+G",
"triggerSubsync": "Ctrl+Alt+S",
"mineSentence": "CommandOrControl+S",
"mineSentenceMultiple": "CommandOrControl+Shift+S",
"markAudioCard": "CommandOrControl+Shift+A",
"openRuntimeOptions": "CommandOrControl+Shift+O",
"multiCopyTimeoutMs": 3000
}
}
```
| 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"`) |
**See `config.example.jsonc`** for the complete list of shortcut configuration options.
Set any shortcut to `null` to disable it.
### Subtitle Position
Set the initial vertical subtitle position (measured from the bottom of the screen):
```json
{
"subtitlePosition": {
"yPercent": 10
}
}
```
| Option | Values | Description |
| ---------- | --------------- | ------------------------------------------------------------------ |
| `yPercent` | number (0 - 100) | Distance from the bottom as a percent of screen height (default: `10`) |
### Subtitle Style
Customize the appearance of primary and secondary subtitles:
See `config.example.jsonc` for detailed configuration options.
```json
{
"subtitleStyle": {
"fontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif",
"fontSize": 35,
"fontColor": "#cad3f5",
"fontWeight": "normal",
"fontStyle": "normal",
"backgroundColor": "rgba(54, 58, 79, 0.5)",
"secondary": {
"fontSize": 24,
"fontColor": "#ffffff",
"backgroundColor": "transparent"
}
}
}
```
| 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)"`) |
| `secondary` | object | Override any of the above for secondary subtitles (optional) |
Secondary subtitle defaults: `fontSize: 24`, `fontColor: "#ffffff"`, `backgroundColor: "transparent"`. Any property not set in `secondary` falls back to the CSS defaults.
**See `config.example.jsonc`** for the complete list of subtitle style configuration options.
### Texthooker
Control whether the browser opens automatically when texthooker starts:
See `config.example.jsonc` for detailed configuration options.
```json
{
"texthooker": {
"openBrowser": true
}
}
```
### WebSocket Server
The overlay includes a built-in WebSocket server that broadcasts subtitle text to connected clients (such as texthooker-ui) for external processing.
By default, the server uses "auto" mode: it starts automatically unless [mpv_websocket](https://github.com/kuroahna/mpv_websocket) is detected at `~/.config/mpv/mpv_websocket`. If you have mpv_websocket installed, the built-in server is skipped to avoid conflicts.
See `config.example.jsonc` for detailed configuration options.
```json
{
"websocket": {
"enabled": "auto",
"port": 6677
}
}
```
| Option | Values | Description |
| --------- | ------------------------- | -------------------------------------------------------- |
| `enabled` | `true`, `false`, `"auto"` | `"auto"` (default) disables if mpv_websocket is detected |
| `port` | number | WebSocket server port (default: 6677) |
### YouTube Subtitle Generation
Set defaults used by the `subminer` launcher for YouTube subtitle extraction/transcription:
```json
{
"youtubeSubgen": {
"mode": "automatic",
"whisperBin": "/path/to/whisper-cli",
"whisperModel": "/path/to/ggml-model.bin",
"primarySubLanguages": ["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)
Precedence for launcher defaults is: CLI flag > environment variable > `config.jsonc` > built-in default.

15
docs/development.md Normal file
View File

@@ -0,0 +1,15 @@
# Contributor Note
To add or change a config option, update `src/config/definitions.ts` first. Defaults, runtime-option metadata, and generated `config.example.jsonc` are derived from this centralized source.
## Environment Variables
| Variable | Description |
| ------------------------ | ---------------------------------------------- |
| `SUBMINER_APPIMAGE_PATH` | Override AppImage location for subminer script |
| `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 |

246
docs/installation.md Normal file
View File

@@ -0,0 +1,246 @@
# Requirements
### Linux
- **Wayland/X11 compositor** (one of the following):
- Hyprland (uses `hyprctl`)
- X11 (uses `xdotool` and `xwininfo`)
- mpv (with IPC socket support)
- mecab and mecab-ipadic (Japanese morphological analyzer)
- fuse2 (for AppImage support)
### macOS
- macOS 10.13 or later
- mpv (with IPC socket support)
- mecab and mecab-ipadic (Japanese morphological analyzer) - optional
- **Accessibility permission** required for window tracking (see [macOS Installation](#macos-installation))
**Optional:**
- fzf (terminal-based video picker, default)
- rofi (GUI-based video picker)
- chafa (thumbnail previews in fzf)
- ffmpegthumbnailer (generate video thumbnails)
- yt-dlp (recommended for reliable YouTube playback/subtitles in mpv)
- bun (required to run the `subminer` wrapper script from source/local installs)
## Installation
### From AppImage (Recommended)
Download the latest AppImage from GitHub Releases:
```bash
# Download and install AppImage
wget https://github.com/sudacode/subminer/releases/download/v1.0.0/subminer-1.0.0.AppImage -O ~/.local/bin/subminer.AppImage
chmod +x ~/.local/bin/subminer.AppImage
# Download subminer wrapper script
wget https://github.com/sudacode/subminer/releases/download/v1.0.0/subminer -O ~/.local/bin/subminer
chmod +x ~/.local/bin/subminer
```
Note: the `subminer` wrapper uses a Bun shebang (`#!/usr/bin/env bun`), so `bun` must be installed and available on `PATH`.
### macOS Installation
If you download a release, use the **DMG** artifact. Open it and drag `SubMiner.app` into `/Applications`.
If needed, you can use the **ZIP** artifact as a fallback by unzipping and dragging `SubMiner.app` into `/Applications`.
Install dependencies using Homebrew:
```bash
brew install mpv mecab mecab-ipadic
```
Build from source:
```bash
git clone https://github.com/sudacode/subminer.git
cd subminer
pnpm install
cd vendor/texthooker-ui && pnpm install && pnpm build && cd ../..
pnpm run build:mac
```
The built app will be available in the `release` directory (`.dmg` and `.zip` on macOS).
If you are building locally without Apple signing credentials, use:
```bash
pnpm run build:mac:unsigned
```
You can launch `SubMiner.app` directly (double-click or `open -a SubMiner`).
Use `--start` when you want SubMiner to begin MPV IPC connection/reconnect behavior.
Use `--texthooker` when you only want the texthooker page (no overlay window).
**Accessibility Permission:**
After launching the app for the first time, grant accessibility permission:
1. Open **System Preferences****Security & Privacy****Privacy** tab
2. Select **Accessibility** from the left sidebar
3. Add SubMiner to the list
Without this permission, window tracking will not work and the overlay won't follow the MPV window.
<!-- ### Maintainer: macOS Release Signing/Notarization
The GitHub release workflow builds signed and notarized macOS artifacts on `macos-latest`.
Set these GitHub Actions secrets before creating a release tag:
- `CSC_LINK` (base64 `.p12` certificate or file URL for Developer ID Application cert)
- `CSC_KEY_PASSWORD` (password for the `.p12`)
- `APPLE_ID` (Apple ID email)
- `APPLE_APP_SPECIFIC_PASSWORD` (app-specific password for notarization)
- `APPLE_TEAM_ID` (Apple Developer Team ID) -->
### From Source
```bash
git clone https://github.com/sudacode/subminer.git
cd subminer
make build
# Install platform artifacts
# - Linux: wrapper + theme (+ AppImage if present)
# - macOS: wrapper + theme + SubMiner.app (from release/*.app or release/*.zip)
make install
```
<!-- ### Arch Linux -->
<!-- ```bash -->
<!-- # Using the PKGBUILD -->
<!-- makepkg -si -->
<!-- ``` -->
### macOS Usage Notes
**Launching MPV with IPC:**
```bash
mpv --input-ipc-server=/tmp/subminer-socket video.mkv
```
**Config Location:**
Settings are stored in `~/.config/SubMiner/config.jsonc` (same as Linux).
**MeCab Installation Paths:**
Common Homebrew install paths:
- Apple Silicon (M1/M2): `/opt/homebrew/bin/mecab`
- Intel: `/usr/local/bin/mecab`
Ensure that `mecab` is available on your PATH when launching subminer (for example, by starting it from a terminal where `which mecab` works), otherwise MeCab may not be detected.
**Fullscreen Mode:**
The overlay should appear correctly in fullscreen. If you encounter issues, check that macOS accessibility permissions are granted (see [macOS Installation](#macos-installation)).
**mpv Plugin Binary Path (macOS):**
Set `binary_path` to your app binary, for example:
```ini
binary_path=/Applications/SubMiner.app/Contents/MacOS/subminer
```
### MPV Plugin (Optional)
The Lua plugin allows you to control the overlay directly from mpv using keybindings:
> [!IMPORTANT]
> `mpv` must be launched with `--input-ipc-server=/tmp/subminer-socket` to allow communication with the application
```bash
# Copy plugin files to mpv config
cp plugin/subminer.lua ~/.config/mpv/scripts/
cp plugin/subminer.conf ~/.config/mpv/script-opts/
```
#### Plugin Keybindings
All keybindings use chord sequences starting with `y`:
| Keybind | 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 |
The menu provides options to start/stop/toggle the visible or invisible overlay layers and open settings. Type to filter or use arrow keys to navigate.
#### Plugin Configuration
Edit `~/.config/mpv/script-opts/subminer.conf`:
```ini
# Path to SubMiner binary (leave empty for auto-detection)
binary_path=
# Path to mpv IPC socket (must match input-ipc-server in mpv.conf)
socket_path=/tmp/subminer-socket
# Enable texthooker WebSocket server
texthooker_enabled=yes
# Texthooker WebSocket port
texthooker_port=5174
# Window manager backend: auto, hyprland, x11, macos
backend=auto
# Automatically start overlay when a file is loaded
auto_start=no
# Automatically show visible overlay when overlay starts
auto_start_visible_overlay=no
# Automatically show invisible overlay when overlay starts
# Values: platform-default, visible, hidden
# platform-default => hidden on Linux, visible on macOS/Windows
auto_start_invisible_overlay=platform-default
# Show OSD messages for overlay status
osd_messages=yes
```
The plugin auto-detects the binary location, searching:
- `/Applications/SubMiner.app/Contents/MacOS/subminer`
- `~/Applications/SubMiner.app/Contents/MacOS/subminer`
- `C:\Program Files\subminer\subminer.exe`
- `C:\Program Files (x86)\subminer\subminer.exe`
- `C:\subminer\subminer.exe`
- `~/.local/bin/subminer.AppImage`
- `/opt/subminer/subminer.AppImage`
- `/usr/local/bin/subminer`
- `/usr/bin/subminer`
**Windows Notes:**
Set the binary and socket path like this:
```ini
binary_path=C:\\Program Files\\subminer\\subminer.exe
socket_path=\\\\.\\pipe\\subminer-socket
```
Launch mpv with:
```bash
mpv --input-ipc-server=\\\\.\\pipe\\subminer-socket video.mkv
```

131
docs/usage.md Normal file
View File

@@ -0,0 +1,131 @@
# SubMiner Script vs MPV Plugin
There are two ways to use SubMiner:
| 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.
`subminer` is implemented as a Bun script and runs directly via shebang (no `bun run` needed), for example: `subminer video.mkv`.
## Usage
```bash
# Browse and play videos
subminer # Current directory (uses fzf)
subminer -R # Use rofi instead of fzf
subminer -d ~/Videos # Specific directory
subminer -r -d ~/Anime # Recursive search
subminer video.mkv # Play specific file
subminer https://youtu.be/... # Play a YouTube URL
subminer ytsearch:"jp news" # Play first YouTube search result
# Options
subminer -T video.mkv # Disable texthooker server
subminer -b x11 video.mkv # Force X11 backend
subminer video.mkv # Uses mpv profile "subminer" by default
subminer -p gpu-hq video.mkv # Override mpv profile
subminer --yt-subgen-mode preprocess --whisper-bin /path/to/whisper-cli --whisper-model /path/to/model.bin https://youtu.be/... # Pre-generate subtitle tracks before playback
# Direct AppImage control
subminer.AppImage --start --texthooker # Start overlay with texthooker
subminer.AppImage --texthooker # Launch texthooker only (no overlay window)
subminer.AppImage --stop # Stop overlay
subminer.AppImage --start --toggle # Start MPV IPC + toggle visibility
subminer.AppImage --start --toggle-invisible-overlay # Start MPV IPC + toggle invisible layer
subminer.AppImage --show-visible-overlay # Force show visible overlay
subminer.AppImage --hide-visible-overlay # Force hide visible overlay
subminer.AppImage --show-invisible-overlay # Force show invisible overlay
subminer.AppImage --hide-invisible-overlay # Force hide invisible overlay
subminer.AppImage --settings # Open Yomitan settings
subminer.AppImage --help # Show all options
```
### MPV Profile Example (mpv.conf)
Add a profile to `~/.config/mpv/mpv.conf`; `subminer` now launches mpv with `--profile=subminer` by default (or override with `subminer -p <profile> ...`):
```ini
[subminer]
# IPC socket (must match SubMiner config)
input-ipc-server=/tmp/subminer-socket
# Prefer JP subs, then EN
slang=ja,jpn,en,eng
# Auto-load external subtitles
sub-auto=fuzzy
sub-file-paths=.;subs;subtitles
# Select primary + secondary subtitle tracks automatically
sid=auto
secondary-sid=auto
secondary-sub-visibility=no
```
`secondary-slang` is not an mpv option; use `slang` with `sid=auto` / `secondary-sid=auto` instead.
### YouTube Playback
`subminer` accepts direct URLs (for example, YouTube links) and `ytsearch:` targets, and forwards them to mpv.
Notes:
- Install `yt-dlp` so mpv can resolve YouTube streams and subtitle tracks reliably.
- `subminer` supports three subtitle-generation modes for YouTube URLs:
- `automatic` (default): starts playback immediately, generates subtitles in the background, and loads them into mpv when ready.
- `preprocess`: generates subtitles first, then starts playback with generated `.srt` files attached.
- `off`: disables launcher generation and leaves subtitle handling to mpv/yt-dlp.
- Primary subtitle target languages come from `youtubeSubgen.primarySubLanguages` (defaults to `["ja","jpn"]`).
- Secondary target languages come from `secondarySub.secondarySubLanguages` (defaults to English if unset).
- `subminer` prefers subtitle tracks from yt-dlp first, then falls back to local `whisper.cpp` (`whisper-cli`) when tracks are missing.
- Whisper translation fallback currently only supports English secondary targets; non-English secondary targets rely on yt-dlp subtitle availability.
- Configure defaults in `~/.config/SubMiner/config.jsonc` under `youtubeSubgen` and `secondarySub`, or override mode/tool paths via CLI flags/environment variables.
## Keybindings
### Global Shortcuts
| Keybind | Action |
| ------------- | ------------------------- |
| `Alt+Shift+O` | Toggle visible overlay |
| `Alt+Shift+I` | Toggle invisible overlay |
| `Alt+Shift+Y` | Open Yomitan settings |
### Overlay Controls (Configurable)
| Input | Action |
| -------------------- | -------------------------------------------------- |
| `Space` | Toggle MPV pause |
| `ArrowRight` | Seek forward 5 seconds |
| `ArrowLeft` | Seek backward 5 seconds |
| `ArrowUp` | Seek forward 60 seconds |
| `ArrowDown` | Seek backward 60 seconds |
| `Shift+H` | Jump to previous subtitle |
| `Shift+L` | Jump to next subtitle |
| `Ctrl+Shift+H` | Replay current subtitle (play to end, then pause) |
| `Ctrl+Shift+L` | Play next subtitle (jump, play to end, then pause) |
| `Q` | Quit mpv |
| `Ctrl+W` | Quit mpv |
| `Right-click` | Toggle MPV pause (outside subtitle area) |
| `Right-click + drag` | Move subtitle position (on subtitle) |
These keybindings only work when the overlay window has focus. See [Configuration](configuration.md) for customization.
### Overlay Chord Shortcuts
| Chord | Action |
| --------- | ------------------------- |
| `y``j` | Open Jimaku subtitle menu |
## How It Works
1. MPV runs with an IPC socket at `/tmp/subminer-socket`
2. The overlay connects and subscribes to subtitle changes
3. Subtitles are tokenized with MeCab and merged into natural word boundaries
4. Words are displayed as clickable spans
5. Clicking a word triggers Yomitan popup for dictionary lookup
6. Texthooker server runs at `http://127.0.0.1:5174` for external tools