diff --git a/docs-site/README.md b/docs-site/README.md
index 16d10ef4..687452cb 100644
--- a/docs-site/README.md
+++ b/docs-site/README.md
@@ -1,4 +1,4 @@
-# SubMiner Docs
+# SubMiner docs
In-repo VitePress documentation source for SubMiner.
diff --git a/docs-site/anilist-integration.md b/docs-site/anilist-integration.md
index 4852f46c..a5a490b2 100644
--- a/docs-site/anilist-integration.md
+++ b/docs-site/anilist-integration.md
@@ -1,10 +1,10 @@
-# AniList Integration
+# AniList integration
-SubMiner can sync your watch progress to [AniList](https://anilist.co) automatically. When you finish an episode, SubMiner detects the title and episode number from the filename, finds the matching AniList entry, and updates your progress via the GraphQL API. Failed updates are retried with exponential backoff in the background.
+SubMiner syncs your watch progress to [AniList](https://anilist.co). Finish an episode and it reads the title and episode number off the filename, finds the matching AniList entry, and updates your progress through the GraphQL API. A failed update retries in the background with exponential backoff.
-AniList data also powers two additional features: [cover art](#cover-art) for the stats dashboard and the [Character Dictionary](/character-dictionary) for in-overlay name lookup.
+The same AniList data feeds [cover art](#cover-art) in the stats dashboard and the [Character Dictionary](/character-dictionary) for in-overlay name lookup.
-[AniList](https://anilist.co) is a free website for tracking which anime you have watched. An **access token** is a private key SubMiner stores so it can update your list on your behalf - you approve it once during setup, and you never paste a password into SubMiner.
+[AniList](https://anilist.co) is a free anime tracking site. The **access token** is a private key SubMiner keeps so it can update your list for you. You approve it once during setup, and your AniList password never touches SubMiner.
## Setup
@@ -32,18 +32,18 @@ If the embedded auth UI fails to render, SubMiner opens the authorize URL in you
You can also set `anilist.accessToken` directly in config to skip the setup flow entirely. When blank, SubMiner uses the locally stored encrypted token.
:::
-## How Tracking Works
+## How tracking works
-SubMiner monitors playback and triggers an AniList progress update when an episode is considered "watched" -- at least 85% of the episode duration viewed and a minimum of 10 minutes watched.
+SubMiner watches playback and pushes an AniList progress update once an episode counts as watched. That means at least 85% of its duration, and at least 10 minutes either way.
The update flow:
-1. **Title detection** -- SubMiner extracts the anime title, season, and episode number from the media filename and path. Season folders such as `Season 2` are treated as a strong season signal. SubMiner tries [`guessit`](https://github.com/guessit-io/guessit) first for accurate parsing, then falls back to an internal filename parser if guessit is unavailable.
-2. **AniList search** -- The base title (with any `Season N` / `SN` marker stripped) is searched against the AniList GraphQL API, and SubMiner picks the best match by comparing titles (romaji, English, native, synonyms) and filtering by episode count. AniList has no notion of numbered seasons -- sequels are separate entries with their own titles (`Zoku`, `Kan`, `2nd Season`), so searching `
Season 3` finds nothing. For season 2 and later, SubMiner instead walks `SEQUEL` relations from the season 1 entry, preferring the TV line, and falls back to ordering the franchise's TV entries by air date when the relation chain is incomplete. If neither locates the season, SubMiner **skips the update** rather than writing progress to the season 1 entry, and tells you to pin the right entry with a [character dictionary override](/character-dictionary#correcting-anilist-matches).
-3. **Progress check** -- SubMiner fetches your current list entry for the matched media. The media must already be in Planning or Watching; otherwise SubMiner shows an MPV message explaining that the update is not possible. If your recorded progress already meets or exceeds the detected episode, the update is skipped.
-4. **Mutation** -- A `SaveMediaListEntry` mutation sets the new progress and marks the entry as `CURRENT`, or `COMPLETED` when the watched episode is the final episode of the season (the "already at this progress" skip is bypassed for the final episode so completion still lands).
+1. **Title detection** - SubMiner extracts the anime title, season, and episode number from the media filename and path. Season folders such as `Season 2` are treated as a strong season signal. SubMiner tries [`guessit`](https://github.com/guessit-io/guessit) first for accurate parsing, then falls back to an internal filename parser if guessit is unavailable.
+2. **AniList search** - The base title (with any `Season N` / `SN` marker stripped) is searched against the AniList GraphQL API, and SubMiner picks the best match by comparing titles (romaji, English, native, synonyms) and filtering by episode count. AniList has no notion of numbered seasons - sequels are separate entries with their own titles (`Zoku`, `Kan`, `2nd Season`), so searching ` Season 3` finds nothing. For season 2 and later, SubMiner instead walks `SEQUEL` relations from the season 1 entry, preferring the TV line, and falls back to ordering the franchise's TV entries by air date when the relation chain is incomplete. If neither locates the season, SubMiner **skips the update** rather than writing progress to the season 1 entry, and tells you to pin the right entry with a [character dictionary override](/character-dictionary#correcting-anilist-matches).
+3. **Progress check** - SubMiner fetches your current list entry for the matched media. The media must already be in Planning or Watching; otherwise SubMiner shows an MPV message explaining that the update is not possible. If your recorded progress already meets or exceeds the detected episode, the update is skipped.
+4. **Mutation** - A `SaveMediaListEntry` mutation sets the new progress and marks the entry as `CURRENT`, or `COMPLETED` when the watched episode is the final episode of the season (the "already at this progress" skip is bypassed for the final episode so completion still lands).
-## Update Queue and Retry
+## Update queue and retry
Failed AniList updates are persisted to a retry queue on disk and retried with exponential backoff.
@@ -58,7 +58,7 @@ After 8 failed attempts, the update is moved to a dead-letter queue and no longe
Use `--anilist-retry-queue` to manually process one ready item from the queue.
-## Cover Art
+## Cover art
SubMiner fetches cover art from AniList for display in the stats dashboard. When a new video starts playing, the cover art fetcher:
@@ -71,11 +71,11 @@ A no-match result is cached for 5 minutes before SubMiner retries, preventing re
If the automatic match is wrong, use **Change AniList Entry** on a title in the stats Library. Relinking rewrites the cached art for every episode of that title, and both the detail view and the Library grid pick up the new cover right away: the grid refetches after a relink, and cover responses carry an ETag and are revalidated on each request instead of being cached for a day.
-## Rate Limiting
+## Rate limiting
All AniList API calls go through a shared rate limiter that enforces a sliding window of 20 requests per minute. The limiter also reads AniList's `X-RateLimit-Remaining` and `Retry-After` response headers and pauses requests when the server signals throttling. This applies to both episode tracking and cover art fetching.
-## Configuration Reference
+## Configuration reference
```jsonc
{
@@ -107,7 +107,7 @@ All AniList API calls go through a shared rate limiter that enforces a sliding w
There is no `characterDictionary.enabled` key: character dictionary sync is enabled by `subtitleStyle.nameMatchEnabled`. See the [Character Dictionary](/character-dictionary) page for full details on the character dictionary feature, including name generation, matching, auto-sync lifecycle, and dictionary entry format.
-## CLI Commands
+## CLI commands
| Command | Description |
| ----------------------- | ------------------------------------------------------------- |
@@ -124,10 +124,10 @@ There is no `characterDictionary.enabled` key: character dictionary sync is enab
- **Token issues:** Run `--anilist-status` to check token state. If the token is invalid or expired, run `--anilist-setup` or `--anilist-logout` and re-authenticate.
- **Updates failing repeatedly:** Run `--anilist-status` to see retry queue counters. Items that fail 8 times are moved to the dead-letter queue. Check network connectivity and AniList API status.
- **Cover art missing:** Cover art is fetched on a best-effort basis using title matching. If the filename is hard to parse, the search may return no results. The fetcher retries after 5 minutes.
-- **Encryption unavailable on Linux:** If you see warnings about safeStorage, try `--password-store=basic_text` as a workaround, or ensure your desktop keyring (gnome-keyring, KWallet) is running.
+- **Encryption unavailable on Linux:** If you see warnings about safeStorage, try `--password-store=basic_text` as a workaround, or start your desktop keyring (gnome-keyring, KWallet).
## Related
-- [Character Dictionary](/character-dictionary) -- AniList-powered character name dictionary for Yomitan
-- [Configuration Reference](/configuration) -- full config options
-- [Jellyfin Integration](/jellyfin-integration) -- media server integration
+- [Character Dictionary](/character-dictionary) - AniList-powered character name dictionary for Yomitan
+- [Configuration Reference](/configuration) - full config options
+- [Jellyfin Integration](/jellyfin-integration) - media server integration
diff --git a/docs-site/aniskip-integration.md b/docs-site/aniskip-integration.md
index fc9bc230..ee5ff5dd 100644
--- a/docs-site/aniskip-integration.md
+++ b/docs-site/aniskip-integration.md
@@ -1,8 +1,8 @@
-# AniSkip Integration
+# AniSkip integration
-SubMiner integrates with [AniSkip](https://aniskip.com) to automatically detect anime intro intervals and let you skip them with a single key press.
+SubMiner looks up anime intro timings from [AniSkip](https://aniskip.com) so you can jump past the OP with one key.
-Intro detection runs in the SubMiner app over the mpv IPC socket. It is available whenever the overlay is connected to mpv - not just at launch - and covers every local file loaded during an mpv session, including playlist advances.
+Intro detection runs in the SubMiner app over the mpv IPC socket. It works whenever the overlay is connected to mpv, not only at launch, and covers every local file loaded during the session including playlist advances.
## Setup
@@ -25,9 +25,9 @@ For best title and episode detection, install [`guessit`](https://github.com/gue
python3 -m pip install --user guessit
```
-Without `guessit`, SubMiner falls back to an internal filename parser which handles most common naming conventions but may miss unusual formats.
+Without `guessit`, SubMiner falls back to its own filename parser. That handles the usual release naming, but unusual formats slip past it.
-## How It Works
+## How it works
On each local file load:
@@ -39,15 +39,15 @@ On each local file load:
When a custom key (other than `TAB` or `y-k`) is configured, the legacy `y-k` chord is also bound as a fallback skip trigger.
-Results are cached per file for the app session; only definitive "no intro found" results are cached, so transient lookup failures are retried on the next file load. Reload detection is also handled: if mpv reloads the same file, SubMiner re-applies the chapter markers without a new API lookup.
+Results are cached per file for the app session. Only a definitive "no intro found" is cached, so a failed lookup gets retried on the next load rather than sticking. If mpv reloads the same file, SubMiner re-applies the chapter markers without hitting the API again.
## Triggering from mpv
-You can trigger AniSkip actions from mpv script-messages:
+AniSkip actions are also reachable from mpv script-messages:
| Command | Effect |
| ------- | ------ |
| `script-message subminer-skip-intro` | Skip to the intro end immediately (same as pressing the key) |
| `script-message subminer-aniskip-refresh` | Force a fresh lookup for the current file, discarding any cached result |
-These are handled by the SubMiner app over the IPC socket.
+The SubMiner app handles both over the IPC socket.
diff --git a/docs-site/anki-integration.md b/docs-site/anki-integration.md
index 9c0a4acf..3a0814c3 100644
--- a/docs-site/anki-integration.md
+++ b/docs-site/anki-integration.md
@@ -1,4 +1,4 @@
-# Anki Integration
+# Anki integration
SubMiner uses the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on to create and update Anki cards with sentence context, audio, and screenshots.
This project is built primarily for [Kiku](https://kiku.youyoumu.my.id/) and [Lapis](https://github.com/donkuri/lapis) note types, including sentence-card and field-grouping behavior.
@@ -19,28 +19,27 @@ This project is built primarily for [Kiku](https://kiku.youyoumu.my.id/) and [La
AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the port in AnkiConnect's settings, update `ankiConnect.url` in your SubMiner config.
-## Auto-Enrichment Transport
+## Auto-enrichment transport
-When you add a word via Yomitan, SubMiner detects the new card and fills in the sentence, audio, image, and translation fields automatically. Two detection methods are available:
+When you add a word via Yomitan, SubMiner detects the new card and fills in the sentence, audio, and image fields automatically. Two detection methods are available:
-**Proxy mode** (default) - SubMiner runs a local _proxy_: a small middleman server that sits between Yomitan and Anki. Yomitan sends new cards to SubMiner, SubMiner enriches them, then passes them along to Anki. This makes enrichment instant.
+**Proxy mode** (default) - SubMiner runs a small local server between Yomitan and Anki. Yomitan sends the new card to SubMiner, SubMiner fills in the media fields, and the finished card goes on to Anki. There is no polling delay.
-**Polling mode** (fallback, when the proxy is disabled) - SubMiner asks AnkiConnect every few seconds whether any new cards were added, then enriches them. Simpler setup, but with a short delay (~3 seconds).
+**Polling mode** (fallback, when the proxy is disabled) - SubMiner asks AnkiConnect every few seconds whether new cards showed up, then fills them in. Less to configure, at the cost of roughly a 3 second delay.
-Use proxy mode if you want immediate enrichment. Use polling mode if your Yomitan instance is external (browser-based) or you prefer minimal configuration.
+Use proxy mode unless your Yomitan runs in a browser rather than the bundled instance, in which case polling is the simpler path.
In both modes, the enrichment workflow is the same:
1. Checks if a duplicate expression already exists (for field grouping).
2. Updates the sentence field with the current subtitle.
3. Generates and uploads audio and image media.
-4. Fills the translation field from the secondary subtitle or AI.
-5. Writes metadata to the miscInfo field.
+4. Writes metadata to the miscInfo field.
Polling mode uses the query `"deck:" added:1` to find recently added cards. If no deck is configured, it searches all decks (`added:1`). In Settings, the AnkiConnect deck dropdown auto-fills and persists Yomitan's current mining deck when available, then falls back to the decks reported by AnkiConnect; stats-dashboard mining also falls back to Yomitan's mining deck when `ankiConnect.deck` is empty.
Known-word sync scope is controlled by `ankiConnect.knownWords.decks`.
-### Proxy Mode Setup (Yomitan / Texthooker)
+### Proxy mode setup (Yomitan / texthooker)
```jsonc
"ankiConnect": {
@@ -83,7 +82,7 @@ In Yomitan, go to Settings → Profile and:
This is only for non-bundled, external/browser Yomitan or other clients. The bundled profile auto-update logic only targets the active profile when its server is blank or still default.
-### Proxy Troubleshooting (quick checks)
+### Proxy troubleshooting (quick checks)
If auto-enrichment appears to do nothing:
@@ -107,7 +106,7 @@ curl -sS http://127.0.0.1:8766 \
- Launcher log: `launcher-YYYY-MM-DD.log`
- mpv log: `mpv-YYYY-MM-DD.log`
-4. Ensure config JSONC is valid and logging shape is correct:
+4. Check that the config JSONC parses and the logging shape is right:
```jsonc
"logging": {
@@ -117,30 +116,31 @@ curl -sS http://127.0.0.1:8766 \
`"logging": "debug"` is invalid for current schema and can break reload/start behavior.
-## Field Mapping
+## Field mapping
SubMiner maps its data to your Anki note fields. Configure these under `ankiConnect.fields`:
```jsonc
"ankiConnect": {
"fields": {
- "word": "Expression", // mined word / expression text
- "audio": "ExpressionAudio", // audio clip from the video
- "image": "Picture", // screenshot or animated clip
- "sentence": "Sentence", // subtitle text
- "miscInfo": "MiscInfo", // metadata (filename, timestamp)
- "translation": "SelectionText" // secondary sub or AI translation
+ "word": "Expression", // mined word / expression text
+ "audio": "SentenceAudio", // sentence audio clip cut from the video
+ "image": "Picture", // screenshot or animated clip
+ "sentence": "Sentence", // subtitle text
+ "miscInfo": "MiscInfo" // metadata (filename, timestamp)
}
}
```
+`fields.audio` receives the **sentence** audio SubMiner cuts from the video, not word audio. Yomitan writes its own dictionary audio when you mine, so point this at a separate field such as `SentenceAudio` to keep the two apart. The built-in default is still `ExpressionAudio`, which collides with Yomitan on note types that use that field for word audio.
+
Field names are matched against your Anki note type case-insensitively (an exact match wins, then a lowercase comparison). If a configured field does not exist on the note type, SubMiner skips it without error.
These mappings always control normal word-card enrichment, including Yomitan proxy/polling updates and manual clipboard updates. Enabling Lapis or Kiku does not replace the configured word-card sentence and audio fields with `Sentence` and `SentenceAudio`. The dedicated sentence-card and audio-card shortcuts still use those Lapis/Kiku field names.
Two related options live alongside `fields`: `ankiConnect.deck` (target deck; empty falls back as described above) and `ankiConnect.tags` (tags added to mined cards, default `["SubMiner"]`; set `[]` to disable tagging). The `miscInfo` content is controlled by `ankiConnect.metadata.pattern` (default `[SubMiner] %f (%t)`; tokens: `%f` filename, `%F` filename with extension, `%t` timestamp, `%T` timestamp with milliseconds, ` ` newline).
-### Minimal Config
+### Minimal config
If you only want sentence and audio on your cards:
@@ -149,14 +149,14 @@ If you only want sentence and audio on your cards:
"enabled": true,
"fields": {
"sentence": "Sentence",
- "audio": "ExpressionAudio"
+ "audio": "SentenceAudio"
}
}
```
-## Media Generation
+## Media generation
-SubMiner uses FFmpeg to generate audio and image media from the video. FFmpeg must be installed and on `PATH`.
+SubMiner shells out to FFmpeg for audio clips and screenshots, so FFmpeg has to be installed and on `PATH`.
For remote streams such as Jellyfin playback, SubMiner downloads the clip's time window once into a temporary Matroska file (a stream copy, no re-encoding) and reads the timing review waveform, audio preview, audio, and image from that file instead of fetching the stream again for each step. The window covers the clip plus padding, plus the visible timeline in timing review, and grows when you reveal more of the timeline. It is deleted when a different window replaces it, after ten minutes without use, or when SubMiner exits. If the download fails, media generation reads the remote stream directly as before.
@@ -183,13 +183,27 @@ Output format: MP3 at 44100 Hz. If the video has multiple audio streams, SubMine
The audio is uploaded to Anki's media folder and inserted as `[sound:audio_.mp3]`.
-Set `media.reviewTiming` to `true` to pause playback and review each word, sentence, or audio card before its media is generated. The review opens with the subtitle range plus configured audio padding. Subtitles usually linger past the dialogue, so once the waveform loads an untouched clip end moves back to just after the line's last speech (plus the configured padding); the Line end rail keeps marking the subtitle timing, Reset restores it, and a line whose speech runs through its end is left alone. Drag either edge of the clip to trim it, drag the middle to slide it without changing its length, or press anywhere else on the waveform to snap the nearer edge there. A focused edge also moves with the arrow keys, by 100 ms alone or 500 ms with Shift, and the 100 ms buttons do the same. Space previews the selection with a playhead that sweeps the clip; the preview ends when the hidden player has actually played the last sample, so output latency such as Bluetooth headphones does not cut the clip short. Enter confirms, and Escape cancels. The Earlier and Later buttons reveal another two seconds of available timeline without moving the selected clip. A speech-weighted waveform shows the mined subtitle as a tinted band with labeled line-start and line-end rails, making adjacent dialogue easier to distinguish. SubMiner uses a center channel when one carries dialogue, then falls back to a mono mix, keeps only the 250 to 3500 Hz speech band, and draws each slice's loudness relative to the clip's own noise floor, so steady background music or ambience reads as a flat line while dialogue stands out. Waveform analysis failure leaves the timing controls available. The confirmed range is exact: SubMiner does not apply audio padding a second time. Static screenshots use its midpoint, and animated AVIF clips use the full confirmed range.
+Set `media.reviewTiming` to `true` to pause playback and check the clip before its media is generated. It applies to word, sentence, and audio cards.
-The review can also pull adjacent subtitle lines onto the card. Press `P` or `N` (or use the Prev and Next steppers above the sentence preview) to add the previous or next line, as many times as lines are available; Shift+`P` and Shift+`N` remove them again. The sentence preview lists every included line with the mined line highlighted, so the card's sentence field is always visible before you confirm, and the clip start or end, along with the line-start and line-end rails on the waveform, follows the outermost added line, keeping the review's audio padding. Confirming writes the combined lines to the sentence field; the Reset button drops the added lines along with any timing changes. Adjacent lines come from the parsed subtitle track when one is loaded; otherwise only lines that already played are offered, and a clip capped by `media.maxMediaDuration` keeps the full combined sentence even when the audio cannot cover every added line.
+The review opens on the subtitle range plus your configured audio padding. Subtitles usually hang around after the dialogue has stopped, so once the waveform loads, an untouched clip end pulls back to just after the last speech in the line. The Line end rail still marks the original subtitle timing, Reset puts it back, and a line whose speech runs right through its end is left alone.
-Canceling the review lets you keep editing, finish with the original timing, keep or create the card without audio or an image, or discard the card. Discard deletes an existing Yomitan or audio card and skips creation for a direct sentence card. Clipboard updates and stats-dashboard mining do not open timing review. Audio preview failure does not block confirmation or card creation. The option is disabled by default and hot-reloads. You can also toggle **Review Media Timing** for the current session from the runtime options palette (`Ctrl/Cmd+Shift+O`).
+**Adjusting the clip.** Drag either edge to trim, drag the middle to slide the whole clip without changing its length, or click anywhere on the waveform to snap the nearer edge there. A focused edge also moves with the arrow keys: 100 ms per press, or 500 ms with Shift. The 100 ms buttons do the same thing. Earlier and Later each reveal two more seconds of timeline without moving the selection.
-### Screenshots (Static)
+**Keys.** Space previews the selection with a playhead sweeping the clip. The preview ends when the hidden player has actually played the last sample, so Bluetooth output latency does not clip the tail. Enter confirms and Escape cancels.
+
+**The waveform.** SubMiner reads a center channel when one carries dialogue and falls back to a mono mix otherwise, keeps only the 250 to 3500 Hz speech band, and draws each slice's loudness against the clip's own noise floor. Steady background music flattens out and dialogue stands up, which makes it much easier to tell adjacent lines apart. The mined subtitle appears as a tinted band with labeled line-start and line-end rails. If waveform analysis fails, the timing controls still work.
+
+The range you confirm is used exactly as-is; SubMiner does not add audio padding a second time. Static screenshots take its midpoint, and animated AVIF clips cover the whole range.
+
+**Pulling in adjacent lines.** Press `P` or `N`, or use the Prev and Next steppers above the sentence preview, to add the previous or next subtitle line. Repeat for as many lines as exist. Shift+`P` and Shift+`N` remove them again. The sentence preview lists every included line with the mined one highlighted, so you always see the sentence field before confirming. The clip bounds and the waveform rails follow the outermost added line, keeping the review's audio padding.
+
+Confirming writes the combined lines to the sentence field. Reset drops the added lines along with any timing changes. Adjacent lines come from the parsed subtitle track when one is loaded; otherwise you only get lines that already played. A clip capped by `media.maxMediaDuration` still keeps the full combined sentence even when the audio cannot stretch to cover every added line.
+
+**Canceling.** You can go back to editing, finish with the original timing, create the card without audio or an image, or discard it. Discard deletes an existing Yomitan or audio card, and skips creation entirely for a direct sentence card. A failed audio preview does not block confirmation or card creation.
+
+Clipboard updates and stats-dashboard mining never open timing review. The option is off by default and hot-reloads. **Review Media Timing** in the runtime options palette (`Ctrl/Cmd+Shift+O`) toggles it for the current session.
+
+### Screenshots (static)
A single frame is captured at the current playback position.
@@ -206,9 +220,9 @@ A single frame is captured at the current playback position.
}
```
-### Animated Clips (AVIF)
+### Animated clips (AVIF)
-Instead of a static screenshot, SubMiner can generate an animated AVIF covering the subtitle duration.
+SubMiner can produce an animated AVIF spanning the subtitle duration instead of a still frame.
```jsonc
"ankiConnect": {
@@ -225,7 +239,7 @@ Instead of a static screenshot, SubMiner can generate an animated AVIF covering
Animated AVIF requires an AV1 encoder (`libaom-av1`, `libsvtav1`, or `librav1e`) in your FFmpeg build. Generation timeout is 60 seconds. `media.syncAnimatedImageToWordAudio` (default `true`) prepends a frozen first frame matching the existing word-audio duration, so the motion starts together with the sentence audio.
-### Behavior Options
+### Behavior options
```jsonc
"ankiConnect": {
@@ -246,40 +260,7 @@ When media is available, mined-card overlay and system notifications include the
`overwriteAudio` applies to automatic card updates and duplicate-card enrichment. Manual clipboard subtitle updates (`Ctrl/Cmd+C`, then `Ctrl/Cmd+V`) always replace generated sentence audio in `ankiConnect.fields.audio`, even when `overwriteAudio` is disabled.
-## AI Translation
-
-SubMiner can auto-translate the mined sentence and fill the translation field.
-Secondary subtitle text still wins when present. AI translation is only attempted when `ankiConnect.ai.enabled` is `true` and no secondary subtitle exists.
-
-```jsonc
-"ai": {
- "enabled": true,
- "apiKey": "sk-...",
- "apiKeyCommand": "",
- "baseUrl": "https://openrouter.ai/api",
- "requestTimeoutMs": 15000
-},
-"ankiConnect": {
- "ai": {
- "enabled": true,
- "model": "openai/gpt-4o-mini",
- "systemPrompt": "Translate mined sentence text only."
- }
-}
-```
-
-`ankiConnect.ai` controls feature-local enablement plus optional `model` / `systemPrompt` overrides.
-Provider credentials and request transport settings live in top-level `ai`.
-
-Translation priority:
-
-1. If a secondary subtitle is available, use it as the translation.
-2. If `ankiConnect.ai.enabled` is `true` and top-level `ai.enabled` is `true`, call the shared AI provider.
-3. If AI translation fails and no secondary subtitle exists, fall back to the original sentence text.
-
-The built-in translation request asks for English output by default. Customize that behavior through `ankiConnect.ai.systemPrompt`.
-
-## Sentence Cards (Lapis)
+## Sentence cards (Lapis)
SubMiner can create standalone sentence cards (without a word/expression) using a separate note type. This is designed for use with [Lapis](https://github.com/donkuri/Lapis) and similar sentence-focused note types.
@@ -302,7 +283,7 @@ The dedicated sentence-card and audio-card shortcuts use the Lapis/Kiku-compatib
To mine multiple subtitle lines as one sentence card, use `Ctrl/Cmd+Shift+S` followed by a digit (1–9) to select how many recent lines to combine.
-## Word Card Type (Kiku/Lapis)
+## Word card type (Kiku/Lapis)
Word cards get a card-type flag when SubMiner fills their sentence, whether that comes from Yomitan auto-enrichment, a manual clipboard update, or stats-dashboard word mining. By default the flag is `IsWordAndSentenceCard`; pick a different one with `ankiConnect.lapisKiku.wordCardKind`.
@@ -317,7 +298,7 @@ Word cards get a card-type flag when SubMiner fills their sentence, whether that
`click` marks `IsClickCard`, `sentence` marks `IsSentenceCard`, `audio` marks `IsAudioCard`, and `none` leaves the flags untouched for templates that manage them elsewhere. Whichever flag is chosen, the other card-type flags are cleared so the note never claims two card types. The setting is only read when `isKiku` or `isLapis` is enabled, and cards mined with Mine Sentence or Mine Audio keep their own flag.
-## Field Grouping (Kiku/Senren)
+## Field grouping (Kiku/Senren)
When you mine the same word multiple times, SubMiner can merge the cards instead of creating duplicates. This is designed for note types that support grouped fields: [Kiku](https://github.com/youyoumu/kiku) and [Senren](https://github.com/BrenoAqua/Senren) (which calls the feature scene switching).
@@ -351,7 +332,7 @@ For Senren note types, enable `isSenren` instead. Kiku and Senren write incompat
**Manual** (`"manual"`): A modal appears in the overlay showing both cards. You choose which card to keep, preview the merge result, then confirm. The modal has a 90-second timeout, after which it cancels automatically.
-### What Gets Merged
+### What gets merged
| Field | Merge behavior |
| -------- | ----------------------------------------------- |
@@ -364,7 +345,7 @@ Identical values from both cards are kept as separate grouped entries; the merge
The merge markup depends on the note type. Kiku entries are wrapped in `` spans ordered newest first. Senren entries follow the [scene switching](https://github.com/BrenoAqua/Senren/blob/main/docs/scene_switching.md) format: sentence, sentenceFurigana, and miscInfo entries use `group` spans when ordinal order is sufficient and numbered `groupN` spans when they need an absolute scene target. Audio and pictures are appended positionally, and the number of sentenceAudio entries drives Senren's scene count. Ungrouped legacy content is wrapped into a group span on first merge, and source `groupN` spans are rebased after the kept note's existing audio scenes.
-### Keyboard Shortcuts in the Modal
+### Keyboard shortcuts in the modal
| Key | Action |
| ----------- | ---------------------------------- |
@@ -373,7 +354,7 @@ The merge markup depends on the note type. Kiku entries are wrapped in `
-SubMiner is configured through a single file (`config.jsonc`). Most settings are also editable from the in-app **Settings** window - you rarely need to edit the file by hand. This page is the full reference: it explains the Settings window, where the config file lives, and documents every option grouped by topic. New to SubMiner? The Quick Start below plus the [Settings window](#settings) cover everything most users need.
+One file, `config.jsonc`, holds everything. Most of it is also editable from the in-app **Settings** window, so hand-editing is rarely necessary.
-## Quick Start
+This page is the full reference. It covers the Settings window, where the config file lives, and every option grouped by topic. If you are just starting out, the Quick Start below and the [Settings window](#settings) are enough.
-For most users, start with this minimal configuration:
+## Quick start
+
+Start here:
```json
{
@@ -35,11 +37,11 @@ For most users, start with this minimal configuration:
Use the known-word deck map to choose which Anki decks and note fields feed the known-word cache.
-Then customize as needed using the sections below.
+Everything else is optional; the sections below cover it.
## Settings
-SubMiner includes a dedicated **Settings** window accessible from the tray menu, the app `--settings` flag, or launcher commands such as `subminer --settings` and `subminer settings`. It is the primary way to configure SubMiner - all changes are written directly to `config.jsonc`, so manual file editing is not required for most users.
+Open the **Settings** window from the tray menu, the app's `--settings` flag, or `subminer settings`. It writes straight to `config.jsonc`, so anything you change there is a normal config edit you can inspect afterward.
The Settings window groups options by workflow instead of mirroring the raw config-file shape:
@@ -57,11 +59,11 @@ Each field still writes to its current `config.jsonc` path. For example, subtitl
The Settings window preserves existing JSONC comments, trailing commas, and unrelated keys. Resetting a field removes the explicit config path so the built-in default applies.
-Secret fields do not display stored values. They show whether a value is configured; entering a new value writes it, and reset clears the explicit path. Prefer command-based secret options such as `ai.apiKeyCommand` when available.
+Secret fields do not display stored values. They show whether a value is configured; entering a new value writes it, and reset clears the explicit path. Prefer command-based secret options such as `jimaku.apiKeyCommand` when available.
Saving validates the candidate config before writing. Live-reloadable changes are applied immediately; other changes return a restart-required banner in the window.
-## Configuration File
+## Configuration file
The Settings window writes to `config.jsonc` directly, so most users do not need to edit the file by hand. The config file and the option reference below are provided for advanced use, scripting, or cases where you prefer editing config directly.
@@ -95,7 +97,7 @@ For valid JSON/JSONC with invalid option values, SubMiner uses warn-and-fallback
On macOS, these validation warnings also open a native dialog with full details (desktop notification banners can truncate long messages).
-### Hot-Reload Behavior
+### Hot-reload behavior
SubMiner watches the active config file (`config.jsonc` or `config.json`) while running and applies supported updates automatically.
@@ -103,7 +105,7 @@ Hot-reloadable settings include subtitle appearance, sidebar controls, keybindin
shortcuts, notifications, logging level, selected source-language preferences,
Jimaku/Subsync settings, AniSkip settings (`mpv.aniskipEnabled`, `mpv.aniskipButtonKey`),
stats keys (`stats.toggleKey`, `stats.markWatchedKey`), the secondary-subtitle default
-mode, and the Anki deck, known-word, N+1, field, sentence-card, AI, and Kiku options
+mode, and the Anki deck, known-word, N+1, field, sentence-card, and Kiku options
listed in the reference tables below.
When these values change, SubMiner applies them live. Invalid config edits are rejected and the previous valid runtime config remains active.
@@ -111,11 +113,10 @@ When these values change, SubMiner applies them live. Invalid config edits are r
Restart-required changes:
- Any other config sections still require restart.
-- Shared top-level `ai` provider settings still require restart.
- AnkiConnect transport/proxy/media/tag fields still require restart unless listed above.
- SubMiner shows an on-screen/system notification listing restart-required sections when they change.
-### Configuration Options Overview
+### Configuration options Overview
The configuration file includes several main sections:
@@ -146,7 +147,6 @@ The configuration file includes several main sections:
**Anki Integration**
-- [**Shared AI Provider**](#shared-ai-provider) - Canonical OpenAI-compatible provider config shared by Anki and YouTube subtitle fixing
- [**AnkiConnect**](#ankiconnect) - Automatic Anki card creation with media
- [**Kiku/Lapis Integration**](#kiku-lapis-integration) - Sentence cards and duplicate handling for Kiku/Lapis/Senren note types
- [**N+1 Word Highlighting**](#n-1-word-highlighting) - Known-word cache and single-target highlighting
@@ -168,7 +168,7 @@ The configuration file includes several main sections:
- [**Updates**](#updates) - Automatic update checks, notifications, and prerelease testing
- [**Notifications**](#notifications) - Overlay notification placement
-## Core Settings
+## Core settings
### Logging
@@ -243,13 +243,13 @@ Configure where overlay notification cards appear:
#### Notification history panel
-Every overlay notification shown during a session is also recorded in a notification history panel. Press `Ctrl/Cmd+N` (configurable via [`shortcuts.toggleNotificationHistory`](#shortcuts-configuration)) to toggle the panel; the binding works whether the overlay or mpv has focus. The panel slides in from the same edge the notifications use — left when `overlayPosition` is `"top-left"`, and right for `"top-right"` or `"top"` (centered). Character dictionary sync uses one live card but records each distinct phase in history. Each entry can be removed individually, or use **Clear** to empty the history. History is session-only and is not persisted across restarts.
+Every overlay notification shown during a session is also recorded in a notification history panel. Press `Ctrl/Cmd+N` (configurable via [`shortcuts.toggleNotificationHistory`](#shortcuts-configuration)) to toggle the panel; the binding works whether the overlay or mpv has focus. The panel slides in from the same edge the notifications use, so left when `overlayPosition` is `"top-left"` and right for `"top-right"` or `"top"` (centered). Character dictionary sync uses one live card but records each distinct phase in history. Each entry can be removed individually, or use **Clear** to empty the history. History is session-only and is not persisted across restarts.
Startup tokenization, subtitle annotation, and character dictionary status follow the configured notification surface. When the surface is `"overlay"` or `"both"`, SubMiner queues those startup notifications until the overlay renderer is ready instead of falling back to mpv OSD. If loading and ready states both finish before the overlay can paint, the loading card is delivered first and then updates to ready shortly after. With `"both"`, character dictionary checking/building/importing/ready status also goes to system notifications; building and importing are only emitted when that work is actually needed. The bundled mpv plugin only shows its startup OSD messages when `ankiConnect.behavior.notificationType` is set to `"osd"` or `"osd-system"` in `config.jsonc`; AniSkip prompts and skip result messages are playback feedback and still route to overlay notifications when configured.
The equivalent direct CLI command is `--playback-feedback ` (`playbackFeedback` internally). It sends that one non-empty feedback string through the same route controlled by `ankiConnect.behavior.notificationType`; it does not change the saved config.
-### Auto-Start Overlay
+### Auto-start overlay
Control whether the overlay automatically becomes visible when it connects to mpv:
@@ -267,7 +267,7 @@ When you launch through the SubMiner app or the `subminer` wrapper, the launcher
On Windows, packaged plugin installs also rewrite the plugin socket path to `\\.\pipe\subminer-socket`.
-### Startup Warmups
+### Startup warmups
Control which startup warmups run in the background versus deferring to first real usage:
@@ -293,7 +293,7 @@ Control which startup warmups run in the background versus deferring to first re
Defaults warm local tokenizer/dictionary work (`true` for `mecab`, `yomitanExtension`, and `subtitleDictionaries`) with `lowPowerMode: false`; Jellyfin remote session warmup is opt-in (`false` by default). Setting a warmup toggle to `false` defers that work until first usage.
-### WebSocket Server
+### WebSocket server
The overlay includes a built-in WebSocket server that broadcasts plain subtitle text to connected clients for external processing.
@@ -357,9 +357,9 @@ See `config.example.jsonc` for detailed configuration options.
| `launchAtStartup` | `true`, `false` | Start texthooker automatically with SubMiner startup (default: `false`) |
| `openBrowser` | `true`, `false` | Open browser tab when texthooker starts (default: `false`) |
-## Subtitle Display
+## Subtitle display
-### Subtitle Style
+### Subtitle style
Customize the appearance of primary and secondary subtitles:
@@ -457,7 +457,7 @@ Secondary subtitle styling lives in the secondary subtitle CSS object. Any CSS p
**See `config.example.jsonc`** for the complete list of subtitle style configuration options.
-### Subtitle Sidebar
+### Subtitle sidebar
Configure the parsed-subtitle sidebar modal.
@@ -519,7 +519,7 @@ For full details on layout modes, behavior, and the keyboard shortcut, see the [
| `N4` | `#8bd5ca` | JLPT N4 underline color |
| `N5` | `#8aadf4` | JLPT N5 underline color |
-### Subtitle Position
+### Subtitle position
Set the initial vertical subtitle position (measured from the bottom of the screen):
@@ -537,7 +537,7 @@ Set the initial vertical subtitle position (measured from the bottom of the scre
In the overlay, you can fine-tune subtitle position at runtime with `Right-click + drag` on subtitle text.
-### Secondary Subtitles
+### Secondary subtitles
Display a second subtitle track (e.g., English alongside Japanese) in the overlay:
@@ -563,8 +563,6 @@ Secondary subtitles do **not** auto-load by default. To turn them on for local a
These two settings apply to local and Jellyfin playback only. YouTube secondary selection is fixed to English and ignores them; see [YouTube Integration](/youtube-integration#secondary-subtitle-languages). `defaultMode` still controls how the loaded secondary bar is displayed in every case.
-Because the mined-card translation field is filled from the secondary subtitle when one is present, leaving `autoLoadSecondarySub` off means local-file cards fall back to AI translation (when configured) or the original sentence text.
-
The secondary-subtitle language list also acts as the fallback secondary-language priority for managed startup subtitle selection on local playback and YouTube playback.
**Display modes:**
@@ -575,7 +573,7 @@ The secondary-subtitle language list also acts as the fallback secondary-languag
**See `config.example.jsonc`** for additional secondary subtitle configuration options.
-## Keyboard & Controls
+## Keyboard and controls
### Keybindings
@@ -641,7 +639,7 @@ Subtitle delay commands (`sub-delay`, `sub-step`) show a native mpv OSD notifica
**See `config.example.jsonc`** for more keybinding examples and configuration options.
-### Shortcuts Configuration
+### Shortcuts configuration
Customize or disable the overlay keyboard shortcuts:
@@ -702,7 +700,7 @@ Set any shortcut to `null` to disable it.
Feature-dependent shortcuts/keybindings only run when their related integration is enabled. For example, Anki/Kiku shortcuts require `ankiConnect.enabled` (and Kiku-specific behavior where applicable), and Jellyfin remote startup behavior requires Jellyfin to be enabled.
-### Controller Support
+### Controller support
SubMiner can read controllers through the Chrome Gamepad API and map them onto the existing keyboard-only overlay workflow.
@@ -818,7 +816,7 @@ If you update this controller documentation or the generated controller examples
Tune `scrollPixelsPerSecond`, `horizontalJumpPixels`, deadzones, repeat timing, and profile `buttonIndices` to match your controller. See [config.example.jsonc](/config.example.jsonc) for the full generated comments for every controller field.
-### Manual Card Update Shortcuts
+### Manual card update shortcuts
When automatic card updates are disabled, new cards are detected but not automatically updated. Use these keyboard shortcuts for manual control:
@@ -845,7 +843,7 @@ When automatic card updates are disabled, new cards are detected but not automat
These shortcuts are only active when the overlay window is visible and automatically disabled when hidden.
-### Session Help Modal
+### Session help modal
The session help modal opens from the overlay with `Ctrl/Cmd+/` by default. The mpv plugin also exposes it through the `y-h` chord. It shows the current session keybindings and color legend.
@@ -869,7 +867,7 @@ The list is generated at runtime from:
When config hot-reload updates shortcut/keybinding/style values, close and reopen the help modal to refresh the displayed entries.
-### Runtime Option Palette
+### 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.
@@ -889,39 +887,7 @@ Palette controls:
- `Enter`: apply selected value
- `Esc`: close
-## Anki Integration
-
-### Shared AI Provider
-
-This is the single, shared connection to an OpenAI-compatible LLM endpoint. Configure it **once** here at the top level, and SubMiner reuses it wherever AI is needed (Anki translation/enrichment and YouTube subtitle fixing). Per-feature toggles and prompt/model tweaks live in their own sections (for example `ankiConnect.ai` and `youtubeSubgen.ai`) and inherit this transport.
-
-```json
-{
- "ai": {
- "enabled": false,
- "apiKey": "",
- "apiKeyCommand": "",
- "model": "openai/gpt-4o-mini",
- "baseUrl": "https://openrouter.ai/api",
- "requestTimeoutMs": 15000
- }
-}
-```
-
-| Option | Values | Description |
-| ------------------ | -------------------- | ------------------------------------------------------------------------------------ |
-| `ai.enabled` | `true`, `false` | Enable shared AI provider features (default: `false`) |
-| `apiKey` | string | Static API key for the shared provider |
-| `apiKeyCommand` | string | Shell command used to resolve the API key (preferred over a plaintext `apiKey`) |
-| `model` | string | Default model identifier requested from the provider (default: `openai/gpt-4o-mini`) |
-| `baseUrl` | string (URL) | OpenAI-compatible base URL (default: `https://openrouter.ai/api`) |
-| `systemPrompt` | string | Default system prompt sent with requests (default: a translation-engine prompt) |
-| `requestTimeoutMs` | integer milliseconds | Shared request timeout (default: `15000`) |
-
-SubMiner uses the shared provider for:
-
-- Anki translation/enrichment when Anki AI is enabled
-- YouTube generated-subtitle fixing when `youtubeSubgen.fixWithAi` is enabled (with optional `youtubeSubgen.ai.model` / `systemPrompt` overrides)
+## Anki integration
### AnkiConnect
@@ -943,16 +909,10 @@ Enable automatic Anki card creation and updates with media generation:
"deck": "Learning::Japanese",
"fields": {
"word": "Expression",
- "audio": "ExpressionAudio",
+ "audio": "SentenceAudio",
"image": "Picture",
"sentence": "Sentence",
- "miscInfo": "MiscInfo",
- "translation": "SelectionText"
- },
- "ai": {
- "enabled": false,
- "model": "",
- "systemPrompt": ""
+ "miscInfo": "MiscInfo"
},
"media": {
"generateAudio": true,
@@ -1010,14 +970,10 @@ This example is intentionally compact. The option table below documents availabl
| `tags` | array of strings | Tags automatically added to cards mined/updated by SubMiner (default: `['SubMiner']`; set `[]` to disable automatic tagging). |
| `ankiConnect.deck` | string | Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to use the Yomitan mining deck when available. In Settings, this dropdown auto-fills and persists Yomitan's current mining deck when available. |
| `fields.word` | string | Card field for mined word / expression text (default: `Expression`) |
-| `fields.audio` | string | Card field for audio files (default: `ExpressionAudio`) |
+| `fields.audio` | string | Card field for the generated sentence audio clip (default: `ExpressionAudio`). Set this to a dedicated field such as `SentenceAudio` so it does not collide with the word audio Yomitan writes. |
| `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`) |
-| `ankiConnect.ai.enabled` | `true`, `false` | Use AI translation for sentence cards. Also auto-attempted when secondary subtitle is missing. |
-| `ankiConnect.ai.model` | string | Optional model override for Anki AI translation/enrichment flows. |
-| `ankiConnect.ai.systemPrompt` | string | Optional system prompt override for Anki AI translation/enrichment flows. |
| `media.generateAudio` | `true`, `false` | Generate audio clips from video (default: `true`) |
| `media.normalizeAudio` | `true`, `false` | Normalize generated sentence-audio loudness during media extraction (default: `true`). Set to `false` to keep raw source loudness. Changes apply live. |
| `media.mirrorMpvVolume` | `true`, `false` | Apply mpv's cubic software-volume curve to each generated sentence-audio clip (default: `true`). This ignores mpv's separate mute state, falls back to unity scaling if volume cannot be read, and applies changes live. |
@@ -1056,10 +1012,7 @@ This example is intentionally compact. The option table below documents availabl
| `isKiku` | object | Kiku-only config: `{ enabled, fieldGrouping, deleteDuplicateInAuto }` (shared sentence/audio/model settings are inherited from `isLapis`) |
| `isSenren` | object | Senren-only config: `{ enabled, fieldGrouping, deleteDuplicateInAuto }`. Merges duplicates using Senren's scene-switching markup. Mutually exclusive with `isKiku.enabled`. |
-`ankiConnect.ai` only controls feature-local enablement plus optional `model` / `systemPrompt` overrides.
-API key resolution, base URL, and timeout live under the shared top-level [`ai`](#shared-ai-provider) config.
-
-### Kiku/Lapis Integration
+### Kiku/Lapis integration
SubMiner is intentionally built for [Kiku](https://kiku.youyoumu.my.id/) and [Lapis](https://github.com/donkuri/lapis) workflows, with note-type-specific behavior built into Anki settings.
@@ -1087,7 +1040,7 @@ SubMiner is intentionally built for [Kiku](https://kiku.youyoumu.my.id/) and [La
- For [Senren](https://github.com/BrenoAqua/Senren) note types, enable `isSenren` instead of `isKiku`. Duplicate merges then use Senren's scene-switching markup (including grouped `miscInfo` entries), and `isSenren.fieldGrouping` supports the same three modes (default: `auto`). Kiku and Senren are mutually exclusive; if both are enabled, Kiku wins and Senren is turned off with a config warning.
- `lapisKiku.wordCardKind` picks the card-type flag set on word cards; see [Word Card Type](#word-card-type). It is read only while `isLapis` or `isKiku` is enabled.
-### Word Card Type
+### Word card type
When SubMiner fills the sentence on a mined word card - from Yomitan auto-enrichment, a manual clipboard update, or stats-dashboard word mining - it marks which card that note should generate. `ankiConnect.lapisKiku.wordCardKind` chooses the flag:
@@ -1101,7 +1054,7 @@ When SubMiner fills the sentence on a mined word card - from Yomitan auto-enrich
The other card-type flags are cleared so a note never claims two card types at once. Notes are skipped when the note type has no field for the chosen flag, and when the note was already mined as a sentence or audio card. Cards created by Mine Sentence and Mine Audio keep their own flag regardless of this setting.
-### N+1 Word Highlighting
+### N+1 word highlighting
When known-word highlighting is enabled, SubMiner builds a local cache of known words from Anki to highlight already learned tokens in subtitle rendering.
@@ -1138,7 +1091,7 @@ To refresh roughly once per day, set:
}
```
-### Field Grouping Modes
+### Field grouping modes
| Mode | Behavior |
| ---------- | -------------------------------------------------------------------------------------------------------------------------- |
@@ -1157,7 +1110,7 @@ When the manual merge popup opens, SubMiner pauses playback and closes any open
Open demo in a new tab
-## External Integrations
+## External integrations
### Jimaku
@@ -1199,7 +1152,7 @@ The keyboard shortcut lives under `shortcuts.openTsukihime` (default `Ctrl+Shift
See [TsukiHime Integration](/tsukihime-integration) for the modal workflow, language tabs, and troubleshooting.
-### Subtitle Sync
+### Subtitle sync
Sync a subtitle track from the overlay picker using `alass` or `ffsubsync`. The picker lets you choose which track gets retimed (the active primary track by default) and, for alass, which reference it is aligned against (the secondary subtitle track by default). Both are **optional external tools** that must be installed separately and available on your `PATH` (or configured via the path options below).
@@ -1224,8 +1177,6 @@ Sync a subtitle track from the overlay picker using `alass` or `ffsubsync`. The
| `ffmpeg_path` | string path | Path to `ffmpeg` (used for internal subtitle extraction). Empty or `null` falls back to `/usr/bin/ffmpeg`. |
| `replace` | `true`, `false` | When `true` (default), overwrite the active subtitle file on successful sync. When `false`, write `_retimed.`. |
-Stats dashboard sentence mining also uses `alass_path` when available to align a local English sidecar against the local Japanese sidecar before filling the card translation field. This stats-only retime writes a temporary cached copy and never edits the original subtitle files.
-
Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`.
Customize it there, or set it to `null` to disable.
@@ -1389,7 +1340,7 @@ Jellyfin playback auto-launched through SubMiner loads the mpv plugin the same w
When Jellyfin is enabled with a server URL and SubMiner is running, the tray menu also shows a `Jellyfin Discovery` checkbox. It starts or stops discovery for the current runtime session only and does not write config. Starting discovery still requires a valid stored or environment-provided Jellyfin auth session.
-### Discord Rich Presence
+### Discord rich presence
Discord Rich Presence is enabled by default. SubMiner publishes a polished activity card that reflects current media title, playback state, and session timer unless you turn it off.
@@ -1436,7 +1387,7 @@ Troubleshooting:
- If images do not render, confirm asset keys exactly match uploaded Discord asset names.
- If Discord is closed/not installed/disconnects, SubMiner continues running and quietly skips presence updates.
-### Immersion Tracking
+### Immersion tracking
Enable or disable local immersion analytics stored in SQLite for mined subtitles and media sessions. This data also powers the stats dashboard:
@@ -1510,7 +1461,7 @@ Set `dbPath` only if you want to relocate the database (for backup, syncing, or
See [Immersion Tracking Storage](/immersion-tracking) for schema details, query templates, dashboard access, retention/rollup behavior, backend portability notes, and the dedicated SQLite verification command.
-### Stats Dashboard
+### Stats dashboard
Configure the local stats UI served from SubMiner and the in-app stats overlay toggle:
@@ -1541,7 +1492,7 @@ Usage notes:
- The dashboard reads from the same immersion-tracking database, so keep `immersionTracking.enabled` on if you want data to appear.
- The UI includes Overview, Library, Trends, Vocabulary, Search, and Sessions tabs.
-### MPV Launcher
+### MPV launcher
Configure the mpv executable, profile, and window state for SubMiner-managed mpv launches (launcher playback, Windows `--launch-mpv`, and Jellyfin idle mpv startup):
@@ -1583,7 +1534,7 @@ Launch mode behavior:
- **`maximized`** - mpv starts maximized via `--window-maximized=yes`, keeping taskbar access.
- **`fullscreen`** - mpv starts in true fullscreen via `--fullscreen`.
-### YouTube Playback Settings
+### YouTube playback settings
Set defaults used by managed subtitle auto-selection and the `subminer` launcher YouTube flow:
@@ -1627,6 +1578,6 @@ Track selection:
These settings come from `config.jsonc` (or built-in defaults); there are no CLI flags or environment variables for subtitle language selection.
-#### YouTube Subtitle Generation (`youtubeSubgen`)
+#### YouTube subtitle generation (`youtubeSubgen`)
-An advanced, template-hidden section for Whisper-based YouTube subtitle generation: `whisperBin`, `whisperModel`, `whisperVadModel`, `whisperThreads` (default `4`), and `fixWithAi` (default `false`), which post-processes generated subtitles through the [Shared AI Provider](#shared-ai-provider) with optional `youtubeSubgen.ai.model` / `systemPrompt` overrides. These keys are accepted in `config.jsonc` but intentionally omitted from the generated template.
+An advanced, template-hidden section for Whisper-based YouTube subtitle generation: `whisperBin`, `whisperModel`, `whisperVadModel`, and `whisperThreads` (default `4`). These keys are accepted in `config.jsonc` but the generated template omits them.
diff --git a/docs-site/demos.md b/docs-site/demos.md
index e0097d4e..c947fb40 100644
--- a/docs-site/demos.md
+++ b/docs-site/demos.md
@@ -1,6 +1,8 @@
-# Feature Demos
+# Feature demos
-Short recordings of SubMiner's key features and integrations from real playback sessions. A few terms you'll see below: _Yomitan_ is the pop-up dictionary used for word lookups, _Jimaku_ is a community subtitle database, _alass_ and _ffsubsync_ are tools that retime subtitles to match the audio, _Jellyfin_ is a self-hosted media server, and a _texthooker_ is a web page that mirrors the current subtitle as selectable text for browser-based tools.
+Short recordings from real playback sessions.
+
+Some vocabulary for what follows. _Yomitan_ is the pop-up dictionary. _Jimaku_ is a community subtitle database. _alass_ and _ffsubsync_ retime subtitles against the audio. _Jellyfin_ is a self-hosted media server. A _texthooker_ is a web page that mirrors the current subtitle as selectable text so browser tools can read it.
-## Anki Card Mining & Enrichment
+## Anki card mining and enrichment
-Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner automatically attaches the sentence, a timing-accurate audio clip, a screenshot, and a translation.
+Mine a card from Yomitan or straight from a subtitle line. SubMiner attaches the sentence, an audio clip cut to the line timing, and a screenshot.
-## Subtitle Download & Sync
+## Subtitle download and sync
-Search and download subtitles from Jimaku, then retime them with alass or ffsubsync - all from within SubMiner.
+Search Jimaku, download a track, then retime it with alass or ffsubsync without leaving SubMiner.
B
B --> C
C --> D
- D -- startup --> E
- D -- user request --> F
+ D - startup --> E
+ D - user request --> F
E --> G
F --> G
G --> H
@@ -52,7 +52,7 @@ flowchart TD
K --> L
```
-## Auto-Load Flow
+## Auto-load flow
On startup with a YouTube URL:
@@ -66,7 +66,7 @@ On startup with a YouTube URL:
6. Missing tracks are downloaded to a temp directory and loaded via `sub-add`.
7. Playback unpauses once the primary subtitle is ready.
-## Manual Subtitle Picker
+## Manual subtitle picker
Press **Ctrl+Alt+C** during YouTube playback to open the subtitle picker overlay. This lets you:
@@ -80,27 +80,27 @@ card to a success notification after the selected tracks load.
The picker displays each track with its language, kind (manual/auto), and title when available.
-## Subtitle Format Handling
+## Subtitle format handling
SubMiner handles several YouTube subtitle formats transparently:
| Format | Handling |
| ---------------------- | -------------------------------------------------------- |
| `srt`, `vtt` | Used directly (preferred for manual tracks) |
-| `srv1`, `srv2`, `srv3` | YouTube TimedText XML --- converted to VTT automatically |
+| `srv1`, `srv2`, `srv3` | YouTube TimedText XML - converted to VTT automatically |
| Auto-generated VTT | Normalized to remove rolling-caption text duplication |
For auto-generated tracks, SubMiner prefers `srv3` > `srv2` > `srv1` > `vtt` (TimedText XML produces cleaner output). For manual tracks, `srt` > `vtt` is preferred.
-## Card Media Cache
+## Card media cache
By default, YouTube card audio and screenshots are extracted directly from mpv's active stream URLs. If generated card media fails with YouTube `403` errors, set `youtube.mediaCache.mode` to `"background"`. Background mode starts a separate `yt-dlp` media download after playback loads, including YouTube URLs opened directly in mpv and resolved stream URLs when mpv still exposes the original YouTube playlist entry. It creates text fields immediately, queues audio/image work for mined notes, and fills those fields once the local cache file is ready.
Background cache downloads are capped at 720p by default (`youtube.mediaCache.maxHeight`; set `0` for unlimited) and use IPv4 and retry flags to reduce YouTube throttling failures. If the background download still fails, SubMiner shows a cache failure notification, shows queued-card failure notifications, and clears those pending updates so cards are not left waiting silently.
-## Configuration Reference
+## Configuration reference
-### Primary Subtitle Languages
+### Primary subtitle languages
```jsonc
{
@@ -114,11 +114,11 @@ Background cache downloads are capped at 720p by default (`youtube.mediaCache.ma
| --------------------- | ---------- | ------------------------------------------------------------------------------------- |
| `primarySubLanguages` | `string[]` | Languages that count as a satisfactory primary subtitle (default `["ja", "jpn"]`). Used by the "primary subtitle missing" notification and by managed local/playlist subtitle selection. |
-YouTube auto-selection itself always picks a Japanese track first (manual over auto), then falls back to any manual track — `primarySubLanguages` does not change which YouTube track is auto-picked.
+YouTube auto-selection itself always picks a Japanese track first (manual over auto), then falls back to any manual track. `primarySubLanguages` does not change which YouTube track is auto-picked.
-### Secondary Subtitle Languages
+### Secondary subtitle languages
-YouTube secondary selection is fixed: SubMiner always tries an English track (manual over auto) and loads it when found. The shared `secondarySub` config does not change YouTube track selection — `secondarySubLanguages` and `autoLoadSecondarySub` apply only to local/Jellyfin sidecar selection — but `defaultMode` still controls how the loaded secondary bar is displayed:
+YouTube secondary selection is fixed: SubMiner always tries an English track (manual over auto) and loads it when found. The shared `secondarySub` config does not change YouTube track selection. `secondarySubLanguages` and `autoLoadSecondarySub` apply only to local and Jellyfin sidecar selection. `defaultMode` still controls how the loaded secondary bar is displayed:
```jsonc
{
@@ -138,10 +138,10 @@ YouTube secondary selection is fixed: SubMiner always tries an English track (ma
These settings come from `config.jsonc` (or built-in defaults); there are no CLI flags or environment variables for subtitle language selection.
-## Limitations and Troubleshooting
+## Limitations and troubleshooting
- **No subtitles found**: The video may not have Japanese subtitles. Open the picker with `Ctrl+Alt+C` to see all available tracks.
-- **yt-dlp not found**: Install `yt-dlp` and ensure it is on `PATH`, or set `SUBMINER_YTDLP_BIN` to the binary path.
+- **yt-dlp not found**: Install `yt-dlp` and put it on `PATH`, or set `SUBMINER_YTDLP_BIN` to the binary path.
- **Probe timeout**: `yt-dlp` has a 15-second timeout per operation. Slow connections or rate-limited IPs may hit this. Retry or update `yt-dlp`.
- **Card media `403` errors**: Switch `youtube.mediaCache.mode` from `"direct"` to `"background"` so card media is generated from a local `yt-dlp` cache instead of ffmpeg reading an expiring YouTube stream URL.
- **Auto-caption quality**: YouTube auto-generated captions vary in quality. Manual subtitles (when available) are always preferred.
@@ -149,10 +149,10 @@ These settings come from `config.jsonc` (or built-in defaults); there are no CLI
- **Secondary subtitle fails**: Secondary track failures never block playback. The primary subtitle loads independently.
- **Native mpv secondary rendering**: Stays hidden during YouTube flows so the SubMiner overlay remains the visible secondary subtitle surface.
-## Related Pages
+## Related pages
-- [Usage --- YouTube Playback](/usage#youtube-playback)
-- [Configuration --- YouTube Playback Settings](/configuration#youtube-playback-settings)
-- [Configuration --- Secondary Subtitles](/configuration#secondary-subtitles)
+- [Usage - YouTube Playback](/usage#youtube-playback)
+- [Configuration - YouTube Playback Settings](/configuration#youtube-playback-settings)
+- [Configuration - Secondary Subtitles](/configuration#secondary-subtitles)
- [Keyboard Shortcuts](/shortcuts)
- [Jellyfin Integration](/jellyfin-integration)