feat(stats): add v1 immersion stats dashboard (#19)
@@ -2,6 +2,8 @@
|
||||
|
||||
In-repo VitePress documentation source for SubMiner.
|
||||
|
||||
Internal architecture/workflow source of truth lives in `docs/README.md` at the repo root. Keep `docs-site/` user-facing.
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
|
||||
@@ -114,6 +114,7 @@ SubMiner maps its data to your Anki note fields. Configure these under `ankiConn
|
||||
```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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Architecture
|
||||
|
||||
This page is a contributor-facing architecture summary. Canonical internal architecture guidance lives in `docs/architecture/README.md` at the repo root.
|
||||
|
||||
SubMiner is split into three cooperating runtimes:
|
||||
|
||||
- Electron desktop app (`src/`) for overlay/UI/runtime orchestration.
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## v0.7.0 (2026-03-19)
|
||||
- Added a full local immersion dashboard release line with Overview, Library, Trends, Vocabulary, and Sessions drill-down views backed by SQLite tracking data.
|
||||
- Added browser-first stats workflows: `subminer stats`, background stats daemon controls (`-b` / `-s`), stats cleanup, and dashboard-side mining actions with media enrichment.
|
||||
- Improved stats accuracy and scale handling with Yomitan token counts, full session timelines, known-word timeline fixes, cross-media vocabulary fixes, and clearer session charts.
|
||||
- Improved overlay/runtime stability with quieter macOS fullscreen recovery, reduced repeated loading OSD popups, and better frequency/noise handling for subtitle annotations.
|
||||
- Added launcher mpv-args passthrough plus Linux plugin wrapper-name fallback for packaged installs.
|
||||
- Added a hover-revealed ↗ button on Sessions tab rows to navigate directly to the anime media-detail view, with correct "Back to Sessions" back-navigation.
|
||||
- Excluded auxiliary-stem `そうだ` grammar tails (MeCab POS3 `助動詞語幹`) from subtitle annotation metadata so frequency, JLPT, and N+1 styling no longer bleed onto grammar-tail tokens.
|
||||
|
||||
## v0.6.5 (2026-03-15)
|
||||
- Seeded the AUR checkout with the repo `.SRCINFO` template before rewriting metadata so tagged releases do not depend on prior AUR state.
|
||||
|
||||
|
||||
@@ -2,7 +2,24 @@
|
||||
|
||||
SubMiner can build a Yomitan-compatible character dictionary from AniList metadata so that character names in subtitles are recognized, highlighted, and enrichable with context — portraits, roles, voice actors, and biographical detail — without leaving the overlay.
|
||||
|
||||
The dictionary is generated per-media, merged across your recently-watched titles, and auto-imported into Yomitan. When a character name appears in a subtitle line, it gets highlighted and becomes clickable for a full profile lookup.
|
||||
The dictionary is generated per-media, merged across your recently-watched titles, and auto-imported into Yomitan. When a character name appears in a subtitle line, it gets highlighted and becomes available for hover-driven Yomitan profile lookup.
|
||||
|
||||
## Stats Dashboard
|
||||
|
||||
The character dictionary and stats dashboard both read from the same local immersion data.
|
||||
|
||||
- Open the dashboard from overlay: press your configured `stats.toggleKey` (default: `` ` `` / `Backquote`).
|
||||
- Open from launcher/CLI: run `subminer stats`.
|
||||
- Open directly: visit `http://127.0.0.1:<stats.serverPort>` when the local server is running.
|
||||
|
||||
Useful config keys:
|
||||
|
||||
- `stats.autoStartServer` — start the local stats server automatically once immersion tracking starts.
|
||||
- `stats.serverPort` — local HTTP port for dashboard and API.
|
||||
- `stats.toggleKey` — key binding for overlay dashboard toggle.
|
||||
- `stats.autoOpenBrowser` — auto-open dashboard browser for `subminer stats`.
|
||||
|
||||
The dashboard gives quick visibility into episode summaries, watch-time rollups, session timelines, and vocabulary/kanji drill-down from the same DB used by character matching.
|
||||
|
||||
## How It Works
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ The configuration file includes several main sections:
|
||||
- [**Jellyfin**](#jellyfin) - Optional Jellyfin auth, library listing, and playback launch
|
||||
- [**Discord Rich Presence**](#discord-rich-presence) - Optional Discord activity card updates
|
||||
- [**Immersion Tracking**](#immersion-tracking) - Track subtitle sessions and mining activity in SQLite
|
||||
- [**Stats Dashboard**](#stats-dashboard) - Local dashboard and overlay for immersion progress
|
||||
- [**YouTube Subtitle Generation**](#youtube-subtitle-generation) - Launcher defaults for yt-dlp + local whisper fallback
|
||||
|
||||
## Core Settings
|
||||
@@ -664,10 +665,10 @@ Use the runtime options palette to toggle settings live while SubMiner is runnin
|
||||
Current runtime options:
|
||||
|
||||
- `ankiConnect.behavior.autoUpdateNewCards` (`On` / `Off`)
|
||||
- `ankiConnect.nPlusOne.highlightEnabled` (`On` / `Off`)
|
||||
- `ankiConnect.knownWords.highlightEnabled` (`On` / `Off`)
|
||||
- `subtitleStyle.enableJlpt` (`On` / `Off`)
|
||||
- `subtitleStyle.frequencyDictionary.enabled` (`On` / `Off`)
|
||||
- `ankiConnect.nPlusOne.matchMode` (`headword` / `surface`)
|
||||
- `ankiConnect.knownWords.matchMode` (`headword` / `surface`)
|
||||
- `ankiConnect.isKiku.fieldGrouping` (`auto` / `manual` / `disabled`)
|
||||
|
||||
Annotation toggles (`nPlusOne`, `enableJlpt`, `frequencyDictionary.enabled`) only apply to new subtitle lines after the toggle. The currently displayed line is not re-tokenized in place.
|
||||
@@ -732,6 +733,7 @@ Enable automatic Anki card creation and updates with media generation:
|
||||
"tags": ["SubMiner"],
|
||||
"deck": "Learning::Japanese",
|
||||
"fields": {
|
||||
"word": "Expression",
|
||||
"audio": "ExpressionAudio",
|
||||
"image": "Picture",
|
||||
"sentence": "Sentence",
|
||||
@@ -795,7 +797,8 @@ This example is intentionally compact. The option table below documents availabl
|
||||
| `proxy.upstreamUrl` | string (URL) | Upstream AnkiConnect URL that proxy forwards to (default: `http://127.0.0.1:8765`) |
|
||||
| `tags` | array of strings | Tags automatically added to cards mined/updated by SubMiner (default: `['SubMiner']`; set `[]` to disable automatic tagging). |
|
||||
| `deck` | string | Anki deck to monitor for new cards |
|
||||
| `ankiConnect.nPlusOne.decks` | array of strings | Decks used for N+1 known-word cache lookups. When omitted/empty, falls back to `ankiConnect.deck`. |
|
||||
| `ankiConnect.knownWords.decks` | array of strings | Decks used for known-word cache lookups. When omitted/empty, falls back to `ankiConnect.deck`. |
|
||||
| `fields.word` | string | Card field for mined word / expression text (default: `Expression`) |
|
||||
| `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`) |
|
||||
@@ -822,13 +825,13 @@ This example is intentionally compact. The option table below documents availabl
|
||||
| `behavior.overwriteImage` | `true`, `false` | Replace existing images on updates; when `false`, new images are appended/prepended per `behavior.mediaInsertMode` (default: `true`) |
|
||||
| `behavior.mediaInsertMode` | `"append"`, `"prepend"` | Where to insert new media when overwrite is off (default: `"append"`) |
|
||||
| `behavior.highlightWord` | `true`, `false` | Highlight the word in sentence context (default: `true`) |
|
||||
| `ankiConnect.nPlusOne.highlightEnabled` | `true`, `false` | Enable fast local highlighting for words already known in Anki (default: `false`) |
|
||||
| `ankiConnect.knownWords.highlightEnabled` | `true`, `false` | Enable fast local highlighting for words already known in Anki (default: `false`) |
|
||||
| `ankiConnect.knownWords.color` | hex color string | Text color for tokens already found in the local known-word cache (default: `"#a6da95"`). |
|
||||
| `ankiConnect.knownWords.matchMode` | `"headword"`, `"surface"` | Matching strategy for known-word highlighting (default: `"headword"`). `headword` uses token headwords; `surface` uses visible subtitle text. |
|
||||
| `ankiConnect.knownWords.refreshMinutes` | number | Minutes between known-word cache refreshes (default: `1440`) |
|
||||
| `ankiConnect.knownWords.decks` | array of strings | Decks used by known-word cache refresh. Leave empty for compatibility with legacy `deck` scope. |
|
||||
| `ankiConnect.nPlusOne.nPlusOne` | hex color string | Text color for the single target token to study when exactly one unknown candidate exists in a sentence (default: `"#c6a0f6"`). |
|
||||
| `ankiConnect.nPlusOne.knownWord` | hex color string | Legacy known-word color kept for backward compatibility (default: `"#a6da95"`). |
|
||||
| `ankiConnect.nPlusOne.matchMode` | `"headword"`, `"surface"` | Matching strategy for known-word highlighting (default: `"headword"`). `headword` uses token headwords; `surface` uses visible subtitle text. |
|
||||
| `ankiConnect.nPlusOne.minSentenceWords` | number | Minimum number of words required in a sentence before single unknown-word N+1 highlighting can trigger (default: `3`). |
|
||||
| `ankiConnect.nPlusOne.refreshMinutes` | number | Minutes between known-word cache refreshes (default: `1440`) |
|
||||
| `ankiConnect.nPlusOne.decks` | array of strings | Decks used by known-word cache refresh. Leave empty for compatibility with legacy `deck` scope. |
|
||||
| `behavior.notificationType` | `"osd"`, `"system"`, `"both"`, `"none"` | Notification type on card update (default: `"osd"`) |
|
||||
| `behavior.autoUpdateNewCards` | `true`, `false` | Automatically update cards on creation (default: `true`) |
|
||||
| `metadata.pattern` | string | Format pattern for metadata: `%f`=filename, `%F`=filename+ext, `%t`=time |
|
||||
@@ -863,20 +866,20 @@ SubMiner is intentionally built for [Kiku](https://kiku.youyoumu.my.id/) and [La
|
||||
|
||||
### N+1 Word Highlighting
|
||||
|
||||
When `ankiConnect.nPlusOne.highlightEnabled` is enabled, SubMiner builds a local cache of known words from Anki to highlight already learned tokens in subtitle rendering.
|
||||
When `ankiConnect.knownWords.highlightEnabled` is enabled, SubMiner builds a local cache of known words from Anki to highlight already learned tokens in subtitle rendering.
|
||||
|
||||
Known-word cache policy:
|
||||
|
||||
- Initial sync runs when the integration starts if the cache is missing or stale.
|
||||
- `ankiConnect.nPlusOne.refreshMinutes` controls the minimum time between refreshes; between refreshes, cached words are reused without querying Anki.
|
||||
- `ankiConnect.knownWords.refreshMinutes` controls the minimum time between refreshes; between refreshes, cached words are reused without querying Anki.
|
||||
- `ankiConnect.nPlusOne.nPlusOne` sets the color for the single target token when exactly one eligible unknown word exists.
|
||||
- `ankiConnect.nPlusOne.minSentenceWords` sets the minimum token count required in a sentence for N+1 highlighting (default: `3`).
|
||||
- `ankiConnect.nPlusOne.knownWord` sets the legacy known-word highlight color for tokens already in Anki.
|
||||
- `ankiConnect.nPlusOne.decks` accepts one or more decks. If empty, it uses the legacy single `ankiConnect.deck` value as scope.
|
||||
- `ankiConnect.knownWords.color` sets the known-word highlight color for tokens already in Anki.
|
||||
- `ankiConnect.knownWords.decks` accepts one or more decks. If empty, it uses the legacy single `ankiConnect.deck` value as scope.
|
||||
- Cache state is persisted to `known-words-cache.json` under the app `userData` directory.
|
||||
- The cache is automatically invalidated when the configured scope changes (for example, when deck changes).
|
||||
- Cache lookups are in-memory. By default, token headwords are matched against cached `Expression` / `Word` values; set `ankiConnect.nPlusOne.matchMode` to `"surface"` for raw subtitle text matching.
|
||||
- `ankiConnect.behavior.nPlusOne*` legacy keys (`nPlusOneHighlightEnabled`, `nPlusOneRefreshMinutes`, `nPlusOneMatchMode`) are deprecated and only kept for backward compatibility.
|
||||
- Cache lookups are in-memory. By default, token headwords are matched against cached `Expression` / `Word` values; set `ankiConnect.knownWords.matchMode` to `"surface"` for raw subtitle text matching.
|
||||
- Legacy moved keys under `ankiConnect.nPlusOne` (`highlightEnabled`, `refreshMinutes`, `matchMode`, `decks`, `knownWord`) and older `ankiConnect.behavior.nPlusOne*` keys are deprecated and only kept for backward compatibility.
|
||||
- Legacy top-level `ankiConnect` migration keys (for example `audioField`, `generateAudio`, `imageType`) are compatibility-only, validated before mapping, and ignored with a warning when invalid.
|
||||
- If AnkiConnect is unreachable, the cache remains in its previous state and an on-screen/system status message is shown.
|
||||
- Known-word sync activity is logged at `INFO`/`DEBUG` level with the `anki` logger scope and includes scope, notes returned, and word counts.
|
||||
@@ -886,9 +889,12 @@ To refresh roughly once per day, set:
|
||||
```json
|
||||
{
|
||||
"ankiConnect": {
|
||||
"nPlusOne": {
|
||||
"knownWords": {
|
||||
"highlightEnabled": true,
|
||||
"refreshMinutes": 1440
|
||||
},
|
||||
"nPlusOne": {
|
||||
"minSentenceWords": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1010,7 +1016,7 @@ Character dictionary sync behavior:
|
||||
|
||||
Current post-watch behavior:
|
||||
|
||||
- SubMiner attempts an update near episode completion (`>=85%` watched and at least `10` minutes watched).
|
||||
- SubMiner attempts an update near episode completion using the shared default minimum watch ratio (`0.85`, or `>=85%`) from `src/shared/watch-threshold.ts`, and requires at least `10` minutes watched. The same ratio is also used by local episode watched state transitions.
|
||||
- Episode/title detection is `guessit`-first with fallback to SubMiner's filename parser.
|
||||
- If `guessit` is unavailable, updates still work via fallback parsing but title matching can be less accurate.
|
||||
- If embedded AniList auth UI fails to render, SubMiner opens the authorize URL in your default browser and shows fallback instructions in-app.
|
||||
@@ -1164,7 +1170,7 @@ Troubleshooting:
|
||||
|
||||
### Immersion Tracking
|
||||
|
||||
Enable or disable local immersion analytics stored in SQLite for mined subtitles and media sessions:
|
||||
Enable or disable local immersion analytics stored in SQLite for mined subtitles and media sessions. This data also powers the stats dashboard:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -1176,12 +1182,20 @@ Enable or disable local immersion analytics stored in SQLite for mined subtitles
|
||||
"queueCap": 1000,
|
||||
"payloadCapBytes": 256,
|
||||
"maintenanceIntervalMs": 86400000,
|
||||
"retentionMode": "preset",
|
||||
"retentionPreset": "balanced",
|
||||
"retention": {
|
||||
"eventsDays": 7,
|
||||
"telemetryDays": 30,
|
||||
"dailyRollupsDays": 365,
|
||||
"monthlyRollupsDays": 1825,
|
||||
"vacuumIntervalDays": 7
|
||||
"eventsDays": 0,
|
||||
"telemetryDays": 0,
|
||||
"sessionsDays": 0,
|
||||
"dailyRollupsDays": 0,
|
||||
"monthlyRollupsDays": 0,
|
||||
"vacuumIntervalDays": 0
|
||||
},
|
||||
"lifetimeSummaries": {
|
||||
"global": true,
|
||||
"anime": true,
|
||||
"media": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1196,11 +1210,16 @@ Enable or disable local immersion analytics stored in SQLite for mined subtitles
|
||||
| `queueCap` | integer (`100`-`100000`) | In-memory queue cap. Overflow drops oldest writes. Default `1000`. |
|
||||
| `payloadCapBytes` | integer (`64`-`8192`) | Event payload byte cap before truncation marker. Default `256`. |
|
||||
| `maintenanceIntervalMs` | integer (`60000`-`604800000`) | Prune + rollup maintenance cadence. Default `86400000` (24h). |
|
||||
| `retention.eventsDays` | integer (`1`-`3650`) | Raw event retention window. Default `7` days. |
|
||||
| `retention.telemetryDays` | integer (`1`-`3650`) | Telemetry retention window. Default `30` days. |
|
||||
| `retention.dailyRollupsDays` | integer (`1`-`36500`) | Daily rollup retention window. Default `365` days. |
|
||||
| `retention.monthlyRollupsDays` | integer (`1`-`36500`) | Monthly rollup retention window. Default `1825` days (~5 years). |
|
||||
| `retention.vacuumIntervalDays` | integer (`1`-`3650`) | Minimum spacing between `VACUUM` passes. Default `7` days. |
|
||||
| `retentionMode` | `preset`,`advanced` | Retention mode. `preset` applies `retentionPreset`, `advanced` uses explicit values only. Default `preset`. |
|
||||
| `retentionPreset` | `minimal`,`balanced`,`deep-history` | Retention preset used when `retentionMode = "preset"`. Default `balanced`. |
|
||||
| `retention.eventsDays` | integer (`0`-`3650`) | Raw event retention window in days. Default `0` (keep all). |
|
||||
| `retention.telemetryDays` | integer (`0`-`3650`) | Telemetry retention window in days. Default `0` (keep all). |
|
||||
| `retention.sessionsDays` | integer (`0`-`3650`) | Session retention window in days. Default `0` (keep all). |
|
||||
| `retention.dailyRollupsDays` | integer (`0`-`36500`) | Daily rollup retention window. Default `0` (keep all). |
|
||||
| `retention.monthlyRollupsDays` | integer (`0`-`36500`) | Monthly rollup retention window. Default `0` (keep all). |
|
||||
| `retention.vacuumIntervalDays` | integer (`0`-`3650`) | Minimum spacing between `VACUUM` passes. `0` disables vacuum. Default `0` (disabled). |
|
||||
|
||||
Default behavior keeps raw events, telemetry, sessions, and rollups forever while still maintaining lifetime summary tables and daily/monthly rollups for faster reads. If you later want bounded retention, switch `retentionMode` or set explicit `retention.*` values.
|
||||
|
||||
When `dbPath` is blank or omitted, SubMiner writes telemetry and session summaries to the default app-data location:
|
||||
|
||||
@@ -1210,7 +1229,36 @@ When `dbPath` is blank or omitted, SubMiner writes telemetry and session summari
|
||||
|
||||
Set `dbPath` only if you want to relocate the database (for backup, syncing, or inspection workflows). The database is created when tracking starts for the first time.
|
||||
|
||||
See [Immersion Tracking Storage](/immersion-tracking) for schema details, query templates, retention/rollup behavior, backend portability notes, and the dedicated SQLite verification command.
|
||||
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
|
||||
|
||||
Configure the local stats UI served from SubMiner and the in-app stats overlay toggle:
|
||||
|
||||
```json
|
||||
{
|
||||
"stats": {
|
||||
"toggleKey": "Backquote",
|
||||
"serverPort": 5175,
|
||||
"autoStartServer": true,
|
||||
"autoOpenBrowser": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Values | Description |
|
||||
| ----------------- | ----------------- | --------------------------------------------------------------------------- |
|
||||
| `toggleKey` | Electron key code | Overlay-local key code used to toggle the stats overlay. Default `Backquote`. |
|
||||
| `serverPort` | integer | Localhost port for the browser stats UI. Default `5175`. |
|
||||
| `autoStartServer` | `true`, `false` | Start the local stats HTTP server automatically once immersion tracking is active. Default `true`. |
|
||||
| `autoOpenBrowser` | `true`, `false` | When `subminer stats` starts the server on demand, also open the dashboard in your default browser. Default `true`. |
|
||||
|
||||
Usage notes:
|
||||
|
||||
- The browser UI is served at `http://127.0.0.1:<serverPort>`.
|
||||
- The overlay toggle is local to the focused visible overlay window; it is not registered as a global OS shortcut.
|
||||
- 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, and Sessions tabs.
|
||||
|
||||
### YouTube Subtitle Generation
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Building & Testing
|
||||
|
||||
For internal architecture/workflow guidance, use `docs/README.md` at the repo root. This page stays focused on contributor-facing build and test commands.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh)
|
||||
@@ -13,6 +15,7 @@ cd SubMiner
|
||||
git submodule update --init --recursive
|
||||
|
||||
bun install
|
||||
(cd stats && bun install --frozen-lockfile)
|
||||
(cd vendor/texthooker-ui && bun install --frozen-lockfile)
|
||||
```
|
||||
|
||||
@@ -200,7 +203,7 @@ Run `make help` for a full list of targets. Key ones:
|
||||
| `make build-launcher` | Generate Bun launcher wrapper at `dist/launcher/subminer` |
|
||||
| `make install` | Install platform artifacts (wrapper, theme, AppImage/app bundle) |
|
||||
| `make install-plugin` | Install mpv Lua plugin and config |
|
||||
| `make deps` | Install JS dependencies (root + texthooker-ui) |
|
||||
| `make deps` | Install JS dependencies (root + stats + texthooker-ui) |
|
||||
| `make pretty` | Run scoped Prettier formatting for maintained source/config files |
|
||||
| `make generate-config` | Generate default config from centralized registry |
|
||||
| `make build-linux` | Convenience wrapper for Linux packaging |
|
||||
@@ -214,7 +217,7 @@ Run `make help` for a full list of targets. Key ones:
|
||||
- To add/change generated config template blocks/comments, update `src/config/definitions/template-sections.ts`.
|
||||
- Keep `src/config/definitions.ts` as the composed public API (`DEFAULT_CONFIG`, registries, template export) that wires domain modules together.
|
||||
- Overlay window/visibility state is owned by `src/core/services/overlay-manager.ts`.
|
||||
- Runtime architecture/module-boundary conventions are documented in [Architecture](/architecture); keep contributor changes aligned with that canonical guide.
|
||||
- Runtime architecture/module-boundary conventions are summarized in [Architecture](/architecture), with canonical internal guidance in `docs/architecture/README.md` at the repo root.
|
||||
- Linux packaged desktop launches pass `--background` using electron-builder `build.linux.executableArgs` in `package.json`.
|
||||
- Prefer direct inline deps objects in `src/main/` modules for simple pass-through wiring.
|
||||
- Add a helper/adapter service only when it performs meaningful adaptation, validation, or reuse (not identity mapping).
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Immersion Tracking
|
||||
|
||||
SubMiner can log your watching and mining activity to a local SQLite database. This is optional and disabled by default.
|
||||
SubMiner can log your watching and mining activity to a local SQLite database, then surface it in the built-in stats dashboard. Tracking is enabled by default and can be turned off if you do not want local analytics.
|
||||
|
||||
When enabled, SubMiner records per-session statistics (watch time, subtitle lines seen, words encountered, cards mined) and maintains daily and monthly rollups. You can query the database directly with any SQLite tool to track your progress over time.
|
||||
When enabled, SubMiner records per-session statistics (watch time, subtitle lines seen, words encountered, cards mined) and maintains exact lifetime summary tables plus daily/monthly rollups. You can view that data in SubMiner's stats UI or query the database directly with any SQLite tool.
|
||||
|
||||
Episode completion for local `watched` state uses the shared `DEFAULT_MIN_WATCH_RATIO` (`85%`) value from `src/shared/watch-threshold.ts`.
|
||||
|
||||
## Enabling
|
||||
|
||||
@@ -18,18 +20,119 @@ When enabled, SubMiner records per-session statistics (watch time, subtitle line
|
||||
- Leave `dbPath` empty to use the default location (`immersion.sqlite` in SubMiner's app-data directory).
|
||||
- Set an explicit path to move the database (useful for backups, cloud syncing, or external tools).
|
||||
|
||||
## Stats Dashboard
|
||||
|
||||
The same immersion data powers the stats dashboard.
|
||||
|
||||
- In-app overlay: focus the visible overlay, then press the key from `stats.toggleKey` (default: `` ` `` / `Backquote`).
|
||||
- Launcher command: run `subminer stats` to start the local stats server on demand and open the dashboard in your browser.
|
||||
- Background server: run `subminer stats -b` to start or reuse a dedicated background stats daemon without keeping the launcher attached, and `subminer stats -s` to stop that daemon.
|
||||
- Maintenance command: run `subminer stats cleanup` or `subminer stats cleanup -v` to backfill/repair vocabulary metadata (`headword`, `reading`, POS) and purge stale or excluded rows from `imm_words` on demand.
|
||||
- Browser page: open `http://127.0.0.1:5175` directly if the local stats server is already running.
|
||||
|
||||
### Dashboard Tabs
|
||||
|
||||
#### Overview
|
||||
|
||||
Recent sessions, streak calendar, watch-time history, and a tracking snapshot with completed episodes/anime totals.
|
||||
|
||||

|
||||
|
||||
#### Library
|
||||
|
||||
Cover-art library with search and sorting, per-series progress, episode drill-down, and direct links into mined cards.
|
||||
|
||||

|
||||
|
||||
#### Trends
|
||||
|
||||
Watch time, sessions, words seen, and per-anime progress/pattern charts with configurable date ranges and grouping.
|
||||
|
||||

|
||||
|
||||
#### Sessions
|
||||
|
||||
Expandable session history with new-word activity, cumulative totals, and pause/seek/card markers. Each session row exposes a hover-revealed ↗ button that navigates to the anime media-detail view for that session; pressing the back button there returns to the Sessions tab.
|
||||
|
||||

|
||||
|
||||
#### Vocabulary
|
||||
|
||||
Top repeated words (click a bar to open the word), new-word timeline, frequency rank table with full readings, kanji breakdown, word exclusion list, and click-through occurrence drilldown with Mine Word / Mine Sentence / Mine Audio buttons.
|
||||
|
||||

|
||||
|
||||
Stats server config lives under `stats`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"stats": {
|
||||
"toggleKey": "Backquote",
|
||||
"serverPort": 5175,
|
||||
"autoStartServer": true,
|
||||
"autoOpenBrowser": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `toggleKey` is overlay-local, not a system-wide shortcut.
|
||||
- `serverPort` controls the localhost dashboard URL.
|
||||
- `autoStartServer` starts the local stats HTTP server on launch once immersion tracking is active, or reuses the dedicated background stats server when one is already running.
|
||||
- `autoOpenBrowser` controls whether `subminer stats` launches the dashboard URL in your browser after ensuring the server is running.
|
||||
- `subminer stats` forces the dashboard server to start even when `autoStartServer` is `false`.
|
||||
- `subminer stats -b` starts or reuses the dedicated background stats daemon and exits after startup acknowledgement.
|
||||
- The background stats daemon is separate from the normal SubMiner overlay app, so you can leave it running and still launch SubMiner later to watch or mine from video.
|
||||
- `subminer stats -s` stops the dedicated background stats daemon without closing any browser tabs.
|
||||
- `subminer stats` fails with an error when `immersionTracking.enabled` is `false`.
|
||||
- `subminer stats cleanup` defaults to vocabulary cleanup, repairs stale `headword`, `reading`, and `part_of_speech` values, attempts best-effort MeCab backfill for legacy rows, and removes rows that still fail vocab filtering.
|
||||
|
||||
## Mining Cards from the Stats Page
|
||||
|
||||
The Vocabulary tab's word detail panel shows example lines from your viewing history. Each example line with a valid source file offers three mining buttons:
|
||||
|
||||
- **Mine Word** — performs a full Yomitan dictionary lookup for the word (definition, reading, pitch accent, etc.) via a short-lived hidden helper, then enriches the card with sentence audio, a screenshot or animated AVIF clip, the highlighted sentence, and metadata extracted from the source video file. Requires Anki and Yomitan dictionaries to be loaded.
|
||||
- **Mine Sentence** — creates a sentence card directly with the `IsSentenceCard` flag set (for Lapis/Kiku workflows), along with audio, image, and translation from the secondary subtitle if available.
|
||||
- **Mine Audio** — creates an audio-only card with the `IsAudioCard` flag, attaching only the sentence audio clip.
|
||||
|
||||
All three modes respect your `ankiConnect` config: deck, model, field mappings, media settings (static vs AVIF, quality, dimensions), audio padding, metadata pattern, and tags. Media generation runs in parallel for faster card creation.
|
||||
|
||||
Secondary subtitle text (typically English translations) is stored alongside primary subtitles during playback and used as the translation field when mining from the stats page.
|
||||
|
||||
### Word Exclusion List
|
||||
|
||||
The Vocabulary tab toolbar includes an **Exclusions** button for hiding words from all vocabulary views. Excluded words are stored in browser localStorage and can be managed (restored or cleared) from the exclusion modal. Exclusions affect stat cards, charts, the frequency rank table, and the word list.
|
||||
|
||||
## Retention Defaults
|
||||
|
||||
Data is kept for the following durations before automatic cleanup:
|
||||
By default, SubMiner keeps all retention tables and raw data (`0` means keep all) while continuing daily/monthly rollup maintenance:
|
||||
|
||||
| Data type | Retention |
|
||||
| -------------- | --------- |
|
||||
| Raw events | 7 days |
|
||||
| Telemetry | 30 days |
|
||||
| Daily rollups | 1 year |
|
||||
| Monthly rollups | 5 years |
|
||||
| Raw events | 0 (keep all) |
|
||||
| Telemetry | 0 (keep all) |
|
||||
| Sessions | 0 (keep all) |
|
||||
| Daily rollups | 0 (keep all) |
|
||||
| Monthly rollups | 0 (keep all) |
|
||||
|
||||
Maintenance runs on startup and every 24 hours. Vacuum runs weekly.
|
||||
Maintenance runs on startup and every 24 hours. Vacuum runs only when `retention.vacuumIntervalDays` is non-zero.
|
||||
|
||||
In practice:
|
||||
|
||||
- Overview totals read from lifetime summary tables, so all-time watch time/cards/words stay exact even if raw query paths evolve.
|
||||
- Anime and episode pages keep lifetime totals from summary tables while session drill-down still reads retained sessions directly. With the current defaults, both are kept forever.
|
||||
- Trends can read the full available history because daily/monthly rollups are also kept forever by default.
|
||||
- Vocabulary and kanji totals are cumulative and not bounded by the raw session retention knobs.
|
||||
|
||||
## Storage / Performance Model
|
||||
|
||||
The tracker is optimized for "keep everything" defaults:
|
||||
|
||||
- Exact all-time totals live in dedicated lifetime summary tables (`imm_lifetime_global`, `imm_lifetime_anime`, `imm_lifetime_media`).
|
||||
- Ended-session totals are persisted onto `imm_sessions`, so most dashboard reads do not need to rescan raw telemetry.
|
||||
- Daily and monthly rollups remain available for chart queries and coarse trend views.
|
||||
- Subtitle text is stored once in `imm_subtitle_lines`; subtitle-line event payloads keep compact metadata only.
|
||||
- Cover-art binaries are deduplicated through a shared blob store so episodes in the same series do not each carry duplicate image bytes.
|
||||
- Hot tables have dedicated indexes for session time ranges, telemetry sample windows, frequency-ranked vocabulary, and cover-art lookup keys.
|
||||
|
||||
## Configurable Knobs
|
||||
|
||||
@@ -44,9 +147,15 @@ All policy options live under `immersionTracking` in your config:
|
||||
| `maintenanceIntervalMs` | How often maintenance runs |
|
||||
| `retention.eventsDays` | Raw event retention |
|
||||
| `retention.telemetryDays` | Telemetry retention |
|
||||
| `retention.sessionsDays` | Session retention |
|
||||
| `retention.dailyRollupsDays` | Daily rollup retention |
|
||||
| `retention.monthlyRollupsDays` | Monthly rollup retention |
|
||||
| `retention.vacuumIntervalDays` | Minimum spacing between vacuums |
|
||||
| `retentionMode` | `preset` or `advanced` |
|
||||
| `retentionPreset` | `minimal`, `balanced`, or `deep-history` (used by `retentionMode`) |
|
||||
| `lifetimeSummaries.global` | Maintain global lifetime totals |
|
||||
| `lifetimeSummaries.anime` | Maintain per-anime lifetime totals |
|
||||
| `lifetimeSummaries.media` | Maintain per-media lifetime totals |
|
||||
|
||||
## Query Templates
|
||||
|
||||
@@ -75,26 +184,43 @@ SELECT
|
||||
s.video_id,
|
||||
s.started_at_ms,
|
||||
s.ended_at_ms,
|
||||
COALESCE(SUM(t.active_watched_ms), 0) AS active_watched_ms,
|
||||
COALESCE(SUM(t.words_seen), 0) AS words_seen,
|
||||
COALESCE(SUM(t.cards_mined), 0) AS cards_mined,
|
||||
COALESCE(s.active_watched_ms, 0) AS active_watched_ms,
|
||||
COALESCE(s.words_seen, 0) AS words_seen,
|
||||
COALESCE(s.cards_mined, 0) AS cards_mined,
|
||||
CASE
|
||||
WHEN COALESCE(SUM(t.active_watched_ms), 0) > 0
|
||||
THEN COALESCE(SUM(t.words_seen), 0) / (COALESCE(SUM(t.active_watched_ms), 0) / 60000.0)
|
||||
WHEN COALESCE(s.active_watched_ms, 0) > 0
|
||||
THEN COALESCE(s.words_seen, 0) / (COALESCE(s.active_watched_ms, 0) / 60000.0)
|
||||
ELSE NULL
|
||||
END AS words_per_min,
|
||||
CASE
|
||||
WHEN COALESCE(SUM(t.active_watched_ms), 0) > 0
|
||||
THEN (COALESCE(SUM(t.cards_mined), 0) * 60.0) / (COALESCE(SUM(t.active_watched_ms), 0) / 60000.0)
|
||||
WHEN COALESCE(s.active_watched_ms, 0) > 0
|
||||
THEN (COALESCE(s.cards_mined, 0) * 60.0) / (COALESCE(s.active_watched_ms, 0) / 60000.0)
|
||||
ELSE NULL
|
||||
END AS cards_per_hour
|
||||
FROM imm_sessions s
|
||||
LEFT JOIN imm_session_telemetry t ON t.session_id = s.session_id
|
||||
GROUP BY s.session_id
|
||||
ORDER BY s.started_at_ms DESC
|
||||
LIMIT ?;
|
||||
```
|
||||
|
||||
### Lifetime anime totals
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
a.anime_id,
|
||||
a.canonical_title,
|
||||
la.total_sessions,
|
||||
la.total_active_ms,
|
||||
la.total_cards,
|
||||
la.total_words_seen,
|
||||
la.total_lines_seen,
|
||||
la.first_watched_ms,
|
||||
la.last_watched_ms
|
||||
FROM imm_lifetime_anime la
|
||||
JOIN imm_anime a ON a.anime_id = la.anime_id
|
||||
ORDER BY la.last_watched_ms DESC
|
||||
LIMIT ?;
|
||||
```
|
||||
|
||||
### Daily rollups
|
||||
|
||||
```sql
|
||||
@@ -136,18 +262,29 @@ LIMIT ?;
|
||||
|
||||
- Write path is asynchronous and queue-backed. Hot paths (subtitle parsing, render, token flows) enqueue telemetry and never await SQLite writes.
|
||||
- Queue overflow policy: drop oldest queued writes, keep newest.
|
||||
- SQLite pragmas: `journal_mode=WAL`, `synchronous=NORMAL`, `foreign_keys=ON`, `busy_timeout=2500`.
|
||||
- SQLite tunings: `journal_mode=WAL`, `synchronous=NORMAL`, `foreign_keys=ON`, `busy_timeout=2500`, bounded WAL growth via `journal_size_limit`.
|
||||
- Maintenance executes `PRAGMA optimize` after periodic cleanup.
|
||||
- Rollups run incrementally from the last processed telemetry sample; startup performs a one-time bootstrap pass.
|
||||
- If retention pruning removes telemetry/session rows, maintenance triggers a full rollup rebuild to resync historical aggregates.
|
||||
- Cover-art blobs are deduplicated into `imm_cover_art_blobs` and referenced from `imm_media_art`.
|
||||
- Large-table reads are index-backed for `sample_ms`, session time windows, frequency-ranked words/kanji, and cover-art identity lookups.
|
||||
- Workload-dependent tuning knobs remain at defaults unless you change them: `cache_size`, `mmap_size`, `temp_store`, `auto_vacuum`.
|
||||
|
||||
### Schema (v3)
|
||||
### Schema (v4)
|
||||
|
||||
Core tables:
|
||||
|
||||
- `imm_videos` — video key/title/source metadata
|
||||
- `imm_sessions` — session UUID, video reference, timing/status
|
||||
- `imm_sessions` — session UUID, video reference, timing/status, final denormalized totals
|
||||
- `imm_session_telemetry` — high-frequency session aggregates over time
|
||||
- `imm_session_events` — event stream with compact numeric event types
|
||||
- `imm_subtitle_lines` — persisted subtitle text and timing per session/video
|
||||
|
||||
Lifetime summary tables:
|
||||
|
||||
- `imm_lifetime_global`
|
||||
- `imm_lifetime_anime`
|
||||
- `imm_lifetime_media`
|
||||
- `imm_lifetime_applied_sessions`
|
||||
|
||||
Rollup tables:
|
||||
|
||||
@@ -158,3 +295,8 @@ Vocabulary tables:
|
||||
|
||||
- `imm_words(id, headword, word, reading, first_seen, last_seen, frequency)`
|
||||
- `imm_kanji(id, kanji, first_seen, last_seen, frequency)`
|
||||
|
||||
Media-art tables:
|
||||
|
||||
- `imm_media_art` — per-video cover metadata plus shared blob reference
|
||||
- `imm_cover_art_blobs` — deduplicated image bytes keyed by blob hash
|
||||
|
||||
@@ -73,9 +73,9 @@ features:
|
||||
src: /assets/tokenization.svg
|
||||
alt: Tracking chart icon
|
||||
title: Immersion Tracking
|
||||
details: Logs watch time, words encountered, and cards mined to SQLite with daily and monthly rollups for long-term progress tracking.
|
||||
details: Logs watch time, words encountered, and cards mined to SQLite, then surfaces the same data in a local stats dashboard with rollups and session drill-down.
|
||||
link: /immersion-tracking
|
||||
linkText: Tracking details
|
||||
linkText: Stats details
|
||||
- icon:
|
||||
src: /assets/cross-platform.svg
|
||||
alt: Cross-platform icon
|
||||
@@ -102,7 +102,7 @@ const demoAssetVersion = '20260223-2';
|
||||
<div class="workflow-step" style="animation-delay: 60ms">
|
||||
<div class="step-number">02</div>
|
||||
<div class="step-title">Lookup</div>
|
||||
<div class="step-desc">Hover or click a token in the interactive overlay to open Yomitan context.</div>
|
||||
<div class="step-desc">Hover a token in the interactive overlay, then trigger Yomitan lookup to open context.</div>
|
||||
</div>
|
||||
<div class="workflow-connector" aria-hidden="true"></div>
|
||||
<div class="workflow-step" style="animation-delay: 120ms">
|
||||
|
||||
@@ -91,6 +91,7 @@ Use `subminer <subcommand> -h` for command-specific help.
|
||||
| `-S, --start-overlay` | Explicitly start overlay after mpv launches |
|
||||
| `-T, --no-texthooker` | Disable texthooker server |
|
||||
| `-p, --profile` | mpv profile name (default: `subminer`) |
|
||||
| `-a, --args` | Pass additional mpv arguments as a quoted string |
|
||||
| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`) |
|
||||
| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) |
|
||||
| `--dev`, `--debug` | Enable app dev-mode (not tied to log level) |
|
||||
|
||||
@@ -4,10 +4,10 @@ This guide walks through the sentence mining loop — from watching a video to c
|
||||
|
||||
## Overview
|
||||
|
||||
SubMiner runs as a transparent overlay on top of mpv. As subtitles play, the overlay displays them as interactive text. You click a word to look it up with Yomitan, then create an Anki card with a single action. SubMiner automatically attaches the sentence, audio clip, and screenshot.
|
||||
SubMiner runs as a transparent overlay on top of mpv. As subtitles play, the overlay displays them as interactive text. You hover a word, trigger Yomitan lookup with your configured lookup key/modifier, then create an Anki card with a single action. SubMiner automatically attaches the sentence, audio clip, and screenshot.
|
||||
|
||||
```text
|
||||
Watch video → See subtitle → Click word → Yomitan lookup → Add to Anki
|
||||
Watch video → See subtitle → Hover word + trigger lookup → Yomitan popup → Add to Anki
|
||||
↓
|
||||
SubMiner auto-fills:
|
||||
sentence, audio, image, translation
|
||||
@@ -30,9 +30,9 @@ SubMiner uses one overlay window with modal surfaces.
|
||||
|
||||
### Primary Subtitle Layer
|
||||
|
||||
The visible overlay renders subtitles as tokenized, clickable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
|
||||
The visible overlay renders subtitles as tokenized hoverable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
|
||||
|
||||
- Word-level click targets for Yomitan lookup
|
||||
- Word-level hover targets for Yomitan lookup
|
||||
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
|
||||
- Optional pause while the Yomitan popup is open (`subtitleStyle.autoPauseVideoOnYomitanPopup`)
|
||||
- Right-click to pause/resume
|
||||
@@ -55,9 +55,10 @@ Jimaku search, field-grouping, runtime options, and manual subsync open as modal
|
||||
## Looking Up Words
|
||||
|
||||
1. Hover over the subtitle area — the overlay activates pointer events.
|
||||
2. Click any word. SubMiner uses Unicode-aware boundary detection (`Intl.Segmenter`) to select it. On macOS, hovering is enough.
|
||||
3. Yomitan detects the selection and opens its lookup popup.
|
||||
4. From the popup, add the word to Anki.
|
||||
2. Hover the word you want. SubMiner keeps per-token boundaries so Yomitan can target that token cleanly.
|
||||
3. Trigger Yomitan lookup with your configured lookup key/modifier (for example `Shift` if that is how your Yomitan profile is set up).
|
||||
4. Yomitan opens its lookup popup for the hovered token.
|
||||
5. From the popup, add the word to Anki.
|
||||
|
||||
### Controller Workflow
|
||||
|
||||
@@ -83,7 +84,7 @@ There are three ways to create cards, depending on your workflow.
|
||||
|
||||
This is the most common flow. Yomitan creates a card in Anki, and SubMiner enriches it automatically.
|
||||
|
||||
1. Click a word → Yomitan popup appears.
|
||||
1. Hover a word, then trigger Yomitan lookup → Yomitan popup appears.
|
||||
2. Click the Anki icon in Yomitan to add the word.
|
||||
3. SubMiner receives or detects the new card:
|
||||
- **Proxy mode** (`ankiConnect.proxy.enabled: true`): immediate enrich after successful `addNote` / `addNotes`.
|
||||
@@ -194,7 +195,7 @@ See [Subtitle Annotations — N+1](/subtitle-annotations#n1-word-highlighting) f
|
||||
|
||||
## Immersion Tracking
|
||||
|
||||
SubMiner can log your watching and mining activity to a local SQLite database — session times, words seen, cards mined, and daily/monthly rollups.
|
||||
SubMiner can log your watching and mining activity to a local SQLite database and expose it in the built-in stats dashboard — session times, words seen, cards mined, and daily/monthly rollups.
|
||||
|
||||
Enable it in your config:
|
||||
|
||||
@@ -205,6 +206,8 @@ Enable it in your config:
|
||||
}
|
||||
```
|
||||
|
||||
See [Immersion Tracking](/immersion-tracking) for the full schema and retention settings.
|
||||
Open the dashboard in the overlay with `stats.toggleKey` (default: `` ` ``), launch it in a browser with `subminer stats`, keep a dedicated background server alive with `subminer stats -b`, stop that background server with `subminer stats -s`, or visit `http://127.0.0.1:5175` directly if the local stats server is already running. The dashboard covers overview totals, anime progress, session detail, and vocabulary drill-down from the same local immersion database.
|
||||
|
||||
See [Immersion Tracking](/immersion-tracking) for dashboard details, schema, and retention settings.
|
||||
|
||||
Next: [Anki Integration](/anki-integration) — field mapping, media generation, and card enrichment configuration.
|
||||
|
||||
@@ -319,6 +319,7 @@
|
||||
"SubMiner"
|
||||
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||
"fields": {
|
||||
"word": "Expression", // Card field for the mined word or expression text.
|
||||
"audio": "ExpressionAudio", // Audio setting.
|
||||
"image": "Picture", // Image setting.
|
||||
"sentence": "Sentence", // Sentence setting.
|
||||
@@ -339,10 +340,19 @@
|
||||
"animatedFps": 10, // Animated fps setting.
|
||||
"animatedMaxWidth": 640, // Animated max width setting.
|
||||
"animatedCrf": 35, // Animated crf setting.
|
||||
"syncAnimatedImageToWordAudio": true, // For animated AVIF images, prepend a frozen first frame matching the existing word-audio duration so motion starts with sentence audio. Values: true | false
|
||||
"audioPadding": 0.5, // Audio padding setting.
|
||||
"fallbackDuration": 3, // Fallback duration setting.
|
||||
"maxMediaDuration": 30 // Max media duration setting.
|
||||
}, // Media setting.
|
||||
"knownWords": {
|
||||
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
||||
"refreshMinutes": 1440, // Minutes between known-word cache refreshes.
|
||||
"addMinedWordsImmediately": true, // Immediately append newly mined card words into the known-word cache. Values: true | false
|
||||
"matchMode": "headword", // Known-word matching strategy for subtitle annotations. Values: headword | surface
|
||||
"decks": {}, // Decks and fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word", "Word Reading"] }.
|
||||
"color": "#a6da95" // Color used for known-word highlights.
|
||||
}, // Known words setting.
|
||||
"behavior": {
|
||||
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
||||
"overwriteImage": true, // Overwrite image setting. Values: true | false
|
||||
@@ -352,13 +362,8 @@
|
||||
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
||||
}, // Behavior setting.
|
||||
"nPlusOne": {
|
||||
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
||||
"refreshMinutes": 1440, // Minutes between known-word cache refreshes.
|
||||
"matchMode": "headword", // Known-word matching strategy for N+1 highlighting. Values: headword | surface
|
||||
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
||||
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
||||
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
||||
"knownWord": "#a6da95" // Color used for legacy known-word highlights.
|
||||
"nPlusOne": "#c6a0f6" // Color used for the single N+1 target token highlight.
|
||||
}, // N plus one setting.
|
||||
"metadata": {
|
||||
"pattern": "[SubMiner] %f (%t)" // Pattern setting.
|
||||
@@ -496,12 +501,33 @@
|
||||
"queueCap": 1000, // In-memory write queue cap before overflow policy applies.
|
||||
"payloadCapBytes": 256, // Max JSON payload size per event before truncation.
|
||||
"maintenanceIntervalMs": 86400000, // Maintenance cadence (prune + rollup + vacuum checks).
|
||||
"retentionMode": "preset", // Retention mode (`preset` uses preset values, `advanced` uses explicit values). Values: preset | advanced
|
||||
"retentionPreset": "balanced", // Retention preset when `retentionMode` is `preset`. Values: minimal | balanced | deep-history
|
||||
"retention": {
|
||||
"eventsDays": 7, // Raw event retention window in days.
|
||||
"telemetryDays": 30, // Telemetry retention window in days.
|
||||
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
||||
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
||||
"vacuumIntervalDays": 7 // Minimum days between VACUUM runs.
|
||||
} // Retention setting.
|
||||
} // Enable/disable immersion tracking.
|
||||
"eventsDays": 0, // Raw event retention window in days. Use 0 to keep all.
|
||||
"telemetryDays": 0, // Telemetry retention window in days. Use 0 to keep all.
|
||||
"sessionsDays": 0, // Session retention window in days. Use 0 to keep all.
|
||||
"dailyRollupsDays": 0, // Daily rollup retention window in days. Use 0 to keep all.
|
||||
"monthlyRollupsDays": 0, // Monthly rollup retention window in days. Use 0 to keep all.
|
||||
"vacuumIntervalDays": 0 // Minimum days between VACUUM runs. Use 0 to disable.
|
||||
}, // Retention setting.
|
||||
"lifetimeSummaries": {
|
||||
"global": true, // Maintain global lifetime stats rows. Values: true | false
|
||||
"anime": true, // Maintain per-anime lifetime stats rows. Values: true | false
|
||||
"media": true // Maintain per-media lifetime stats rows. Values: true | false
|
||||
} // Lifetime summaries setting.
|
||||
}, // Enable/disable immersion tracking.
|
||||
|
||||
// ==========================================
|
||||
// Stats Dashboard
|
||||
// Local immersion stats dashboard served on localhost and available as an in-app overlay.
|
||||
// Uses the immersion tracking database for overview, trends, sessions, and vocabulary views.
|
||||
// ==========================================
|
||||
"stats": {
|
||||
"toggleKey": "Backquote", // Key code to toggle the stats overlay.
|
||||
"markWatchedKey": "KeyW", // Key code to mark the current video as watched and advance to the next playlist entry.
|
||||
"serverPort": 6969, // Port for the stats HTTP server.
|
||||
"autoStartServer": true, // Automatically start the stats server on launch. Values: true | false
|
||||
"autoOpenBrowser": true // Automatically open the stats dashboard in a browser when the server starts. Values: true | false
|
||||
} // Local immersion stats dashboard served on localhost and available as an in-app overlay.
|
||||
}
|
||||
|
||||
BIN
docs-site/public/screenshots/anki-mining.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
docs-site/public/screenshots/annotations-key.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
docs-site/public/screenshots/annotations.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs-site/public/screenshots/stats-library.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
docs-site/public/screenshots/stats-overview.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs-site/public/screenshots/stats-sessions.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
docs-site/public/screenshots/stats-trends.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
docs-site/public/screenshots/stats-vocabulary.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
docs-site/public/screenshots/texthooker-empty.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
docs-site/public/screenshots/texthooker.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
docs-site/public/screenshots/yomitan-lookup.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
@@ -68,6 +68,9 @@ Mouse-hover playback behavior is configured separately from shortcuts: `subtitle
|
||||
| `Ctrl/Cmd+Shift+O` | Open runtime options palette | `shortcuts.openRuntimeOptions` |
|
||||
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
||||
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
||||
| `` ` `` | Toggle stats overlay | `stats.toggleKey` |
|
||||
|
||||
The stats toggle is handled inside the focused visible overlay window. It is configurable through the top-level `stats.toggleKey` setting and defaults to `Backquote`.
|
||||
|
||||
## Controller Shortcuts
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
SubMiner annotates subtitle tokens in real time as they appear in the overlay. Four annotation layers work together to surface useful context while you watch: **N+1 highlighting**, **character-name highlighting**, **frequency highlighting**, and **JLPT tagging**.
|
||||
|
||||
All four are opt-in and configured under `subtitleStyle` and `ankiConnect.nPlusOne` in your config. They apply independently — you can enable any combination.
|
||||
All four are opt-in and configured under `subtitleStyle`, `ankiConnect.knownWords`, and `ankiConnect.nPlusOne` in your config. They apply independently — you can enable any combination.
|
||||
|
||||
Before any of those layers render, SubMiner strips annotation metadata from tokens that are usually just subtitle glue or annotation noise. Standalone particles, auxiliaries, adnominals, common explanatory endings like `んです` / `のだ`, merged trailing quote-particle forms like `...って`, auxiliary-stem grammar tails like `そうだ` (MeCab POS3 `助動詞語幹`), repeated kana interjections, and similar non-lexical helper tokens remain hoverable in the subtitle text, but they render as plain tokens without known-word, N+1, frequency, JLPT, or name-match annotation styling.
|
||||
|
||||
## N+1 Word Highlighting
|
||||
|
||||
@@ -20,13 +22,13 @@ N+1 highlighting identifies sentences where you know every word except one, maki
|
||||
|
||||
| Option | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `ankiConnect.nPlusOne.highlightEnabled` | `false` | Enable N+1 highlighting |
|
||||
| `ankiConnect.nPlusOne.refreshMinutes` | `60` | Minutes between Anki cache refreshes |
|
||||
| `ankiConnect.nPlusOne.decks` | `[]` | Decks to query (falls back to `ankiConnect.deck`) |
|
||||
| `ankiConnect.nPlusOne.matchMode` | `"headword"` | `"headword"` (dictionary form) or `"surface"` (raw text) |
|
||||
| `ankiConnect.knownWords.highlightEnabled` | `false` | Enable known-word cache lookups used by N+1 highlighting |
|
||||
| `ankiConnect.knownWords.refreshMinutes` | `1440` | Minutes between Anki cache refreshes |
|
||||
| `ankiConnect.knownWords.decks` | `[]` | Decks to query (falls back to `ankiConnect.deck`) |
|
||||
| `ankiConnect.knownWords.matchMode` | `"headword"` | `"headword"` (dictionary form) or `"surface"` (raw text) |
|
||||
| `ankiConnect.nPlusOne.minSentenceWords` | `3` | Minimum tokens in a sentence for N+1 to trigger |
|
||||
| `subtitleStyle.nPlusOneColor` | `#c6a0f6` | Color for the single unknown target word |
|
||||
| `subtitleStyle.knownWordColor` | `#a6da95` | Color for already-known tokens |
|
||||
| `ankiConnect.nPlusOne.nPlusOne` | `#c6a0f6` | Color for the single unknown target word |
|
||||
| `ankiConnect.knownWords.color` | `#a6da95` | Color for already-known tokens |
|
||||
|
||||
::: tip
|
||||
Set `refreshMinutes` to `1440` (24 hours) for daily sync if your Anki collection is large.
|
||||
@@ -34,7 +36,7 @@ Set `refreshMinutes` to `1440` (24 hours) for daily sync if your Anki collection
|
||||
|
||||
## Character-Name Highlighting
|
||||
|
||||
Character-name matches are built from the active merged SubMiner character dictionary, which auto-syncs character data from AniList for your recently-watched titles. Matching names are highlighted in subtitles and become clickable for full character profiles — portraits, roles, voice actors, and biographical detail.
|
||||
Character-name matches are built from the active merged SubMiner character dictionary, which auto-syncs character data from AniList for your recently-watched titles. Matching names are highlighted in subtitles and become available for hover-driven Yomitan character profiles — portraits, roles, voice actors, and biographical detail.
|
||||
|
||||
**How it works:**
|
||||
|
||||
@@ -80,6 +82,10 @@ When `sourcePath` is omitted, SubMiner searches default install/runtime location
|
||||
Frequency highlighting skips tokens that look like non-lexical noise (kana reduplication, short kana endings like `っ`), even when dictionary ranks exist.
|
||||
:::
|
||||
|
||||
::: info
|
||||
Frequency, JLPT, and N+1 metadata are only shown for tokens that survive the subtitle-annotation noise filter. Standalone grammar tokens like `は`, `です`, and `この` are intentionally left unannotated even if a dictionary can assign them metadata.
|
||||
:::
|
||||
|
||||
## JLPT Tagging
|
||||
|
||||
JLPT tagging adds colored underlines to tokens based on their JLPT level (N1–N5), giving you an at-a-glance sense of difficulty distribution in each subtitle line.
|
||||
@@ -115,7 +121,7 @@ JLPT tagging requires the offline vocabulary bundle. See [JLPT Vocabulary Bundle
|
||||
|
||||
All annotation layers can be toggled at runtime via the mpv command menu without restarting:
|
||||
|
||||
- `ankiConnect.nPlusOne.highlightEnabled` (`On` / `Off`)
|
||||
- `ankiConnect.knownWords.highlightEnabled` (`On` / `Off`)
|
||||
- `subtitleStyle.nameMatchEnabled` (`On` / `Off`)
|
||||
- `subtitleStyle.enableJlpt` (`On` / `Off`)
|
||||
- `subtitleStyle.frequencyDictionary.enabled` (`On` / `Off`)
|
||||
|
||||
@@ -178,12 +178,12 @@ SubMiner does not load the source tree directly from `vendor/subminer-yomitan`;
|
||||
|
||||
If you installed from the AppImage and see this error, the package may be incomplete. Re-download the AppImage or place the unpacked Yomitan extension manually in `~/.config/SubMiner/yomitan`.
|
||||
|
||||
**Yomitan popup does not appear when clicking words**
|
||||
**Yomitan lookup popup does not appear when hovering words or triggering lookup**
|
||||
|
||||
- Verify Yomitan loaded successfully — check the terminal output for "Loaded Yomitan extension".
|
||||
- Yomitan requires dictionaries to be installed. Open Yomitan settings (`Alt+Shift+Y` or `SubMiner.AppImage --settings`) and confirm at least one dictionary is imported.
|
||||
- If `yomitan.externalProfilePath` is set, import/check dictionaries in the external app/profile instead. SubMiner treats that profile as read-only and does not open its own Yomitan settings window.
|
||||
- If the overlay shows subtitles but words are not clickable, the tokenizer may have failed. See the MeCab section below.
|
||||
- If the overlay shows subtitles but hover lookup never resolves on tokens, the tokenizer may have failed. See the MeCab section below.
|
||||
|
||||
## MeCab / Tokenization
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
3. The overlay connects and subscribes to subtitle changes
|
||||
4. Subtitles are tokenized with Yomitan's internal parser
|
||||
5. Words are displayed as interactive spans in the overlay
|
||||
6. Hovering or clicking a word triggers Yomitan popup for dictionary lookup
|
||||
6. Hover a word, then trigger Yomitan lookup with your configured lookup key/modifier to open the Yomitan popup
|
||||
7. Optional [subtitle annotations](/subtitle-annotations) (N+1, character-name, frequency, JLPT) highlight useful cues in real time
|
||||
|
||||
There are two ways to use SubMiner:
|
||||
@@ -56,6 +56,7 @@ subminer ytsearch:"jp news" # Play first YouTube search result
|
||||
subminer --setup # Open first-run setup popup
|
||||
subminer --log-level debug video.mkv # Enable verbose logs for launch/debugging
|
||||
subminer --log-level warn video.mkv # Set logging level explicitly
|
||||
subminer --args '--fs=opengl-hq --ytdl-format=bestvideo*+bestaudio/best' video.mkv # Pass extra mpv args
|
||||
|
||||
# Options
|
||||
subminer -T video.mkv # Disable texthooker server
|
||||
@@ -189,6 +190,8 @@ Top-level launcher flags like `--jellyfin-*` and `--yt-subgen-*` are intentional
|
||||
- `--secondary-sid=auto`
|
||||
- `--secondary-sub-visibility=no`
|
||||
|
||||
You can append additional MPV arguments with launcher `-a/--args`, for example `--args "--ao=alsa --volume=80"`.
|
||||
|
||||
You can define a matching profile in `~/.config/mpv/mpv.conf` for consistency when launching `mpv` manually or from other tools. `subminer` launches with `--profile=subminer` by default (or override with `subminer -p <profile> ...`):
|
||||
|
||||
```ini
|
||||
|
||||