docs: overhaul documentation and add four new pages

- Add mining-workflow.md: end-to-end sentence mining guide
- Add anki-integration.md: AnkiConnect setup, field mapping, media generation, field grouping
- Add mpv-plugin.md: chord keybindings, subminer.conf options, script messages
- Add troubleshooting.md: common issues and solutions by category
- Rewrite architecture.md to reflect current ~1,400-line main.ts and ~35 services
- Expand development.md from ~25 lines to full dev guide
- Fix URLs to ksyasuda/SubMiner, version to v0.1.0, AppImage naming
- Update VitePress sidebar with three-group layout (Getting Started, Reference, Development)
- Update navigation in index.md, README.md, docs/README.md
- Remove obsolete planning artifacts (plan.md, investigation.md, comparison.md, composability.md, refactor-main-checklist.md)
This commit is contained in:
2026-02-10 23:25:14 -08:00
parent 9f0f8a2ce9
commit 781e6dd4fa
16 changed files with 1045 additions and 1295 deletions

View File

@@ -24,8 +24,8 @@ export default {
{ text: 'Docs', link: '/' },
{ text: 'Installation', link: '/installation' },
{ text: 'Usage', link: '/usage' },
{ text: 'Mining', link: '/mining-workflow' },
{ text: 'Configuration', link: '/configuration' },
{ text: 'Development', link: '/development' },
{ text: 'Architecture', link: '/architecture' },
],
sidebar: [
@@ -35,12 +35,21 @@ export default {
{ text: 'Overview', link: '/' },
{ text: 'Installation', link: '/installation' },
{ text: 'Usage', link: '/usage' },
{ text: 'Mining Workflow', link: '/mining-workflow' },
],
},
{
text: 'Reference',
items: [
{ text: 'Configuration', link: '/configuration' },
{ text: 'Anki Integration', link: '/anki-integration' },
{ text: 'MPV Plugin', link: '/mpv-plugin' },
{ text: 'Troubleshooting', link: '/troubleshooting' },
],
},
{
text: 'Development',
items: [
{ text: 'Development', link: '/development' },
{ text: 'Architecture', link: '/architecture' },
],

View File

@@ -1,7 +1,6 @@
import DefaultTheme from 'vitepress/theme';
import { useRoute } from 'vitepress';
import { nextTick, onMounted, watch } from 'vue';
import mermaid from 'mermaid';
import '@catppuccin/vitepress/theme/macchiato/mauve.css';
import './mermaid-modal.css';
@@ -107,7 +106,8 @@ function attachMermaidInteractions(nodes: HTMLElement[]) {
async function getMermaid() {
if (!mermaidLoader) {
mermaidLoader = Promise.resolve().then(() => {
mermaidLoader = import('mermaid').then((module) => {
const mermaid = module.default;
mermaid.initialize({
startOnLoad: false,
securityLevel: 'loose',

View File

@@ -1,37 +1,23 @@
# Documentation
Use this directory for detailed SubMiner documentation.
SubMiner documentation is built with [VitePress](https://vitepress.dev/).
## Local Docs Site
Run the VitePress docs site locally:
```bash
pnpm run docs:dev
make docs-dev # Dev server at http://localhost:5173
make docs # Build static output
make docs-preview # Preview built site at http://localhost:4173
```
Build static docs output:
## Pages
```bash
pnpm run docs:build
```
- [Installation](/installation)
- Platform requirements
- AppImage / macOS / source installs
- mpv plugin setup
- [Usage](/usage)
- Script vs plugin workflow
- Running SubMiner with mpv
- Keybindings and runtime behavior
- [Configuration](/configuration)
- Full config file reference and option details
- [Development](/development)
- Contributor notes
- Architecture and extension rules
- Environment variables
- License and acknowledgments
- [Architecture](/architecture)
- Service-oriented runtime structure
- Composition and lifecycle model
- Extension design rules
- [Installation](/installation) — Platform requirements, AppImage/macOS/source installs, mpv plugin
- [Usage](/usage) — Script vs plugin workflow, keybindings, YouTube playback
- [Mining Workflow](/mining-workflow) — End-to-end sentence mining guide, overlay layers, card creation
- [Configuration](/configuration) — Full config file reference and option details
- [Anki Integration](/anki-integration) — AnkiConnect setup, field mapping, media generation, field grouping
- [MPV Plugin](/mpv-plugin) — Chord keybindings, subminer.conf options, script messages
- [Troubleshooting](/troubleshooting) — Common issues and solutions by category
- [Development](/development) — Building, testing, contributing, environment variables
- [Architecture](/architecture) — Service-oriented design, composition model, extension rules

262
docs/anki-integration.md Normal file
View File

@@ -0,0 +1,262 @@
# 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.
## Prerequisites
1. Install [Anki](https://apps.ankiweb.net/).
2. Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on (code: `2055492159`).
3. Keep Anki running while using SubMiner.
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.
## How Polling Works
SubMiner polls AnkiConnect at a regular interval (default: 3 seconds, configurable via `ankiConnect.pollingRate`) to detect new cards. When it finds a card that was added since the last poll:
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.
Polling uses the query `"deck:<your-deck>" added:1` to find recently added cards. If no deck is configured, it searches all decks.
## Field Mapping
SubMiner maps its data to your Anki note fields. Configure these under `ankiConnect.fields`:
```jsonc
"ankiConnect": {
"fields": {
"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
}
}
```
Field names must match your Anki note type exactly (case-sensitive). If a configured field does not exist on the note type, SubMiner skips it without error.
### Minimal Config
If you only want sentence and audio on your cards:
```jsonc
"ankiConnect": {
"enabled": true,
"fields": {
"sentence": "Sentence",
"audio": "ExpressionAudio"
}
}
```
## Media Generation
SubMiner uses FFmpeg to generate audio and image media from the video. FFmpeg must be installed and on `PATH`.
### Audio
Audio is extracted from the video file using the subtitle's start and end timestamps, with configurable padding added before and after.
```jsonc
"ankiConnect": {
"media": {
"generateAudio": true,
"audioPadding": 0.5, // seconds before and after subtitle timing
"maxMediaDuration": 30 // cap total duration in seconds
}
}
```
Output format: MP3 at 44100 Hz. If the video has multiple audio streams, SubMiner uses the active stream.
The audio is uploaded to Anki's media folder and inserted as `[sound:audio_<timestamp>.mp3]`.
### Screenshots (Static)
A single frame is captured at the current playback position.
```jsonc
"ankiConnect": {
"media": {
"generateImage": true,
"imageType": "static",
"imageFormat": "jpg", // "jpg", "png", or "webp"
"imageQuality": 92, // 1100
"imageMaxWidth": null, // optional, preserves aspect ratio
"imageMaxHeight": null
}
}
```
### Animated Clips (AVIF)
Instead of a static screenshot, SubMiner can generate an animated AVIF covering the subtitle duration.
```jsonc
"ankiConnect": {
"media": {
"generateImage": true,
"imageType": "avif",
"animatedFps": 10,
"animatedMaxWidth": 640,
"animatedMaxHeight": null,
"animatedCrf": 35 // 063, lower = better quality
}
}
```
Animated AVIF requires an AV1 encoder (`libaom-av1`, `libsvtav1`, or `librav1e`) in your FFmpeg build. Generation timeout is 60 seconds.
### Behavior Options
```jsonc
"ankiConnect": {
"behavior": {
"overwriteAudio": true, // replace existing audio, or append
"overwriteImage": true, // replace existing image, or append
"mediaInsertMode": "append", // "append" or "prepend" to field content
"autoUpdateNewCards": true, // auto-update when new card detected
"notificationType": "osd" // "osd", "system", "both", or "none"
}
}
```
## AI Translation
SubMiner can auto-translate the mined sentence and fill the translation field. By default, if a secondary subtitle track is available, its text is used. When AI is enabled, SubMiner calls an LLM API instead.
```jsonc
"ankiConnect": {
"ai": {
"enabled": true,
"alwaysUseAiTranslation": false, // true = ignore secondary sub
"apiKey": "sk-...",
"model": "openai/gpt-4o-mini",
"baseUrl": "https://openrouter.ai/api",
"targetLanguage": "English",
"systemPrompt": "You are a translation engine. Return only the translation."
}
}
```
Translation priority:
1. If `alwaysUseAiTranslation` is `true`, always call the AI API.
2. If a secondary subtitle is available, use it as the translation.
3. If AI is enabled and no secondary subtitle exists, call the AI API.
4. Otherwise, leave the field empty.
## 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.
```jsonc
"ankiConnect": {
"isLapis": {
"enabled": true,
"sentenceCardModel": "Japanese sentences",
"sentenceCardSentenceField": "Sentence",
"sentenceCardAudioField": "SentenceAudio"
}
}
```
Trigger with the mine sentence shortcut (`Ctrl/Cmd+S` by default). The card is created directly via AnkiConnect with the sentence, audio, and image filled in.
To mine multiple subtitle lines as one sentence card, use `Ctrl/Cmd+Shift+S` followed by a digit (19) to select how many recent lines to combine.
## Field Grouping (Kiku)
When you mine the same word multiple times, SubMiner can merge the cards instead of creating duplicates. This is designed for note types like [Kiku](https://github.com/donkuri/Kiku) that support grouped sentence/audio/image fields.
```jsonc
"ankiConnect": {
"isKiku": {
"enabled": true,
"fieldGrouping": "manual", // "auto", "manual", or "disabled"
"deleteDuplicateInAuto": true // delete new card after auto-merge
}
}
```
### Modes
**Disabled** (`"disabled"`): No duplicate detection. Each card is independent.
**Auto** (`"auto"`): When a duplicate expression is found, SubMiner merges the new card into the existing one automatically. Both sentences, audio clips, and images are preserved. If `deleteDuplicateInAuto` is true, the new card is deleted after merging.
**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
| Field | Merge behavior |
| --- | --- |
| Sentence | Both sentences preserved, labeled `[Original]` / `[Duplicate]` |
| Audio | Both `[sound:...]` entries kept |
| Image | Both images kept |
### Keyboard Shortcuts in the Modal
| Key | Action |
| --- | --- |
| `1` / `2` | Select card 1 or card 2 to keep |
| `Enter` | Confirm selection |
| `Esc` | Cancel (keep both cards unchanged) |
## Full Config Example
```jsonc
{
"ankiConnect": {
"enabled": true,
"url": "http://127.0.0.1:8765",
"pollingRate": 3000,
"fields": {
"audio": "ExpressionAudio",
"image": "Picture",
"sentence": "Sentence",
"miscInfo": "MiscInfo",
"translation": "SelectionText"
},
"media": {
"generateAudio": true,
"generateImage": true,
"imageType": "static",
"imageFormat": "jpg",
"imageQuality": 92,
"audioPadding": 0.5,
"maxMediaDuration": 30
},
"behavior": {
"overwriteAudio": true,
"overwriteImage": true,
"mediaInsertMode": "append",
"autoUpdateNewCards": true,
"notificationType": "osd"
},
"ai": {
"enabled": false,
"apiKey": "",
"model": "openai/gpt-4o-mini",
"baseUrl": "https://openrouter.ai/api",
"targetLanguage": "English"
},
"isKiku": {
"enabled": false,
"fieldGrouping": "disabled",
"deleteDuplicateInAuto": true
},
"isLapis": {
"enabled": false,
"sentenceCardModel": "Japanese sentences",
"sentenceCardSentenceField": "Sentence",
"sentenceCardAudioField": "SentenceAudio"
}
}
}
```

View File

@@ -1,6 +1,6 @@
# Architecture
SubMiner uses a service-oriented Electron main-process architecture where `src/main.ts` acts as the composition root and behavior lives in small runtime services under `src/core/services`.
SubMiner uses a service-oriented Electron main-process architecture where `src/main.ts` (~1,400 lines) acts as the composition root and behavior lives in focused services under `src/core/services/` (~35 service files).
## Goals
@@ -12,54 +12,102 @@ SubMiner uses a service-oriented Electron main-process architecture where `src/m
- services compose through explicit inputs/outputs
- orchestration is separate from implementation
## Current Structure
## Project Structure
- `src/main.ts`
- Composition root for lifecycle wiring and non-overlay runtime state.
- Owns long-lived process state for trackers, runtime flags, and client instances.
- Delegates behavior to services.
- `src/core/services/overlay-manager-service.ts`
- Owns overlay/window state (`mainWindow`, `invisibleWindow`, visible/invisible overlay flags).
- Provides a narrow state API used by `main.ts` and overlay services.
- `src/core/services/*`
- Stateless or narrowly stateful units for a specific responsibility.
- Examples: startup bootstrap/ready flow, app lifecycle wiring, CLI command handling, IPC registration, overlay visibility, MPV IPC behavior, shortcut registration, subtitle websocket, jimaku/subsync helpers.
- `src/core/utils/*`
- Pure helpers and coercion/config utilities.
- `src/cli/*`
- CLI parsing and help output.
- `src/config/*`
- Config schema/definitions, defaults, validation, and template generation.
- `src/window-trackers/*`
- Backend-specific tracker implementations plus selection index.
- `src/jimaku/*`, `src/subsync/*`
- Domain-specific integration helpers.
```text
src/
main.ts # Composition root — lifecycle wiring and state ownership
preload.ts # Electron preload bridge
types.ts # Shared type definitions
core/
services/ # ~35 focused service modules (see below)
utils/ # Pure helpers and coercion/config utilities
cli/ # CLI parsing and help output
config/ # Config schema, defaults, validation, template generation
renderer/ # Overlay renderer (HTML/CSS/JS)
window-trackers/ # Backend-specific tracker implementations (Hyprland, X11, macOS)
jimaku/ # Jimaku API integration helpers
subsync/ # Subtitle sync (alass/ffsubsync) helpers
subtitle/ # Subtitle processing utilities
tokenizers/ # Tokenizer implementations
token-mergers/ # Token merge strategies
translators/ # AI translation providers
```
### Service Layer (`src/core/services/`)
- **Startup** — `startup-service`, `app-lifecycle-service`
- **Overlay** — `overlay-manager-service`, `overlay-window-service`, `overlay-visibility-service`, `overlay-bridge-service`, `overlay-runtime-init-service`
- **Shortcuts** — `shortcut-service`, `overlay-shortcut-service`, `overlay-shortcut-handler`, `shortcut-fallback-service`, `numeric-shortcut-service`
- **MPV** — `mpv-service`, `mpv-control-service`, `mpv-render-metrics-service`
- **IPC** — `ipc-service`, `ipc-command-service`, `runtime-options-ipc-service`
- **Mining** — `mining-service`, `field-grouping-service`, `field-grouping-overlay-service`, `anki-jimaku-service`, `anki-jimaku-ipc-service`
- **Subtitles** — `subtitle-ws-service`, `subtitle-position-service`, `secondary-subtitle-service`, `tokenizer-service`
- **Integrations** — `jimaku-service`, `subsync-service`, `subsync-runner-service`, `texthooker-service`, `yomitan-extension-loader-service`, `yomitan-settings-service`
- **Config** — `runtime-config-service`, `cli-command-service`
## Flow Diagram
```mermaid
flowchart TD
Main["src/main.ts\n(composition root)"] --> Startup["runStartupBootstrapRuntimeService"]
Main --> Lifecycle["startAppLifecycleService"]
Lifecycle --> AppReady["runAppReadyRuntimeService"]
classDef root fill:#1f2937,stroke:#111827,color:#f9fafb,stroke-width:1.5px;
classDef orchestration fill:#334155,stroke:#0f172a,color:#e2e8f0;
classDef domain fill:#1d4ed8,stroke:#1e3a8a,color:#dbeafe;
classDef boundary fill:#065f46,stroke:#064e3b,color:#d1fae5;
Main --> OverlayMgr["overlay-manager-service"]
Main --> Ipc["ipc-service / ipc-command-service"]
Main --> Mpv["mpv-service / mpv-control-service"]
Main --> Shortcuts["shortcut-service / overlay-shortcut-service"]
Main --> RuntimeOpts["runtime-options-ipc-service"]
Main --> Subtitle["subtitle-ws-service / secondary-subtitle-service"]
subgraph Entry["Entrypoint"]
Main["src/main.ts\ncomposition root"]
end
class Main root;
Main --> Config["src/config/*"]
Main --> Cli["src/cli/*"]
Main --> Trackers["src/window-trackers/*"]
Main --> Integrations["src/jimaku/* + src/subsync/*"]
subgraph Boot["Startup Orchestration"]
Startup["startup-service"]
Lifecycle["app-lifecycle-service"]
AppReady["app-ready flow"]
end
class Startup,Lifecycle,AppReady orchestration;
OverlayMgr --> OverlayWindow["overlay-window-service"]
OverlayMgr --> OverlayVisibility["overlay-visibility-service"]
Mpv --> Subtitle
Ipc --> RuntimeOpts
Shortcuts --> OverlayMgr
subgraph Runtime["Runtime Domains"]
OverlayMgr["overlay-manager-service"]
OverlayWindow["overlay-window-service"]
OverlayVisibility["overlay-visibility-service"]
Ipc["ipc-service\nipc-command-service"]
RuntimeOpts["runtime-options-ipc-service"]
Mpv["mpv-service\nmpv-control-service"]
Subtitle["subtitle-ws-service\nsecondary-subtitle-service"]
Shortcuts["shortcut-service\noverlay-shortcut-service"]
end
class OverlayMgr,OverlayWindow,OverlayVisibility,Ipc,RuntimeOpts,Mpv,Subtitle,Shortcuts domain;
subgraph Adapters["External Boundaries"]
Config["src/config/*"]
Cli["src/cli/*"]
Trackers["src/window-trackers/*"]
Integrations["src/jimaku/*\nsrc/subsync/*"]
end
class Config,Cli,Trackers,Integrations boundary;
Main -->|bootstraps| Startup
Main -->|registers lifecycle hooks| Lifecycle
Lifecycle -->|triggers| AppReady
Main -->|wires| OverlayMgr
Main -->|wires| Ipc
Main -->|wires| Mpv
Main -->|wires| Shortcuts
Main -->|wires| RuntimeOpts
Main -->|wires| Subtitle
Main -->|loads| Config
Main -->|parses| Cli
Main -->|delegates backend state| Trackers
Main -->|calls integrations| Integrations
OverlayMgr -->|creates window| OverlayWindow
OverlayMgr -->|applies visibility policy| OverlayVisibility
Ipc -->|updates| RuntimeOpts
Mpv -->|feeds timing + subtitle context| Subtitle
Shortcuts -->|drives overlay actions| OverlayMgr
```
## Composition Pattern
@@ -75,36 +123,54 @@ This keeps side effects explicit and makes behavior easy to unit-test with fakes
## Lifecycle Model
- Startup:
- `runStartupBootstrapRuntimeService` handles initial argv/env/backend setup and decides generate-config flow vs app lifecycle start.
- **Startup:**
- `startup-service` handles initial argv/env/backend setup and decides generate-config flow vs app lifecycle start.
- `app-lifecycle-service` handles Electron single-instance + lifecycle event registration.
- `runAppReadyRuntimeService` performs ready-time initialization (config load, websocket policy, tokenizer/tracker setup, overlay auto-init decisions).
- Runtime:
- App-ready flow performs ready-time initialization (config load, websocket policy, tokenizer/tracker setup, overlay auto-init decisions).
- **Runtime:**
- CLI/shortcut/IPC events map to service calls.
- Overlay and MPV state sync through dedicated services.
- Runtime options and mining flows are coordinated via service boundaries.
- Shutdown:
- `startAppLifecycleService` registers cleanup hooks (`will-quit`) while teardown behavior stays delegated to focused services from `main.ts`.
- **Shutdown:**
- `app-lifecycle-service` registers cleanup hooks (`will-quit`) while teardown behavior stays delegated to focused services from `main.ts`.
```mermaid
flowchart LR
Args["CLI args"] --> Bootstrap["runStartupBootstrapRuntimeService"]
Bootstrap -->|generate-config| Exit["exit"]
Bootstrap -->|normal start| AppLifecycle["startAppLifecycleService"]
AppLifecycle --> Ready["runAppReadyRuntimeService"]
Ready --> Runtime["IPC + shortcuts + mpv events"]
Runtime --> Overlay["overlay visibility + mining actions"]
Runtime --> Subsync["subsync + secondary sub flows"]
Runtime --> WillQuit["app will-quit"]
WillQuit --> Cleanup["service-level cleanup + unregister"]
flowchart TD
classDef phase fill:#334155,stroke:#0f172a,color:#e2e8f0;
classDef decision fill:#7c2d12,stroke:#431407,color:#ffedd5;
classDef runtime fill:#0369a1,stroke:#0c4a6e,color:#e0f2fe;
classDef shutdown fill:#14532d,stroke:#052e16,color:#dcfce7;
Args["CLI args / env"] --> Startup["startup-service"]
class Args,Startup phase;
Startup --> Decision{"generate-config?"}
class Decision decision;
Decision -->|yes| WriteConfig["write config + exit"]
Decision -->|no| Lifecycle["app-lifecycle-service"]
class WriteConfig,Lifecycle phase;
Lifecycle --> Ready["app-ready flow\n(config + websocket policy + tracker/tokenizer init)"]
class Ready phase;
Ready --> RuntimeBus["event loop:\nIPC + shortcuts + mpv events"]
RuntimeBus --> Overlay["overlay visibility + mining actions"]
RuntimeBus --> Subtitle["subtitle + secondary-subtitle processing"]
RuntimeBus --> Subsync["subsync / jimaku integration actions"]
class RuntimeBus,Overlay,Subtitle,Subsync runtime;
RuntimeBus --> WillQuit["Electron will-quit"]
WillQuit --> Cleanup["service-level teardown\n(unregister hooks, close resources)"]
class WillQuit,Cleanup shutdown;
```
## Why This Design
- Smaller blast radius: changing one feature usually touches one service.
- Better testability: most behavior can be tested without Electron windows/mpv.
- Better reviewability: PRs can be scoped to one subsystem.
- Backward compatibility: CLI flags and IPC channels can remain stable while internals evolve.
- **Smaller blast radius:** changing one feature usually touches one service.
- **Better testability:** most behavior can be tested without Electron windows/mpv.
- **Better reviewability:** PRs can be scoped to one subsystem.
- **Backward compatibility:** CLI flags and IPC channels can remain stable while internals evolve.
## Extension Rules

View File

@@ -1,20 +1,99 @@
# Contributor Note
# Development
To add or change a config option, update `src/config/definitions.ts` first. Defaults, runtime-option metadata, and generated `config.example.jsonc` are derived from this centralized source.
## Prerequisites
## Architecture
- [Node.js](https://nodejs.org/) (LTS)
- [pnpm](https://pnpm.io/)
- [Bun](https://bun.sh) (for the `subminer` wrapper script)
The current runtime design, composition model, and extension guidelines are documented in [`architecture.md`](/architecture).
## Setup
Contributor guidance:
```bash
git clone https://github.com/ksyasuda/SubMiner.git
cd SubMiner
make deps
# or manually:
pnpm install
pnpm -C vendor/texthooker-ui install
```
## Building
```bash
# TypeScript compile (fast, for development)
pnpm run build
# Full platform build (includes texthooker-ui + AppImage/DMG)
make build
# Platform-specific builds
make build-linux # Linux AppImage
make build-macos # macOS DMG + ZIP (signed)
make build-macos-unsigned # macOS DMG + ZIP (unsigned)
```
## Running Locally
```bash
pnpm run dev # builds + launches with --start --dev flags
```
## Testing
```bash
pnpm run test:config # Config schema and validation tests
pnpm run test:core # Core service tests (~67 tests)
pnpm run test:subtitle # Subtitle pipeline tests
```
All test commands build first, then run via Node's built-in test runner (`node --test`).
## Config Generation
```bash
# Generate default config to ~/.config/SubMiner/config.jsonc
make generate-config
# Regenerate the repo's config.example.jsonc from centralized defaults
make generate-example-config
# or: pnpm run generate:config-example
```
## Documentation Site
The docs use [VitePress](https://vitepress.dev/):
```bash
make docs-dev # Dev server at http://localhost:5173
make docs # Build static output
make docs-preview # Preview built site at http://localhost:4173
```
## Makefile Reference
Run `make help` for a full list of targets. Key ones:
| Target | Description |
| --- | --- |
| `make build` | Build platform package for detected OS |
| `make install` | Install platform artifacts (wrapper, theme, AppImage/app bundle) |
| `make install-plugin` | Install mpv Lua plugin and config |
| `make deps` | Install JS dependencies (root + texthooker-ui) |
| `make generate-config` | Generate default config from centralized registry |
| `make docs-dev` | Run VitePress dev server |
## Contributor Notes
- To add or change a config option, update `src/config/definitions.ts` first. Defaults, runtime-option metadata, and generated `config.example.jsonc` are derived from this centralized source.
- Overlay window/visibility state is owned by `src/core/services/overlay-manager-service.ts`.
- Prefer direct inline deps objects in `main.ts` for simple pass-through wiring.
- Add a helper/adapter service only when it performs meaningful adaptation, validation, or reuse (not identity mapping).
- See [Architecture](/architecture) for the composition model and extension rules.
## Environment Variables
| Variable | Description |
| ------------------------ | ---------------------------------------------- |
| Variable | Description |
| --- | --- |
| `SUBMINER_APPIMAGE_PATH` | Override AppImage location for subminer script |
| `SUBMINER_YT_SUBGEN_MODE` | Override `youtubeSubgen.mode` for launcher |
| `SUBMINER_WHISPER_BIN` | Override `youtubeSubgen.whisperBin` for launcher |

View File

@@ -15,15 +15,15 @@ hero:
- theme: brand
text: Installation
link: /installation
- theme: alt
text: Mining Workflow
link: /mining-workflow
- theme: alt
text: Configuration
link: /configuration
- theme: alt
text: Development
link: /development
- theme: alt
text: Architecture
link: /architecture
text: Troubleshooting
link: /troubleshooting
features:
- title: End-to-end workflow
@@ -33,11 +33,3 @@ features:
- title: Contributor docs
details: Build, test, and package SubMiner with the development notes in this docs set.
---
<!-- ## Documentation Sections
- [Installation](/installation)
- [Usage](/usage)
- [Configuration](/configuration)
- [Development](/development)
- [Architecture](/architecture) -->

153
docs/mining-workflow.md Normal file
View File

@@ -0,0 +1,153 @@
# Mining Workflow
This guide walks through the sentence mining loop — from watching a video to creating Anki cards with audio, screenshots, and context.
## 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.
```text
Watch video → See subtitle → Click word → Yomitan lookup → Add to Anki
SubMiner auto-fills:
sentence, audio, image, translation
```
## The Two Overlay Layers
SubMiner uses two overlay layers, each serving a different purpose.
### Visible Overlay
The visible overlay renders subtitles as tokenized, clickable word spans. Each word is a separate element with reading and headword data attached. This layer is styled independently from mpv subtitles and supports:
- Word-level click targets for Yomitan lookup
- Right-click to pause/resume
- Right-click + drag to reposition subtitles
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
Toggle with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
### Invisible Overlay
The invisible overlay is a transparent layer that aligns precisely with mpv's own subtitle rendering. It reproduces the subtitle text at the exact position and size mpv uses, so you can click directly on the subtitles you see in the video.
This layer uses mpv's subtitle render metrics (font size, margins, position, scaling) and converts them from mpv's scaled-pixel system (reference height 720) to actual screen pixels.
Toggle with `Alt+Shift+I` (global) or `y-i` (mpv plugin).
**Position edit mode**: Press `Ctrl/Cmd+Shift+P` to enter edit mode, then use arrow keys (or `hjkl`) to nudge the position. `Shift` moves 4 px at a time. Press `Enter` or `Ctrl+S` to save, `Esc` to cancel.
## Looking Up Words
### On the Visible Overlay
1. Hover over the subtitle area — the overlay activates pointer events.
2. Click a word. SubMiner selects it using Unicode-aware word boundary detection (`Intl.Segmenter`).
3. Yomitan detects the text selection and opens its popup with dictionary results.
4. From the Yomitan popup, you can add the word directly to Anki.
### On the Invisible Overlay
1. The invisible layer sits over mpv's own subtitle text.
2. Click on any word in the subtitle — SubMiner maps your click position to the underlying text.
3. On macOS, word selection happens automatically on hover.
4. Yomitan popup appears for lookup and card creation.
## Creating Anki Cards
There are three ways to create cards, depending on your workflow.
### 1. Auto-Update from Yomitan
This is the most common flow. Yomitan creates a card in Anki, and SubMiner detects it via polling and enriches it automatically.
1. Click a word → Yomitan popup appears.
2. Click the Anki icon in Yomitan to add the word.
3. SubMiner detects the new card (polls AnkiConnect every 3 seconds by default).
4. SubMiner updates the card with:
- **Sentence**: The current subtitle line.
- **Audio**: Extracted from the video using the subtitle's start/end timing (plus configurable padding).
- **Image**: A screenshot or animated clip from the current playback position.
- **Translation**: From the secondary subtitle track, or generated via AI if configured.
- **MiscInfo**: Metadata like filename and timestamp.
Configure which fields to fill in `ankiConnect.fields`. See [Anki Integration](/anki-integration) for details.
### 2. Mine Sentence (Hotkey)
Create a standalone sentence card without going through Yomitan:
- **Mine current sentence**: `Ctrl/Cmd+S` (configurable via `shortcuts.mineSentence`)
- **Mine multiple lines**: `Ctrl/Cmd+Shift+S` followed by a digit 19 to select how many recent subtitle lines to combine.
The sentence card uses the note type configured in `isLapis.sentenceCardModel` with the sentence and audio fields mapped by `isLapis.sentenceCardSentenceField` and `isLapis.sentenceCardAudioField`.
### 3. Mark as Audio Card
After adding a word via Yomitan, press the audio card shortcut to overwrite the audio with a longer clip spanning the full subtitle timing.
## Secondary Subtitles
SubMiner can display a secondary subtitle track (typically English) alongside the primary Japanese subtitles. This is useful for:
- Quick comprehension checks without leaving the mining flow.
- Auto-populating the translation field on mined cards.
### Display Modes
Cycle through modes with the configured shortcut:
- **Hidden**: Secondary subtitle not shown.
- **Visible**: Always displayed below the primary subtitle.
- **Hover**: Only shown when you hover over the primary subtitle.
When a card is created, SubMiner uses the secondary subtitle text as the translation field value (unless AI translation is configured to override it).
## Field Grouping (Kiku)
If you mine the same word from different sentences, SubMiner can merge the cards instead of creating duplicates. This feature is designed for use with [Kiku](https://github.com/donkuri/Kiku) and similar note types that support grouped fields.
### How It Works
1. You add a word via Yomitan.
2. SubMiner detects the new card and checks if a card with the same expression already exists.
3. If a duplicate is found:
- **Auto mode** (`fieldGrouping: "auto"`): Merges automatically. Both sentences, audio clips, and images are combined into the existing card. The duplicate is optionally deleted.
- **Manual mode** (`fieldGrouping: "manual"`): A modal appears showing both cards side by side. You choose which card to keep and preview the merged result before confirming.
### What Gets Merged
- **Sentence fields**: Both sentences kept, marked with `[Original]` and `[Duplicate]`.
- **Audio fields**: Both audio clips preserved as separate `[sound:...]` entries.
- **Image fields**: Both images preserved.
Configure in `ankiConnect.isKiku`. See [Anki Integration](/anki-integration#field-grouping-kiku) for the full reference.
## Jimaku Subtitle Search
SubMiner integrates with [Jimaku](https://jimaku.cc) to search and download subtitle files for anime directly from the overlay.
1. Open the Jimaku modal via the configured shortcut (`Ctrl+Alt+J` by default).
2. SubMiner auto-fills the search from the current video filename (title, season, episode).
3. Browse matching entries and select a subtitle file to download.
4. The subtitle is loaded into mpv as a new track.
Requires an internet connection. An API key (`jimaku.apiKey`) is optional but recommended for higher rate limits.
## Texthooker
SubMiner runs a local HTTP server at `http://127.0.0.1:5174` (configurable port) that serves a texthooker UI. This allows external tools — such as a browser-based Yomitan instance — to receive subtitle text in real time.
The texthooker page displays the current subtitle and updates as new lines arrive. This is useful if you prefer to do lookups in a browser rather than through the overlay's built-in Yomitan.
## Subtitle Sync (Subsync)
If your subtitle file is out of sync with the audio, SubMiner can resynchronize it using [alass](https://github.com/kaegi/alass) or [ffsubsync](https://github.com/smacke/ffsubsync).
1. Open the subsync modal from the overlay.
2. Select the sync engine (alass or ffsubsync).
3. For alass, select a reference subtitle track from the video.
4. SubMiner runs the sync and reloads the corrected subtitle.
Install the sync tools separately — see [Troubleshooting](/troubleshooting#subtitle-sync-subsync) if the tools are not found.

174
docs/mpv-plugin.md Normal file
View File

@@ -0,0 +1,174 @@
# MPV Plugin
The SubMiner mpv plugin (`subminer.lua`) provides in-player keybindings to control the overlay without leaving mpv. It communicates with SubMiner by invoking the AppImage (or binary) with CLI flags.
## Installation
```bash
cp plugin/subminer.lua ~/.config/mpv/scripts/
cp plugin/subminer.conf ~/.config/mpv/script-opts/
# or: make install-plugin
```
mpv must have IPC enabled for SubMiner to connect:
```ini
# ~/.config/mpv/mpv.conf
input-ipc-server=/tmp/subminer-socket
```
## Keybindings
All keybindings use a `y` chord prefix — press `y`, then the second key:
| Chord | Action |
| --- | --- |
| `y-y` | Open menu |
| `y-s` | Start overlay |
| `y-S` | Stop overlay |
| `y-t` | Toggle visible overlay |
| `y-i` | Toggle invisible overlay |
| `y-I` | Show invisible overlay |
| `y-u` | Hide invisible overlay |
| `y-o` | Open settings window |
| `y-r` | Restart overlay |
| `y-c` | Check status |
## Menu
Press `y-y` to open an interactive menu in mpv's OSD:
```text
SubMiner:
1. Start overlay
2. Stop overlay
3. Toggle overlay
4. Toggle invisible overlay
5. Open options
6. Restart overlay
7. Check status
```
Select an item by pressing its number.
## Configuration
Create or edit `~/.config/mpv/script-opts/subminer.conf`:
```ini
# Path to SubMiner binary. Leave empty for auto-detection.
binary_path=
# MPV IPC socket path. Must match input-ipc-server in mpv.conf.
socket_path=/tmp/subminer-socket
# Enable the texthooker WebSocket server.
texthooker_enabled=yes
# Port for the texthooker server.
texthooker_port=5174
# Window manager backend: auto, hyprland, sway, x11, macos.
backend=auto
# Start the overlay automatically when a file is loaded.
auto_start=no
# Show the visible overlay on auto-start.
auto_start_visible_overlay=no
# Invisible overlay startup: platform-default, visible, hidden.
# platform-default = hidden on Linux, visible on macOS/Windows.
auto_start_invisible_overlay=platform-default
# Show OSD messages for overlay status changes.
osd_messages=yes
# Logging level: debug, info, warn, error.
log_level=info
```
### Option Reference
| Option | Default | Values | Description |
| --- | --- | --- | --- |
| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary |
| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path |
| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server |
| `texthooker_port` | `5174` | 165535 | Texthooker server port |
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
| `auto_start` | `no` | `yes` / `no` | Auto-start overlay on file load |
| `auto_start_visible_overlay` | `no` | `yes` / `no` | Show visible layer on auto-start |
| `auto_start_invisible_overlay` | `platform-default` | `platform-default`, `visible`, `hidden` | Invisible layer on auto-start |
| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
## Binary Auto-Detection
When `binary_path` is empty, the plugin searches platform-specific locations:
**Linux:**
1. `~/.local/bin/SubMiner.AppImage`
2. `/opt/SubMiner/SubMiner.AppImage`
3. `/usr/local/bin/SubMiner`
4. `/usr/bin/SubMiner`
**macOS:**
1. `/Applications/SubMiner.app/Contents/MacOS/SubMiner`
2. `~/Applications/SubMiner.app/Contents/MacOS/SubMiner`
**Windows:**
1. `C:\Program Files\SubMiner\SubMiner.exe`
2. `C:\Program Files (x86)\SubMiner\SubMiner.exe`
3. `C:\SubMiner\SubMiner.exe`
## Backend Detection
When `backend=auto`, the plugin detects the window manager:
1. **macOS** — detected via platform or `OSTYPE`.
2. **Hyprland** — detected via `HYPRLAND_INSTANCE_SIGNATURE`.
3. **Sway** — detected via `SWAYSOCK`.
4. **X11** — detected via `XDG_SESSION_TYPE=x11` or `DISPLAY`.
5. **Fallback** — defaults to X11 with a warning.
## Script Messages
The plugin can be controlled from other mpv scripts or the mpv command line using script messages:
```
script-message subminer-start
script-message subminer-stop
script-message subminer-toggle
script-message subminer-toggle-invisible
script-message subminer-show-invisible
script-message subminer-hide-invisible
script-message subminer-menu
script-message subminer-options
script-message subminer-restart
script-message subminer-status
```
The `subminer-start` message accepts overrides:
```
script-message subminer-start backend=hyprland socket=/custom/path texthooker=no log-level=debug
```
## Lifecycle
- **File loaded**: If `auto_start=yes`, the plugin starts the overlay and applies visibility preferences after a short delay.
- **MPV shutdown**: The plugin sends a stop command to gracefully shut down both the overlay and the texthooker server.
- **Texthooker**: Starts as a separate subprocess before the overlay to ensure the app lock is acquired first.
## Using with the `subminer` Wrapper
The `subminer` wrapper script handles mpv launch, socket setup, and overlay lifecycle automatically. You do not need the plugin if you always use the wrapper.
The plugin is useful when you:
- Launch mpv from other tools (file managers, media centers).
- Want on-demand overlay control without the wrapper.
- Use mpv's built-in file browser or playlist features.
You can install both — the plugin provides chord keybindings for convenience, while the wrapper handles the full lifecycle.

View File

@@ -1,46 +0,0 @@
# Main.ts Refactor Checklist
This checklist is the safety net for `src/main.ts` decomposition work.
## Invariants (Do Not Break)
- Keep all existing CLI flags and aliases working.
- Keep IPC channel names and payload shapes backward-compatible.
- Preserve overlay behavior:
- visible overlay toggles and follows expected state
- invisible overlay toggles and mouse passthrough behavior
- Preserve MPV integration behavior:
- connect/disconnect flows
- subtitle updates and overlay updates
- Preserve texthooker mode (`--texthooker`) and subtitle websocket behavior.
- Preserve mining/runtime options actions and trigger paths.
## Per-PR Required Automated Checks
- `pnpm run build`
- `pnpm run test:config`
- `pnpm run test:core`
- Current line gate script for milestone:
- Example Gate 1: `pnpm run check:main-lines:gate1`
## Per-PR Manual Smoke Checks
- CLI:
- `electron . --help` output is valid
- `--start`, `--stop`, `--toggle` still route correctly
- Overlay:
- visible overlay show/hide/toggle works
- invisible overlay show/hide/toggle works
- Subtitle behavior:
- subtitle updates still render
- copy/mine shortcuts still function
- Integration:
- runtime options palette opens
- texthooker mode serves UI and can be opened
## Extraction Rules
- Move code verbatim first, refactor internals second.
- Keep temporary adapters/shims in `main.ts` until parity is verified.
- Limit each PR to one subsystem/risk area.
- If a regression appears, revert only that extraction slice and keep prior working structure.

189
docs/troubleshooting.md Normal file
View File

@@ -0,0 +1,189 @@
# Troubleshooting
Common issues and how to resolve them.
## MPV Connection
**Overlay starts but shows no subtitles**
SubMiner connects to mpv via a Unix socket (or named pipe on Windows). If the socket does not exist or the path does not match, the overlay will appear but subtitles will never arrive.
- Ensure mpv is running with `--input-ipc-server=/tmp/subminer-socket`.
- If you use a custom socket path, set it in both your mpv config and SubMiner config (`mpvSocketPath`).
- The `subminer` wrapper script sets the socket automatically when it launches mpv. If you launch mpv yourself, the `--input-ipc-server` flag is required.
SubMiner retries the connection automatically with increasing delays (200 ms, 500 ms, 1 s, 2 s on first connect; 1 s, 2 s, 5 s, 10 s on reconnect). If mpv exits and restarts, the overlay reconnects without needing a restart.
**"Failed to parse MPV message"**
Logged when a malformed JSON line arrives from the mpv socket. Usually harmless — SubMiner skips the bad line and continues. If it happens constantly, check that nothing else is writing to the same socket path.
## AnkiConnect
**"AnkiConnect: unable to connect"**
SubMiner polls AnkiConnect at `http://127.0.0.1:8765` (configurable via `ankiConnect.url`). This error means Anki is not running or the AnkiConnect add-on is not installed.
- Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on in Anki.
- Make sure Anki is running before you start mining.
- If you changed the AnkiConnect port, update `ankiConnect.url` in your config.
SubMiner retries with exponential backoff (up to 5 s) and suppresses repeated error logs after 5 consecutive failures. When Anki comes back, you will see "AnkiConnect connection restored".
**Cards are created but fields are empty**
Field names in your config must match your Anki note type exactly (case-sensitive). Check `ankiConnect.fields` — for example, if your note type uses `SentenceAudio` but your config says `Audio`, the field will not be populated.
See [Anki Integration](/anki-integration) for the full field mapping reference.
**"Update failed" OSD message**
Shown when SubMiner tries to update a card that no longer exists, or when AnkiConnect rejects the update. Common causes:
- The card was deleted in Anki between polling and update.
- The note type changed and a mapped field no longer exists.
## Overlay
**Overlay does not appear**
- Confirm SubMiner is running: `SubMiner.AppImage --start` or check for the process.
- On Linux, the overlay requires a compositor. Hyprland and Sway are supported natively; X11 requires `xdotool` and `xwininfo`.
- On macOS, grant Accessibility permission to SubMiner in System Settings > Privacy & Security > Accessibility.
**Overlay appears but clicks pass through / cannot interact**
- On Linux, mouse passthrough can be unreliable — this is a known Electron/platform limitation. The overlay keeps pointer events enabled by default on Linux.
- On macOS/Windows, `setIgnoreMouseEvents` toggles automatically. If clicks stop working, toggle the overlay off and back on (`Alt+Shift+O`).
- Make sure you are hovering over the subtitle area — the overlay only becomes interactive when the cursor is over subtitle text.
**Overlay is on the wrong monitor or position**
SubMiner positions the overlay by tracking the mpv window. If tracking fails:
- Hyprland: Ensure `hyprctl` is available.
- Sway: Ensure `swaymsg` is available.
- X11: Ensure `xdotool` and `xwininfo` are installed.
If the overlay position is slightly off, use invisible subtitle position edit mode (`Ctrl/Cmd+Shift+P`) to fine-tune the offset with arrow keys, then save with `Enter` or `Ctrl+S`.
## Yomitan
**"Yomitan extension not found in any search path"**
SubMiner bundles Yomitan and searches for it in these locations (in order):
1. `vendor/yomitan` (relative to executable)
2. `<resources>/yomitan` (Electron resources path)
3. `/usr/share/SubMiner/yomitan`
4. `~/.config/SubMiner/extensions/yomitan`
If you installed from the AppImage and see this error, the package may be incomplete. Re-download the AppImage or place the Yomitan extension manually in `~/.config/SubMiner/extensions/yomitan`.
**Yomitan popup does not appear when clicking words**
- 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 the overlay shows subtitles but words are not clickable, the tokenizer may have failed. See the MeCab section below.
## MeCab / Tokenization
**"MeCab not found on system"**
This is informational, not an error. SubMiner uses Yomitan's internal parser as the primary tokenizer and falls back to MeCab when needed. If MeCab is not installed, Yomitan handles all tokenization.
To install MeCab:
- **Arch Linux**: `sudo pacman -S mecab mecab-ipadic`
- **Ubuntu/Debian**: `sudo apt install mecab libmecab-dev mecab-ipadic-utf8`
- **macOS**: `brew install mecab mecab-ipadic`
**Words are not segmented correctly**
Japanese word boundaries depend on the tokenizer. If segmentation seems wrong:
- Install MeCab for improved accuracy as a fallback.
- Note that CJK characters without spaces are segmented using `Intl.Segmenter` or character-level fallback, which is not always perfect.
## Media Generation
**"FFmpeg not found"**
SubMiner uses FFmpeg to extract audio clips and generate screenshots. Install it:
- **Arch Linux**: `sudo pacman -S ffmpeg`
- **Ubuntu/Debian**: `sudo apt install ffmpeg`
- **macOS**: `brew install ffmpeg`
Without FFmpeg, card creation still works but audio and image fields will be empty.
**Audio or screenshot generation hangs**
Media generation has a 30-second timeout (60 seconds for animated AVIF). If your video file is on a slow network mount or the codec requires software decoding, generation may time out. Try:
- Using a local copy of the video file.
- Reducing `media.imageQuality` or switching from `avif` to `static` image type.
- Checking that `media.maxMediaDuration` is not set too high.
## Shortcuts
**"Failed to register global shortcut"**
Global shortcuts (`Alt+Shift+O`, `Alt+Shift+I`, `Alt+Shift+Y`) may conflict with other applications or desktop environment keybindings.
- Check your DE/WM keybinding settings for conflicts.
- Change the shortcuts in your config under `shortcuts.toggleVisibleOverlayGlobal`, `shortcuts.toggleInvisibleOverlayGlobal`.
- On Wayland, global shortcut registration has limitations depending on the compositor.
**Overlay keybindings not working**
Overlay-local shortcuts (Space, arrow keys, etc.) only work when the overlay window has focus. Click on the overlay or use the global shortcut to toggle it to give it focus.
## Subtitle Timing
**"Subtitle timing not found; copy again while playing"**
This OSD message appears when you try to mine a sentence but SubMiner has no timing data for the current subtitle. Causes:
- The video is paused and no subtitle has been received yet.
- The subtitle track changed and timing data was cleared.
- You are using an external subtitle file that mpv has not fully loaded.
Resume playback and wait for the next subtitle to appear, then try mining again.
## Subtitle Sync (Subsync)
**"Configured alass executable not found"**
Install alass or configure the path:
- **Arch Linux (AUR)**: `yay -S alass-git`
- Set the path: `subsync.alass_path` in your config.
**"Subtitle synchronization failed"**
SubMiner tries alass first, then falls back to ffsubsync. If both fail:
- Ensure the reference subtitle track exists in the video (alass requires a source track).
- Check that `ffmpeg` is available (used to extract the internal subtitle track).
- Try running the sync tool manually to see detailed error output.
## Jimaku
**"Jimaku request failed" or HTTP 429**
The Jimaku API has rate limits. If you see 429 errors, wait for the retry duration shown in the OSD message and try again. If you have a Jimaku API key, set it in `jimaku.apiKey` or `jimaku.apiKeyCommand` to get higher rate limits.
## Platform-Specific
### Linux
- **Wayland (Hyprland/Sway)**: Window tracking uses compositor-specific commands. If `hyprctl` or `swaymsg` are not on `PATH`, tracking will fail silently.
- **X11**: Requires `xdotool` and `xwininfo`. If missing, the overlay cannot track the mpv window position.
- **Mouse passthrough**: On Linux, Electron's mouse passthrough is unreliable. SubMiner keeps pointer events enabled, meaning you may need to toggle the overlay off to interact with mpv controls underneath.
### macOS
- **Accessibility permission**: Required for window tracking. Grant it in System Settings > Privacy & Security > Accessibility.
- **Font rendering**: macOS uses a 0.87x font compensation factor for subtitle alignment between mpv and the overlay. If text alignment looks off, adjust the invisible subtitle offset.
- **Gatekeeper**: If macOS blocks SubMiner, right-click the app and select "Open" to bypass the warning, or remove the quarantine attribute: `xattr -d com.apple.quarantine /path/to/SubMiner.app`