diff --git a/assets/kiku-integration-poster.jpg b/assets/kiku-integration-poster.jpg
index 9f23cd0..4f9e263 100644
Binary files a/assets/kiku-integration-poster.jpg and b/assets/kiku-integration-poster.jpg differ
diff --git a/assets/kiku-integration.gif b/assets/kiku-integration.gif
new file mode 100644
index 0000000..c92f3de
Binary files /dev/null and b/assets/kiku-integration.gif differ
diff --git a/assets/kiku-integration.mkv b/assets/kiku-integration.mkv
new file mode 100644
index 0000000..97d0f90
Binary files /dev/null and b/assets/kiku-integration.mkv differ
diff --git a/assets/kiku-integration.mp4 b/assets/kiku-integration.mp4
new file mode 100644
index 0000000..a3692f1
Binary files /dev/null and b/assets/kiku-integration.mp4 differ
diff --git a/assets/kiku-integration.webm b/assets/kiku-integration.webm
index bb995f0..4bcc2a2 100644
Binary files a/assets/kiku-integration.webm and b/assets/kiku-integration.webm differ
diff --git a/assets/minecard-poster.jpg b/assets/minecard-poster.jpg
new file mode 100644
index 0000000..c338624
Binary files /dev/null and b/assets/minecard-poster.jpg differ
diff --git a/assets/minecard.gif b/assets/minecard.gif
index a288825..4d45c78 100644
Binary files a/assets/minecard.gif and b/assets/minecard.gif differ
diff --git a/assets/minecard.jpg b/assets/minecard.jpg
new file mode 100644
index 0000000..0734e8b
Binary files /dev/null and b/assets/minecard.jpg differ
diff --git a/assets/minecard.mkv b/assets/minecard.mkv
index 6b1cff6..027697f 100644
Binary files a/assets/minecard.mkv and b/assets/minecard.mkv differ
diff --git a/assets/minecard.mp4 b/assets/minecard.mp4
index 3017ec2..2885b02 100644
Binary files a/assets/minecard.mp4 and b/assets/minecard.mp4 differ
diff --git a/assets/minecard.webm b/assets/minecard.webm
index ae930fe..4aa05d8 100644
Binary files a/assets/minecard.webm and b/assets/minecard.webm differ
diff --git a/docs/architecture.md b/docs/architecture.md
index f1ed391..3d968d9 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -4,7 +4,7 @@ SubMiner is split into three cooperating runtimes:
- Electron desktop app (`src/`) for overlay/UI/runtime orchestration.
- Launcher CLI (`launcher/`) for mpv/app command workflows.
-- mpv Lua plugin (`plugin/subminer/main.lua` + module files) for player-side controls and IPC handoff.
+- mpv Lua plugin (`plugin/subminer/init.lua` + module files) for player-side controls and IPC handoff.
Within the desktop app, `src/main.ts` is a composition root that wires small runtime/domain modules plus core services.
@@ -26,7 +26,9 @@ launcher/ # Standalone CLI launcher wrapper and mpv helpers
config/ # Launcher config parsers + CLI parser builder
main.ts # Launcher entrypoint and command dispatch
plugin/
- subminer/ # mpv plugin modules + main entrypoint
+ subminer/ # Modular mpv plugin (init · main · bootstrap · lifecycle · process
+ # state · messages · hover · ui · options · environment · log
+ # binary · aniskip · aniskip_match)
src/
main-entry.ts # Background-mode bootstrap wrapper before loading main.js
main.ts # Entry point — delegates to runtime composers/domain modules
@@ -66,24 +68,26 @@ src/
renderer/ # Overlay renderer (modularized UI/runtime)
handlers/ # Keyboard/mouse interaction modules
modals/ # Jimaku/Kiku/subsync/runtime-options/session-help modals
- positioning/ # Invisible-layer layout + offset controllers
+ positioning/ # Subtitle position controller (drag-to-reposition)
window-trackers/ # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS)
jimaku/ # Jimaku API integration helpers
subsync/ # Subtitle sync (alass/ffsubsync) helpers
subtitle/ # Subtitle processing utilities
tokenizers/ # Tokenizer implementations
+ anki-integration/ # AnkiConnect proxy server + note-update enrichment workflow
token-mergers/ # Token merge strategies
translators/ # AI translation providers
```
### Service Layer (`src/core/services/`)
-- **Overlay/window runtime:** `overlay-manager.ts`, `overlay-window.ts`, `overlay-visibility.ts`, `overlay-bridge.ts`, `overlay-runtime-init.ts`, `overlay-content-measurement.ts`, `overlay-drop.ts`
+- **Overlay/window runtime:** `overlay-manager.ts`, `overlay-window.ts`, `overlay-visibility.ts`, `overlay-bridge.ts`, `overlay-runtime-init.ts`, `overlay-content-measurement.ts`
- **Shortcuts/input:** `shortcut.ts`, `overlay-shortcut.ts`, `overlay-shortcut-handler.ts`, `shortcut-fallback.ts`, `numeric-shortcut.ts`
- **MPV runtime:** `mpv.ts`, `mpv-transport.ts`, `mpv-protocol.ts`, `mpv-properties.ts`, `mpv-render-metrics.ts`
- **Mining + Anki/Jimaku runtime:** `mining.ts`, `field-grouping.ts`, `field-grouping-overlay.ts`, `anki-jimaku.ts`, `anki-jimaku-ipc.ts`
-- **Subtitle/token pipeline:** `subtitle-processing-controller.ts`, `subtitle-position.ts`, `subtitle-ws.ts`, `tokenizer.ts` + `tokenizer/*` stage modules
+- **Subtitle/token pipeline:** `subtitle-processing-controller.ts`, `subtitle-position.ts`, `subtitle-ws.ts`, `tokenizer.ts` + `tokenizer/*` stage modules (including `parser-enrichment-worker-runtime.ts` for async MeCab enrichment and `yomitan-parser-runtime.ts`)
- **Integrations:** `jimaku.ts`, `subsync.ts`, `subsync-runner.ts`, `texthooker.ts`, `jellyfin.ts`, `jellyfin-remote.ts`, `discord-presence.ts`, `yomitan-extension-loader.ts`, `yomitan-settings.ts`
+- **Anki integration:** `anki-integration.ts`, `anki-integration/anki-connect-proxy.ts` (local proxy for push-based auto-enrichment), `anki-integration/note-update-workflow.ts`
- **Config/runtime controls:** `config-hot-reload.ts`, `runtime-options-ipc.ts`, `cli-command.ts`, `startup.ts`
- **Domain submodules:** `anilist/*` (token/update queue/updater), `immersion-tracker/*` (storage/session/metadata/query/reducer)
@@ -95,14 +99,15 @@ The renderer keeps `renderer.ts` focused on orchestration. UI behavior is delega
src/renderer/
renderer.ts # Entrypoint/orchestration only
context.ts # Shared runtime context contract
- state.ts # Centralized renderer mutable state
+ state.ts # Centralized renderer mutable state (visible overlay only)
error-recovery.ts # Global renderer error boundary + recovery actions
overlay-content-measurement.ts # Reports rendered bounds to main process
subtitle-render.ts # Primary/secondary subtitle rendering + style application
positioning.ts # Facade export for positioning controller
+ yomitan-popup.ts # Yomitan popup iframe detection utilities
positioning/
- controller.ts # Position controller orchestration
- position-state.ts # Position state helpers
+ controller.ts # Subtitle drag-position controller
+ position-state.ts # Position state helpers (yPercent)
handlers/
keyboard.ts # Keybindings, chord handling, modal key routing
mouse.ts # Hover/drag behavior, selection + observer wiring
@@ -120,7 +125,7 @@ src/renderer/
### Launcher + Plugin Runtimes
- `launcher/main.ts` dispatches commands through `launcher/commands/*` and shared config readers in `launcher/config/*`. It handles mpv startup, app passthrough, Jellyfin helper commands, and playback handoff.
-- `plugin/subminer/main.lua` runs inside mpv and loads module files for overlay toggles, hover-token messages, and AniSkip intro-skip UX.
+- `plugin/subminer/init.lua` runs inside mpv and loads modular Lua files: `main.lua` (orchestration), `bootstrap.lua` (startup), `lifecycle.lua` (connect/disconnect), `process.lua` (process management), `state.lua` (shared state), `messages.lua` (IPC), `hover.lua` (hover-token highlight rendering), `ui.lua` (OSD rendering), `options.lua` (config), `environment.lua` (detection), `log.lua` (logging), `binary.lua` (path resolution), `aniskip.lua` + `aniskip_match.lua` (intro-skip UX).
## Flow Diagram
@@ -138,7 +143,7 @@ flowchart LR
subgraph ExtRt["External Runtimes"]
Launcher["launcher/
CLI dispatch"]:::extrt
- Plugin["subminer/main.lua
mpv plugin"]:::extrt
+ Plugin["subminer/init.lua
mpv plugin"]:::extrt
end
subgraph Ext["External Systems"]
@@ -161,8 +166,9 @@ flowchart LR
subgraph Svc["Services — src/core/services/"]
Mpv["MPV Stack
transport · protocol
properties · metrics"]:::svc
- Overlay["Overlay Manager
window · visibility · bridge"]:::svc
+ OverlaySvc["Overlay Manager
window · visibility · bridge
mpv-sub-visibility"]:::svc
Mining["Mining & Subtitles
mining · field-grouping
subtitle-ws · tokenizer"]:::svc
+ AnkiProxy["Anki Integration
anki-connect-proxy
note-update-workflow"]:::svc
Integrations["Integrations
jimaku · subsync
texthooker · yomitan"]:::svc
Tracking["Tracking
anilist · jellyfin
immersion · discord"]:::svc
Config["Config & Runtime
hot-reload
runtime-options"]:::svc
@@ -171,7 +177,7 @@ flowchart LR
Bridge(["preload.ts
Electron IPC"]):::bridge
subgraph Rend["Renderer — src/renderer/"]
- Overlay["Main overlay window
primary + secondary subtitles"]:::rend
+ OverlayWin["Main overlay window
primary + secondary subtitles"]:::rend
UI["subtitle-render
positioning
handlers · modals"]:::rend
end
@@ -182,16 +188,16 @@ flowchart LR
Comp --> Svc
mpvExt <-->|"JSON socket"| Mpv
- AnkiExt <-->|"HTTP"| Mining
+ AnkiExt <-->|"HTTP"| AnkiProxy
JimakuExt <-->|"HTTP"| Integrations
- TrackerExt <-->|"platform"| Overlay
+ TrackerExt <-->|"platform"| OverlaySvc
AnilistExt <-->|"HTTP"| Tracking
JellyfinExt <-->|"HTTP"| Tracking
DiscordExt <-->|"RPC"| Integrations
- Overlay & Mining --> Bridge
- Bridge --> Overlay
- Overlay --> UI
+ OverlaySvc & Mining --> Bridge
+ Bridge --> OverlayWin
+ OverlayWin --> UI
style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5
style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5
@@ -259,10 +265,10 @@ For domains migrated to reducer-style transitions (for example AniList token/que
- **Module-level init:** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs all services, and wires dependency injection. `runAndApplyStartupState()` parses CLI args and detects the compositor backend.
- **Startup:** If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks.
- **Critical-path init:** Once `app.whenReady()` fires, `composeAppReadyRuntime()` runs strict config reload, resolves keybindings, creates the `MpvIpcClient` (which immediately connects and subscribes to 26 properties), and initializes the `RuntimeOptionsManager`, `SubtitleTimingTracker`, and `ImmersionTrackerService`.
-- **Overlay runtime:** `initializeOverlayRuntime()` creates the primary overlay window (interactive Yomitan lookups and subtitle rendering) and registers global shortcuts and bounds tracking via the active window tracker.
-- **Background warmups:** Non-critical services are launched asynchronously: MeCab tokenizer check, Yomitan extension load, JLPT + frequency dictionary prewarm, optional Jellyfin remote session, Discord presence service, and AniList token refresh. Warmup coverage is configurable through `startupWarmups` (including low-power mode that defers all but Yomitan).
+- **Overlay runtime:** `initializeOverlayRuntime()` creates the primary overlay window (interactive Yomitan lookups and subtitle rendering), registers global shortcuts, and sets up bounds tracking via the active window tracker. mpv subtitle suppression is handled by a dedicated `overlay-mpv-sub-visibility` service.
+- **Background warmups:** Non-critical services are launched asynchronously: MeCab tokenizer check (with async worker thread), Yomitan extension load, JLPT + frequency dictionary prewarm, optional Jellyfin remote session, Discord presence service, AniList token refresh, and optional AnkiConnect proxy server. Warmup coverage is configurable through `startupWarmups` (including low-power mode that defers all but Yomitan).
- **Runtime:** Event-driven. mpv property changes, IPC messages, CLI commands, overlay shortcuts, and hot-reload notifications route through runtime handlers/composers. Subtitle text flows through `SubtitlePipeline` (normalize → tokenize → merge), and results are sent to the main overlay renderer and modal surfaces.
-- **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, and cleans Anki/AniList state.
+- **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, stops the AnkiConnect proxy server, and cleans Anki/AniList state.
```mermaid
flowchart LR
@@ -303,13 +309,14 @@ flowchart LR
subgraph WarmupGroup[" "]
direction TB
- W1["MeCab"]:::warmup
+ W1["MeCab
+ worker thread"]:::warmup
W2["Yomitan"]:::warmup
W3["JLPT + freq
dictionaries"]:::warmup
W4["Jellyfin"]:::warmup
W5["Discord"]:::warmup
W6["AniList"]:::warmup
- W1 ~~~ W2 ~~~ W3 ~~~ W4 ~~~ W5 ~~~ W6
+ W7["AnkiConnect
proxy"]:::warmup
+ W1 ~~~ W2 ~~~ W3 ~~~ W4 ~~~ W5 ~~~ W6 ~~~ W7
end
Warmups --> WarmupGroup
@@ -333,7 +340,7 @@ flowchart LR
Quit --> T1["Tray · config watcher
global shortcuts"]:::shutdown
Quit --> T2["WebSocket · texthooker
mpv socket · OSD log"]:::shutdown
Quit --> T3["Window tracker
Yomitan parser"]:::shutdown
- Quit --> T4["Immersion tracker
Jellyfin · Discord
Anki · AniList"]:::shutdown
+ Quit --> T4["Immersion tracker
Jellyfin · Discord
Anki proxy · AniList"]:::shutdown
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
```
diff --git a/docs/public/assets/anki-card.svg b/docs/public/assets/anki-card.svg
index bf9d4aa..7bd78fd 100644
--- a/docs/public/assets/anki-card.svg
+++ b/docs/public/assets/anki-card.svg
@@ -1,15 +1,40 @@
diff --git a/docs/public/assets/highlight.svg b/docs/public/assets/highlight.svg
index 98edbaa..fa933af 100644
--- a/docs/public/assets/highlight.svg
+++ b/docs/public/assets/highlight.svg
@@ -1,13 +1,39 @@
diff --git a/docs/public/assets/keyboard.svg b/docs/public/assets/keyboard.svg
index 8148d13..bdaf25c 100644
--- a/docs/public/assets/keyboard.svg
+++ b/docs/public/assets/keyboard.svg
@@ -1,21 +1,31 @@
diff --git a/docs/public/assets/kiku-integration-poster.jpg b/docs/public/assets/kiku-integration-poster.jpg
index 9f23cd0..4f9e263 100644
Binary files a/docs/public/assets/kiku-integration-poster.jpg and b/docs/public/assets/kiku-integration-poster.jpg differ
diff --git a/docs/public/assets/kiku-integration.gif b/docs/public/assets/kiku-integration.gif
new file mode 100644
index 0000000..c92f3de
Binary files /dev/null and b/docs/public/assets/kiku-integration.gif differ
diff --git a/docs/public/assets/kiku-integration.mkv b/docs/public/assets/kiku-integration.mkv
new file mode 100644
index 0000000..97d0f90
Binary files /dev/null and b/docs/public/assets/kiku-integration.mkv differ
diff --git a/docs/public/assets/kiku-integration.mp4 b/docs/public/assets/kiku-integration.mp4
new file mode 100644
index 0000000..a3692f1
Binary files /dev/null and b/docs/public/assets/kiku-integration.mp4 differ
diff --git a/docs/public/assets/kiku-integration.webm b/docs/public/assets/kiku-integration.webm
index bb995f0..4bcc2a2 100644
Binary files a/docs/public/assets/kiku-integration.webm and b/docs/public/assets/kiku-integration.webm differ
diff --git a/docs/public/assets/minecard-poster.jpg b/docs/public/assets/minecard-poster.jpg
index a1e8139..c338624 100644
Binary files a/docs/public/assets/minecard-poster.jpg and b/docs/public/assets/minecard-poster.jpg differ
diff --git a/docs/public/assets/minecard.gif b/docs/public/assets/minecard.gif
index a288825..4d45c78 100644
Binary files a/docs/public/assets/minecard.gif and b/docs/public/assets/minecard.gif differ
diff --git a/docs/public/assets/minecard.mkv b/docs/public/assets/minecard.mkv
index 6b1cff6..027697f 100644
Binary files a/docs/public/assets/minecard.mkv and b/docs/public/assets/minecard.mkv differ
diff --git a/docs/public/assets/minecard.mp4 b/docs/public/assets/minecard.mp4
index 3017ec2..2885b02 100644
Binary files a/docs/public/assets/minecard.mp4 and b/docs/public/assets/minecard.mp4 differ
diff --git a/docs/public/assets/minecard.webm b/docs/public/assets/minecard.webm
index ae930fe..4aa05d8 100644
Binary files a/docs/public/assets/minecard.webm and b/docs/public/assets/minecard.webm differ
diff --git a/docs/public/assets/subtitle-download.svg b/docs/public/assets/subtitle-download.svg
index 76f7dc9..f795ea2 100644
--- a/docs/public/assets/subtitle-download.svg
+++ b/docs/public/assets/subtitle-download.svg
@@ -1,16 +1,35 @@
diff --git a/docs/public/assets/texthooker.svg b/docs/public/assets/texthooker.svg
index 4da1998..bf305bf 100644
--- a/docs/public/assets/texthooker.svg
+++ b/docs/public/assets/texthooker.svg
@@ -1,19 +1,46 @@
diff --git a/docs/public/assets/tokenization.svg b/docs/public/assets/tokenization.svg
index b74b8db..d9450d0 100644
--- a/docs/public/assets/tokenization.svg
+++ b/docs/public/assets/tokenization.svg
@@ -1,16 +1,34 @@
diff --git a/scripts/mkv-to-readme-video.sh b/scripts/mkv-to-readme-video.sh
index 9f017d0..2f24908 100755
--- a/scripts/mkv-to-readme-video.sh
+++ b/scripts/mkv-to-readme-video.sh
@@ -12,12 +12,13 @@ Description:
- .mp4 (H.264 + AAC, prefers NVIDIA GPU if available)
- .webm (AV1/VP9 + Opus, prefers NVIDIA GPU if available)
- .gif (palette-optimised, 15 fps)
+ - -poster.jpg (single frame for video poster fallback)
Options:
-f, --force Overwrite existing output files
Encoding profile:
- - Crop: 1920x1080 at x=760 y=180
+ - Crop: 1920x1080 at x=760 y=200
- MP4: H.264 + AAC
- WebM: AV1/VP9 + Opus at 30 fps
USAGE
@@ -28,11 +29,11 @@ input=""
while [[ $# -gt 0 ]]; do
case "$1" in
- -h|--help)
+ -h | --help)
usage
exit 0
;;
- -f|--force)
+ -f | --force)
force=1
;;
-*)
@@ -74,6 +75,7 @@ base="${filename%.*}"
mp4_out="$dir/$base.mp4"
webm_out="$dir/$base.webm"
gif_out="$dir/$base.gif"
+poster_out="$dir/$base-poster.jpg"
overwrite_flag="-n"
if [[ "$force" -eq 1 ]]; then
@@ -81,7 +83,7 @@ if [[ "$force" -eq 1 ]]; then
fi
if [[ "$force" -eq 0 ]]; then
- for output in "$mp4_out" "$webm_out" "$gif_out"; do
+ for output in "$mp4_out" "$webm_out" "$gif_out" "$poster_out"; do
if [[ -e "$output" ]]; then
echo "Error: output exists: $output (use --force to overwrite)" >&2
exit 1
@@ -94,7 +96,7 @@ has_encoder() {
ffmpeg -hide_banner -encoders 2> /dev/null | grep -qE "[[:space:]]${encoder}[[:space:]]"
}
-crop_vf="crop=1920:1080:760:180"
+crop_vf="crop=1920:1080:760:205"
webm_vf="${crop_vf},fps=30"
gif_vf="${crop_vf},fps=15,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3"
@@ -162,7 +164,15 @@ ffmpeg "$overwrite_flag" -i "$input" \
-vf "$gif_vf" \
"$gif_out"
+echo "Generating poster: $poster_out"
+ffmpeg "$overwrite_flag" -ss 00:00:05 -i "$input" \
+ -vf "$crop_vf" \
+ -vframes 1 \
+ -q:v 2 \
+ "$poster_out"
+
echo "Done."
echo "MP4: $mp4_out"
echo "WebM: $webm_out"
echo "GIF: $gif_out"
+echo "Poster: $poster_out"