diff --git a/README.md b/README.md index 4687789..eafd931 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ An all-in-one sentence mining overlay for MPV with AnkiConnect and dictionary (Y - Yomitan integration for fast, on-screen lookups - Japanese text tokenization using MeCab with smart word boundary detection - Integrated texthooker-ui server for use with Yomitan -- Integrated websocket server (if [mpv_websocket](https://github.com/kuroahna/mpv_websocket) is not found) to send lines to the texthooker +- Integrated WebSocket server (if [mpv_websocket](https://github.com/kuroahna/mpv_websocket) is not found) to send lines to the texthooker - AnkiConnect integration for automatic card creation with media (audio/image) - Invisible subtitle position edit mode (`Ctrl/Cmd+Shift+P`, arrow keys to adjust, `Enter`/`Ctrl+S` save, `Esc` cancel) @@ -32,29 +32,31 @@ Optional but recommended: `yt-dlp`, `fzf`, `rofi`, `chafa`, `ffmpegthumbnailer`. ### Linux (AppImage) +Download the latest release from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest): + ```bash -wget https://github.com/sudacode/subminer/releases/download/v1.0.0/subminer-1.0.0.AppImage -O ~/.local/bin/subminer.AppImage -chmod +x ~/.local/bin/subminer.AppImage -wget https://github.com/sudacode/subminer/releases/download/v1.0.0/subminer -O ~/.local/bin/subminer +wget https://github.com/ksyasuda/SubMiner/releases/download/v0.1.0/SubMiner-0.1.0.AppImage -O ~/.local/bin/SubMiner.AppImage +chmod +x ~/.local/bin/SubMiner.AppImage +wget https://github.com/ksyasuda/SubMiner/releases/download/v0.1.0/subminer -O ~/.local/bin/subminer chmod +x ~/.local/bin/subminer ``` -`subminer` uses a [Bun](https://bun.com) shebang, so `bun` must be on `PATH`. +The `subminer` wrapper uses a [Bun](https://bun.sh) shebang, so `bun` must be on `PATH`. -### From source +### From Source ```bash -git clone https://github.com/sudacode/subminer.git -cd subminer +git clone https://github.com/ksyasuda/SubMiner.git +cd SubMiner make build make install ``` -For macOS app bundle / signing / permissions details, use `docs/installation.md`. +For macOS builds, signing, and platform-specific details, see [docs/installation.md](docs/installation.md). ## Quick Start -1. Copy and customize [`config.example.jsonc`](config.example.jsonc) to `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc` if `XDG_CONFIG_HOME` is unset). +1. Copy and customize [`config.example.jsonc`](config.example.jsonc) to `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`). 2. Start mpv with IPC enabled: ```bash @@ -86,6 +88,7 @@ subminer -T video.mkv # disable texthooker ```bash cp plugin/subminer.lua ~/.config/mpv/scripts/ cp plugin/subminer.conf ~/.config/mpv/script-opts/ +# or: make install-plugin ``` Requires mpv IPC: `--input-ipc-server=/tmp/subminer-socket` @@ -97,18 +100,22 @@ Overlay Jimaku shortcut default: `Ctrl+Alt+J` (`shortcuts.openJimaku`). Detailed guides live in [`docs/`](docs/README.md): -- [Installation](docs/installation.md) -- [Usage](docs/usage.md) -- [Configuration](docs/configuration.md) -- [Development](docs/development.md) -- [Architecture](docs/architecture.md) (includes `OverlayManager` state ownership and deps wiring rules) +- [Installation](docs/installation.md) — Platform requirements, AppImage/macOS/source installs, mpv plugin +- [Usage](docs/usage.md) — Script vs plugin workflow, keybindings, YouTube playback +- [Mining Workflow](docs/mining-workflow.md) — End-to-end mining guide, overlay layers, card creation +- [Configuration](docs/configuration.md) — Full config reference and option details +- [Anki Integration](docs/anki-integration.md) — AnkiConnect setup, field mapping, media generation +- [MPV Plugin](docs/mpv-plugin.md) — Chord keybindings, subminer.conf options, script messages +- [Troubleshooting](docs/troubleshooting.md) — Common issues and solutions +- [Development](docs/development.md) — Building, testing, contributing +- [Architecture](docs/architecture.md) — Service-oriented design, composition model ### Third-Party Components This project includes the following third-party components: -- **[Yomitan](https://github.com/yomidevs/yomitan)** - GPL-3.0 -- **[texthooker-ui](https://github.com/Renji-XD/texthooker-ui)** - MIT +- **[Yomitan](https://github.com/yomidevs/yomitan)** — GPL-3.0 +- **[texthooker-ui](https://github.com/Renji-XD/texthooker-ui)** — MIT ### Acknowledgments diff --git a/comparison.md b/comparison.md deleted file mode 100644 index 1b38104..0000000 --- a/comparison.md +++ /dev/null @@ -1,201 +0,0 @@ -# SubMiner vs Memento: Comparative Analysis - -## Overview - -| | **SubMiner** | **Memento** | -| ------------------ | ------------------------------------- | ---------------------------------------- | -| **Language** | TypeScript (Electron) | C++ (Qt6) | -| **Video Player** | External mpv (IPC socket) | Embedded libmpv (OpenGL widget) | -| **Dictionary** | Yomitan (embedded browser extension) | Own SQLite engine (Yomichan-format dicts) | -| **Platforms** | Linux (X11/Wayland), macOS | Linux, macOS, Windows | -| **Maturity** | Early (v0.1.0) | Mature (v1.7.2, AUR/Flathub packages) | - ---- - -## Where Memento is Stronger - -### 1. Self-Contained Native Application - -Memento embeds mpv directly via libmpv, creating a seamless single-window experience. Player controls, subtitle display, and dictionary lookups all live in one cohesive application. SubMiner runs as a separate overlay process communicating with mpv via IPC, which introduces latency and compositor-specific complexity (Hyprland, Sway, X11 each need their own window-tracking backend). - -### 2. Built-In Dictionary Engine - -Memento has its own SQLite-backed dictionary engine that directly imports Yomichan zip files. No external browser extension needed. This gives Memento tighter control over the lookup UX — results appear inline, kanji cards show stroke order, pitch accent diagrams render natively. SubMiner delegates all dictionary rendering to Yomitan, so the lookup experience depends entirely on the external extension. - -### 3. Deconjugation Engine - -Memento has a full 90+ rule deconjugation engine as a fallback when MeCab isn't available. This means grammar-aware search works even without optional dependencies. SubMiner relies entirely on MeCab for morphological analysis and falls back to raw, unsegmented text if MeCab is unavailable. - -### 4. Windows Support - -Memento has fully documented Windows builds (MSYS2/MinGW), portable installs, and platform-specific troubleshooting docs. SubMiner doesn't ship Windows builds and the platform detection code is untested on Windows. - -### 5. Kanji Detail Cards - -Memento displays rich kanji detail cards with stroke order visualization, JLPT level, frequency rankings, classifications, and codepoints — all rendered natively in Qt. SubMiner delegates kanji display entirely to Yomitan. - -### 6. Packaging & Distribution - -Memento is available on AUR, Flathub, and provides pre-built binaries for all three platforms. It's an established project with a known user base and mature release process. - -### 7. OCR Support - -Memento supports optional OCR via manga-ocr for extracting text from on-screen images (useful for hardsubbed content or manga). SubMiner has no OCR capability. - -### 8. Resource Efficiency - -As a native C++/Qt application, Memento has a significantly smaller memory footprint than SubMiner's Electron-based architecture, which typically consumes 150-300MB of RAM for the Chromium runtime alone. - ---- - -## Where SubMiner is Stronger - -### 1. Anki Integration Depth - -SubMiner's Anki workflow is significantly more advanced: - -- **Automatic polling** detects new cards and auto-populates media fields (no manual trigger needed after the initial mine action) -- **Kiku field grouping** detects duplicate words across cards and merges them into grouped cards with a two-step preview modal -- **Dual note type support** (Lapis sentence cards + Kiku vocabulary cards) with independent field mappings -- **AI translation fallback** uses OpenAI-compatible APIs (OpenRouter, local models) to generate translations when secondary subtitles are unavailable - -Memento's Anki integration is capable but more traditional: manual "Add to Anki" buttons with configurable templates and no automation beyond the initial card creation. - -### 2. Subtitle Timing Intelligence - -SubMiner maintains a subtitle timing cache (200-entry LRU with 5-minute TTL) and uses fuzzy edit-distance matching to locate audio segments for media extraction. This means audio clips are precisely timed even when card creation is delayed or when the subtitle text has been slightly modified. Memento captures the current frame and audio at the moment of card creation. - -### 3. Multi-Copy & Batch Mining - -SubMiner supports multi-copy mode (`Ctrl+Shift+C` to batch-copy N recent subtitle lines) and multi-mine mode (`Ctrl+Alt+S`). This is useful for dialogue-heavy scenes where you want context spanning multiple subtitle lines in a single card. - -### 4. YouTube Subtitle Generation - -SubMiner has built-in Whisper integration for generating subtitles from YouTube videos that lack them. Three modes are available: automatic (generate on-the-fly), preprocess (generate before playback), and off. Memento uses yt-dlp for streaming but has no subtitle generation capability for videos without existing subtitles. - -### 5. Jimaku Integration - -SubMiner connects to the Jimaku anime subtitle API for searching and downloading subtitle files directly from the overlay UI. This streamlines the workflow of finding Japanese subtitles for anime. Memento has no equivalent feature. - -### 6. Subtitle Sync (Subsync) - -SubMiner integrates with alass and ffsubsync for in-session subtitle resynchronization, with a modal UI for selecting the sync engine and source audio track. Memento relies on mpv's native subtitle delay controls, which only handle constant offsets rather than non-linear drift. - -### 7. Texthooker / WebSocket Broadcasting - -SubMiner runs a texthooker HTTP server (port 5174) and a WebSocket server (port 6677), broadcasting subtitles and tokenization data to external tools in real-time. This enables integration with browser-based dictionaries, other mining tools, or custom automation workflows. Memento is fully self-contained with no external broadcast mechanism. - -### 8. AI Translation - -SubMiner can call OpenAI-compatible APIs to generate translations on-the-fly when secondary subtitles aren't available. This includes configurable system prompts, target language selection, and auto-retry with exponential backoff. Memento has no AI or machine translation capability. - -### 9. Runtime Options Modal - -SubMiner has a `Ctrl+Shift+O` palette for toggling session-only options (auto-update cards, field grouping, etc.) without editing config files or restarting. Memento requires navigating through its settings dialog for configuration changes. - -### 10. Overlay Architecture - -SubMiner's transparent overlay design means it works with any existing mpv instance — users keep their mpv configuration, plugins, shaders, and keybindings untouched. Memento requires using its own player, and while it supports mpv config files, they must be placed in Memento's separate config directory. - ---- - -## Where They're Comparable - -| Feature | SubMiner | Memento | -| ------------------------ | ------------------------------------- | ------------------------------------- | -| MeCab tokenization | Yes (optional) | Yes (optional) | -| Secondary subtitles | Yes (3 display modes) | Yes (requires mpv >= 0.35) | -| Configurable styling | CSS variables in config | Qt stylesheet + settings dialog | -| mpv config support | Via mpv directly (user's own config) | Separate Memento config directory | -| AnkiConnect protocol | Yes | Yes | -| Media capture | FFmpeg (audio + image/AVIF) | mpv screenshot + audio extraction | -| Yomichan dict compat | Via Yomitan extension | Native import of Yomichan zip files | -| Pitch accent display | Via Yomitan | Native rendering | -| Streaming (yt-dlp) | Yes | Yes | - ---- - -## Architectural Trade-offs - -### Overlay vs Integrated App - -| Aspect | SubMiner (Overlay) | Memento (Integrated) | -| --------------- | ------------------------------------------------------------ | ------------------------------------------------------- | -| UX cohesion | Separate window; requires compositor cooperation | Single window; everything in one place | -| mpv flexibility | Works with any mpv instance and config | Must use Memento's player; separate config directory | -| Latency | IPC socket introduces small delay | Direct libmpv calls; near-zero latency | -| Platform quirks | Needs per-compositor window tracking (Hyprland/Sway/X11) | Qt handles platform abstraction | - -### Electron vs Native C++/Qt - -| Aspect | SubMiner (Electron/TypeScript) | Memento (C++/Qt6) | -| ------------------ | ------------------------------------------- | ----------------------------------------- | -| Dev velocity | Rapid iteration, rich npm ecosystem | Slower development, manual memory mgmt | -| Memory footprint | ~150-300MB (Chromium runtime) | ~30-80MB typical | -| Startup time | Slower (Chromium spin-up) | Fast native startup | -| Dependency weight | Node modules + Electron binary (~200MB) | System libraries (Qt, mpv, SQLite) | -| UI flexibility | Full HTML/CSS/JS; easy to style | Qt widgets; powerful but more constrained | - -### External Yomitan vs Built-In Dictionary - -| Aspect | SubMiner (Yomitan) | Memento (Built-in) | -| ------------------ | ------------------------------------------------- | --------------------------------------------- | -| Maintenance burden | Free updates from Yomitan project | Must maintain dictionary engine in-house | -| UX control | Limited to Yomitan's popup behavior | Full control over lookup UI and rendering | -| Setup complexity | Requires Yomitan extension + dictionaries | Import dictionaries directly; no extension | -| Feature parity | Gets all Yomitan features automatically | Must implement each feature (pitch, kanji) | - ---- - -## Code Quality Comparison - -### SubMiner - -**Strengths:** -- Clean TypeScript with strict mode -- Centralized config registry with validation and example generation -- Robust error handling with exponential backoff retry logic -- Good separation of concerns (tokenization, media generation, Anki polling) - -**Weaknesses:** -- Monolithic `main.ts` at ~5,000 lines — could benefit from modularization -- Limited test coverage (only `config.test.ts` exists) -- Token merging heuristics are complex (14 helper functions) and may miss edge cases - -### Memento - -**Strengths:** -- Modular C++ architecture with clear directory structure (dict/, player/, gui/, anki/) -- Qt signal/slot pattern keeps components decoupled -- Thread-safe dictionary access via `QReadWriteLock` -- Smart pointer usage throughout; no obvious memory management issues -- Settings migration system for version upgrades - -**Weaknesses:** -- 90+ hardcoded deconjugation rules need manual maintenance for new patterns -- No visible test suite in the repository -- MpvWidget property map handles 40+ properties in a single class - ---- - -## Summary - -**Memento** is a more mature, polished, self-contained desktop application. Its strength is the integrated experience — one app that handles video playback, dictionary lookups, and Anki card creation in a single native window. It has better Windows support, a built-in dictionary engine with deconjugation fallback, kanji stroke-order cards, and OCR support. It's the safer choice for a user who wants something that works out of the box with minimal setup. - -**SubMiner** is more innovative in its Anki workflow automation, mining UX, and external integrations. Features like automatic card polling, Kiku field grouping, AI translation fallback, Whisper subtitle generation, Jimaku subtitle search, subsync integration, and WebSocket broadcasting are all capabilities Memento doesn't have. The overlay architecture also means users can keep their existing mpv setup completely untouched. - -### Key Memento Advantages to Learn From - -- Built-in deconjugation fallback (grammar awareness without MeCab) -- OCR support for non-subtitle text -- Windows platform support -- Self-contained dictionary engine (no external browser extension dependency) -- Lower resource footprint as a native application - -### Key SubMiner Advantages to Protect - -- Anki automation depth (polling, field grouping, AI translation) -- Subtitle intelligence (timing cache, Whisper generation, Jimaku, subsync) -- External tool interop (WebSocket, texthooker server) -- Overlay model (works with any mpv instance and configuration) -- Rapid feature development enabled by TypeScript/Electron diff --git a/composability.md b/composability.md deleted file mode 100644 index 57a1b38..0000000 --- a/composability.md +++ /dev/null @@ -1,324 +0,0 @@ -# SubMiner Composability and Extensibility Plan - -## Goals - -- Reduce coupling concentrated in `src/main.ts`. -- Make new features additive (new files/modules) instead of invasive (edits across many files). -- Standardize extension points for trackers, subtitle processing, IPC actions, and integrations. -- Preserve existing behavior while incrementally migrating architecture. - -## Current Constraints (From Existing Code) - -- `src/main.ts` is the effective integration bus (~5k LOC) and mixes lifecycle, IPC, shortcuts, mpv integration, overlay control, and feature flows. -- Input surfaces are fragmented (CLI args, global shortcuts, renderer IPC) and often route through separate logic paths. -- Some extension points already exist (`src/window-trackers/*`, centralized config definitions in `src/config/definitions.ts`) but still use hardcoded selection/wiring. -- Type contracts are duplicated between main/preload/renderer in places, which increases drift risk. - -## Target Architecture - -### 1. Composition Root + Services - -- Keep `src/main.ts` as bootstrap only. -- Move behavior into focused services: - - `src/core/app-orchestrator.ts` - - `src/core/services/overlay-service.ts` - - `src/core/services/mpv-service.ts` - - `src/core/services/shortcut-service.ts` - - `src/core/services/ipc-service.ts` - - `src/core/services/subtitle-service.ts` - -### 2. Module System - -- Introduce module contract: - - `src/core/module.ts` - - `src/core/module-registry.ts` -- Built-in features become modules: - - anki module - - jimaku module - - runtime options module - - texthooker/websocket module - - subsync module - -### 3. Action Bus - -- Add typed action dispatch (single command path for CLI/shortcut/IPC): - - `src/core/actions.ts` (action type map) - - `src/core/action-bus.ts` (register/dispatch) -- All triggers emit actions; business logic subscribes once. - -### 4. Provider Registries (Strategy Pattern) - -- Replace hardcoded switch/if wiring with registries: - - tracker providers - - tokenizer providers - - translation providers - - subsync backend providers - -### 5. Shared IPC Contract - -- Single source of truth for IPC channels and payloads: - - `src/ipc/contract.ts` - - `src/ipc/main-api.ts` - - `src/ipc/renderer-api.ts` -- `preload.ts` and renderer consume typed wrappers instead of ad hoc channel strings. - -### 6. Subtitle Pipeline - -- Middleware/stage pipeline: - - ingest -> normalize -> tokenize -> merge -> enrich -> emit -- Files: - - `src/subtitle/pipeline.ts` - - `src/subtitle/stages/*.ts` - -### 7. Module-Owned Config Extensions - -- Keep `src/config/definitions.ts` as the central resolved output. -- Add registration path so modules can contribute config/runtime option metadata and defaults before final resolution. - -## Phased Delivery Plan - -## Phase 0: Baseline and Safety Net - -### Scope - -- Add architecture docs and migration guardrails. -- Increase tests around currently stable behavior before structural changes. - -### Changes - -- Add architecture decision notes: - - `docs/development.md` (new section: architecture/migration) - - `docs/architecture.md` (new) -- Add basic smoke tests for command routing, overlay visibility toggles, and config load behavior. - -### Exit Criteria - -- Existing behavior documented with acceptance checklist. -- CI/build + config tests green. - -## Phase 1: Extract I/O Surfaces - -### Scope - -- Isolate IPC and global shortcut registration from `src/main.ts` without changing semantics. - -### Changes - -- Create: - - `src/core/services/ipc-service.ts` - - `src/core/services/shortcut-service.ts` -- Move `ipcMain.handle/on` registration blocks from `src/main.ts` into `IpcService.register(...)`. -- Move global shortcut registration into `ShortcutService.registerGlobal(...)`. -- Keep old entrypoints in `main.ts` delegating to new service methods. - -### Exit Criteria - -- No user-visible behavior changes. -- `src/main.ts` shrinks materially. -- Manual verification: - - overlay toggles - - copy/mine shortcuts - - runtime options open/cycle - -## Phase 2: Introduce Action Bus - -### Scope - -- Canonicalize command execution path. - -### Changes - -- Add typed action bus (`src/core/action-bus.ts`, `src/core/actions.ts`). -- Convert these triggers to dispatch actions instead of direct calls: - - CLI handling (`handleCliCommand` path) - - shortcut handlers - - IPC command handlers -- Keep existing business functions as subscribers initially. - -### Exit Criteria - -- One action path per command (no duplicated behavior branches). -- Unit tests for command mapping (CLI -> action, shortcut -> action, IPC -> action). - -## Phase 3: Module Runtime Skeleton - -### Scope - -- Add module contract and migrate one low-risk feature first. - -### Changes - -- Introduce: - - `src/core/module.ts` - - `src/core/module-registry.ts` - - `src/core/app-context.ts` -- First migration target: runtime options - - create `src/modules/runtime-options/module.ts` - - wire existing `RuntimeOptionsManager` through module lifecycle. - -### Exit Criteria - -- Runtime options fully owned by module. -- Core app can start with module list and deterministic module init order. - -## Phase 4: Provider Registries - -### Scope - -- Remove hardcoded provider selection. - -### Changes - -- Window tracker: - - Replace switch in `src/window-trackers/index.ts` with registry API. - - Add provider objects for hyprland/sway/x11/macos. -- Tokenizer/translator/subsync: - - Add analogous registries in new provider directories. - -### Exit Criteria - -- Adding a provider requires adding one file + registration line. -- No edits required in composition root for new provider variants. - -## Phase 5: Shared IPC Contract - -### Scope - -- Eliminate channel/payload drift between main/preload/renderer. - -### Changes - -- Add typed IPC contract files under `src/ipc/`. -- Refactor `src/preload.ts` to generate API from shared contract wrappers. -- Refactor renderer calls to consume typed API interface from shared module. - -### Exit Criteria - -- Channel names declared once. -- Compile-time checking across main/preload/renderer for payload mismatch. - -## Phase 6: Subtitle Pipeline - -### Scope - -- Extract subtitle transformations into composable stages. - -### Changes - -- Create pipeline framework and migrate existing tokenization/merge flow: - - stage: normalize subtitle - - stage: tokenize (`MecabTokenizer` adapter) - - stage: merge tokens (`mergeTokens` adapter) - - stage: post-processing/enrichment hooks -- Integrate pipeline execution into subtitle update loop. - -### Exit Criteria - -- Current output parity for tokenization/merged token behavior. -- Stage-level tests for deterministic input/output. - -## Phase 7: Moduleized Integrations - -### Scope - -- Convert major features to modules in descending complexity. - -### Migration Order - -1. Jimaku -2. Texthooker/WebSocket bridge -3. Subsync -4. Anki integration - -### Exit Criteria - -- Each feature has independent init/start/stop lifecycle. -- App startup composes modules instead of hardcoded inline setup. - -## Recommended File Layout (Target) - -```text -src/ - core/ - app-orchestrator.ts - app-context.ts - actions.ts - action-bus.ts - module.ts - module-registry.ts - services/ - ipc-service.ts - shortcut-service.ts - overlay-service.ts - mpv-service.ts - subtitle-service.ts - modules/ - runtime-options/module.ts - jimaku/module.ts - texthooker/module.ts - subsync/module.ts - anki/module.ts - ipc/ - contract.ts - main-api.ts - renderer-api.ts - subtitle/ - pipeline.ts - stages/ - normalize.ts - tokenize-mecab.ts - merge-default.ts - enrich.ts -``` - -## PR Breakdown (Suggested) - -1. PR1: Phase 1 (`IpcService`, `ShortcutService`, no behavior changes) -2. PR2: Phase 2 (action bus + trigger mapping) -3. PR3: Phase 3 (module skeleton + runtime options module) -4. PR4: Phase 4 (window tracker registry + provider pattern) -5. PR5: Phase 5 (shared IPC contract) -6. PR6: Phase 6 (subtitle pipeline) -7. PR7+: Phase 7 (feature-by-feature module migrations) - -## Validation Matrix - -- Build/typecheck: - - `pnpm run build` -- Config correctness: - - `pnpm run test:config` -- Manual checks per PR: - - app start/stop/toggle CLI - - visible/invisible overlay control - - global shortcuts - - subtitle render and token display - - runtime options open + cycle - - anki mine flow + update-from-clipboard - -## Backward Compatibility Rules - -- Preserve existing CLI flags and IPC channel behavior during migration. -- Maintain config shape compatibility; only additive changes unless versioned migration is added. -- Keep default behavior equivalent for all supported compositor backends. - -## Key Risks and Mitigations - -- Risk: behavior regressions during extraction. - - Mitigation: move code verbatim first, refactor second; keep thin delegates in `main.ts` until stable. -- Risk: module lifecycle race conditions. - - Mitigation: explicit init order, idempotent `start/stop`, and startup dependency checks. -- Risk: IPC contract churn breaks renderer. - - Mitigation: contract wrappers and compile-time types; temporary compatibility adapters. -- Risk: feature coupling around anki/mine flows. - - Mitigation: defer high-coupling module migrations until action bus and shared app context are stable. - -## Definition of Done (Program-Level) - -- `src/main.ts` reduced to bootstrap/composition responsibilities. -- New feature prototype can be added as an isolated module with: - - module file - - optional provider registration - - optional config schema registration - - no invasive edits across unrelated subsystems -- IPC and command routing are single-path and typed. -- Existing user workflows remain intact. diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index deea54f..af1a589 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -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' }, ], diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 257b89e..ab01c93 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -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', diff --git a/docs/README.md b/docs/README.md index a6d58ff..feaa4fe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 diff --git a/docs/anki-integration.md b/docs/anki-integration.md new file mode 100644 index 0000000..850a5f1 --- /dev/null +++ b/docs/anki-integration.md @@ -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:" 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_.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, // 1–100 + "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 // 0–63, 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 (1–9) 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" + } + } +} +``` diff --git a/docs/architecture.md b/docs/architecture.md index b18cf18..6430c68 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/docs/development.md b/docs/development.md index 3ea5724..559d25a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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 | diff --git a/docs/index.md b/docs/index.md index f995aa9..4cf4cfc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. --- - - diff --git a/docs/mining-workflow.md b/docs/mining-workflow.md new file mode 100644 index 0000000..0082e74 --- /dev/null +++ b/docs/mining-workflow.md @@ -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 1–9 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. diff --git a/docs/mpv-plugin.md b/docs/mpv-plugin.md new file mode 100644 index 0000000..a657b40 --- /dev/null +++ b/docs/mpv-plugin.md @@ -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` | 1–65535 | 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. diff --git a/docs/refactor-main-checklist.md b/docs/refactor-main-checklist.md deleted file mode 100644 index d223128..0000000 --- a/docs/refactor-main-checklist.md +++ /dev/null @@ -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. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..65114bb --- /dev/null +++ b/docs/troubleshooting.md @@ -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. `/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` diff --git a/investigation.md b/investigation.md deleted file mode 100644 index 4266383..0000000 --- a/investigation.md +++ /dev/null @@ -1,219 +0,0 @@ -# Refactoring Investigation Report - -## Overview - -This report evaluates the SubMiner refactoring effort on the `refactor` branch against the plan defined in `plan.md` and the safety checklist in `docs/refactor-main-checklist.md`. The refactoring aimed to eliminate unnecessary abstraction layers, consolidate related services, fix known bugs, and add test coverage. - -**Result**: 65 commits, 104 files changed, main.ts reduced from ~5,800 to 1,398 lines, all 67 tests passing, build succeeds. - ---- - -## Phase 1: Delete Thin Wrappers — COMPLETE - -All 9 target wrapper services and 7 associated test files have been deleted and their logic inlined into callers (mostly main.ts or the services they fed). - -| Target File | Status | -|-------------|--------| -| `config-warning-runtime-service.ts` + test | Deleted, inlined | -| `overlay-modal-restore-service.ts` + test | Deleted, inlined | -| `runtime-options-manager-runtime-service.ts` + test | Deleted, inlined | -| `app-logging-runtime-service.ts` + test | Deleted, inlined | -| `overlay-send-service.ts` (no test) | Deleted, inlined | -| `startup-resource-runtime-service.ts` + test | Deleted, inlined | -| `config-generation-runtime-service.ts` + test | Deleted, inlined | -| `app-shutdown-runtime-service.ts` + test | Deleted, inlined | -| `shortcut-ui-deps-runtime-service.ts` + test | Deleted, inlined | - -**Files removed**: 16 (9 services + 7 tests) -**No issues found.** - ---- - -## Phase 2: Consolidate DI Adapter Services — COMPLETE - -All 4 dependency-injection adapter services have been merged into the services they fed, and their test files removed. - -| Adapter | Merged Into | Status | -|---------|------------|--------| -| `cli-command-deps-runtime-service.ts` + test | `cli-command-service.ts` | Done | -| `ipc-deps-runtime-service.ts` + test | `ipc-service.ts` | Done | -| `tokenizer-deps-runtime-service.ts` + test | `tokenizer-service.ts` | Done | -| `app-lifecycle-deps-runtime-service.ts` + test | `app-lifecycle-service.ts` | Done | - -**Files removed**: 8 (4 services + 4 tests) -**No issues found.** - ---- - -## Phase 3: Consolidate Related Services — COMPLETE - -All planned service merges have been executed. - -| Consolidation | Source Files | Result File | Status | -|--------------|-------------|-------------|--------| -| Overlay visibility | `overlay-visibility-runtime-service.ts` | `overlay-visibility-service.ts` | Done | -| Overlay broadcast | `overlay-broadcast-runtime-service.ts` + test | `overlay-manager-service.ts` | Done | -| Overlay shortcuts | `overlay-shortcut-lifecycle-service.ts`, `overlay-shortcut-fallback-runner.ts` | `overlay-shortcut-service.ts` + `overlay-shortcut-handler.ts` | Done | -| Numeric shortcuts | `numeric-shortcut-runtime-service.ts` + test, `numeric-shortcut-session-service.ts` | `numeric-shortcut-service.ts` | Done | -| Startup/bootstrap | `startup-bootstrap-runtime-service.ts` + test, `app-ready-runtime-service.ts` + test | `startup-service.ts` | Done | - -**Files removed**: ~10 -**No issues found.** - ---- - -## Phase 4: Fix Bugs and Code Quality — COMPLETE - -### 4.1 Debug console.log statements -**Status**: RESOLVED — No debug `console.log` or `console.warn` calls remain in `overlay-visibility-service.ts`. - -### 4.2 `as never` type cast -**Status**: RESOLVED — No `as never` cast remains in `tokenizer-service.ts`. The type mismatch was fixed properly. - -### 4.3 fieldGroupingResolver race condition -**Status**: RESOLVED — Fixed in `main.ts` (lines 305–332) with a sequence counter mechanism. Each new resolver increments `fieldGroupingResolverSequence`, and wrapped resolvers check if their sequence matches the current value before executing. Stale resolutions are correctly ignored. - -### 4.4 Async callback safety -**Status**: RESOLVED -- **CLI commands** (`cli-command-service.ts`): Async commands use `runAsyncWithOsd` helper (lines 177–187) which catches errors, logs them, and displays via MPV OSD. -- **IPC handlers** (`ipc-service.ts`): Async handlers use `ipcMain.handle` (not `.on`), which properly awaits and propagates promise results. Synchronous handlers correctly use `ipcMain.on`. - -### 4.5 `-runtime-service` naming convention -**Status**: RESOLVED — No files with `-runtime-service` in the name exist under `src/core/services/`. All have been renamed to `*-service.ts`. - ---- - -## Phase 5: Add Tests for Critical Untested Services — COMPLETE - -Tests added per plan: - -| Service | Test File | Tests | -|---------|-----------|-------| -| `mpv-service.ts` (761 lines) | `mpv-service.test.ts` | Socket protocol, property changes, reconnection | -| `subsync-service.ts` (427 lines) | `subsync-service.test.ts` | Config resolution, command construction, error handling | -| `tokenizer-service.ts` (305 lines) | `tokenizer-service.test.ts` | Parser init, token extraction, edge cases | -| `cli-command-service.ts` (204 lines) | `cli-command-service.test.ts` (290 lines) | Expanded: all dispatch paths, error propagation | - -**Total**: 67 tests passing across all test suites. - ---- - -## Phase 6: Directory Restructure — NOT STARTED (Optional) - -Services remain in the flat `src/core/services/` directory. This phase was explicitly marked optional and low-priority. The current flat structure with ~25 files is manageable. - ---- - -## Checklist Compliance (docs/refactor-main-checklist.md) - -### Invariants - -| Invariant | Status | -|-----------|--------| -| CLI flags and aliases working | Verified via tests | -| IPC channel names backward-compatible | No channel names changed | -| Overlay toggle behavior preserved | Logic moved verbatim | -| MPV integration behavior preserved | Logic moved verbatim, tested | -| Texthooker mode preserved | Not altered | -| Mining/runtime options paths preserved | Logic moved verbatim | - -### Automated Checks - -| Check | Status | -|-------|--------| -| `pnpm run build` | Passes | -| `pnpm run test:core` | 67/67 passing | - -### Manual Smoke Checks - -**Status**: NOT YET PERFORMED — Visual overlay rendering, card mining flow, and field-grouping interaction require a real desktop session with MPV. Automated tests validate logic correctness but cannot catch rendering regressions. - ---- - -## Loose Ends and Concerns - -### 1. Unused Architectural Scaffolding (~500 lines, dead code) - -The following files exist in the repository but are **not imported or used anywhere**: - -**Core abstractions:** -| File | Lines | Purpose | -|------|-------|---------| -| `src/core/action-bus.ts` | 22 | Generic action dispatcher (`register`/`dispatch`) | -| `src/core/actions.ts` | 17 | Union type `AppAction` with 16 action variants | -| `src/core/app-context.ts` | 46 | Module context interfaces | -| `src/core/module-registry.ts` | 37 | Module lifecycle manager (init/start/stop) | -| `src/core/module.ts` | 7 | `SubminerModule` interface | - -**Module implementations:** -| File | Lines | Purpose | -|------|-------|---------| -| `src/modules/runtime-options/module.ts` | 62 | Runtime options module | -| `src/modules/subsync/module.ts` | 79 | Subsync module | -| `src/modules/jimaku/module.ts` | 73 | Jimaku module | - -**IPC abstraction layer:** -| File | Lines | Purpose | -|------|-------|---------| -| `src/ipc/contract.ts` | 62 | IPC channel name constants | -| `src/ipc/main-api.ts` | 20 | Main process IPC helpers | -| `src/ipc/renderer-api.ts` | 28 | Renderer process IPC helpers | - -These represent scaffolding for a module-based architecture that was prototyped but never wired into main.ts. The current codebase uses the service-oriented approach throughout. - -**Recommendation**: Remove if abandoned, or document with clear intent if planned for a future phase. - -### 2. New Consolidated Services Without Test Coverage - -Seven service files created or consolidated during Phase 3 lack dedicated tests: - -| File | Lines | Risk Level | Reason | -|------|-------|------------|--------| -| `overlay-shortcut-handler.ts` | 216 | Higher | Complex runtime handlers with fallback logic | -| `mining-service.ts` | 179 | Higher | 6 public functions orchestrating mining workflows | -| `anki-jimaku-service.ts` | 173 | Higher | Complex IPC registration with multiple handlers | -| `startup-service.ts` | ~150 | Medium | Bootstrap and app-ready orchestration | -| `numeric-shortcut-service.ts` | 133 | Medium | Session state management | -| `subsync-runner-service.ts` | 86 | Lower | Thin runtime wrapper | -| `jimaku-service.ts` | 81 | Lower | Config accessor functions | - -Phase 5 targeted the 4 highest-risk *previously existing* untested services. The Phase 3 consolidation created new files that inherited logic from multiple sources — those were not explicitly called out in the plan for testing. - -### 3. Null Safety in Consolidated Services - -All checked consolidated services handle null/undefined correctly: - -- **`mpv-control-service.ts`**: Accepts `MpvRuntimeClientLike | null`, uses null checks and optional chaining before all access. -- **`overlay-bridge-service.ts`**: Returns `false` early if window is null or destroyed (`line 17: if (!options.mainWindow || options.mainWindow.isDestroyed()) return false`). -- **`startup-service.ts`**: Uses dependency injection with all deps provided upfront, async operations awaited in sequence, config access uses optional chaining. - -**No null safety issues found.** - -### 4. Import Consistency - -All 68 imports in `main.ts` from `./core/services` resolve to existing exports. No references to deleted files remain. The barrel export in `index.ts` has 79 entries with no dead exports (one minor case: `isGlobalShortcutRegisteredSafe` is only used within the service layer itself, not by main.ts). - ---- - -## Risk Assessment - -| Area | Risk | Mitigation | -|------|------|------------| -| Logic regression from inlining | Low | Code moved verbatim, 67 tests pass | -| Overlay rendering regression | Medium | Requires manual smoke test with MPV | -| Unused scaffolding becoming stale | Low | Remove or document with clear intent | -| Missing test coverage on new files | Medium | Add tests for the 3 higher-risk services | -| Race conditions | Low | fieldGroupingResolver race fixed with sequence counter | -| Async error swallowing | Low | All async paths have error boundaries | - ---- - -## Recommendations - -1. **Remove unused scaffolding** (`src/core/action-bus.ts`, `src/core/actions.ts`, `src/core/app-context.ts`, `src/core/module-registry.ts`, `src/core/module.ts`, `src/modules/`, `src/ipc/`) — ~500 lines of dead code that contradicts the refactoring goal of reducing unnecessary abstraction. - -2. **Add tests for higher-risk consolidated services** — `overlay-shortcut-handler.ts`, `mining-service.ts`, and `anki-jimaku-service.ts` have the most complex logic among the untested new files. - -3. **Perform desktop smoke test** — Verify overlay rendering, card mining, and field-grouping interaction in a real session with MPV running. - -4. **Consider removing `isGlobalShortcutRegisteredSafe` from barrel export** — It's only used internally by the service layer, not by main.ts. diff --git a/plan.md b/plan.md deleted file mode 100644 index b861797..0000000 --- a/plan.md +++ /dev/null @@ -1,377 +0,0 @@ -# SubMiner Refactoring Plan - -## Goals - -- Eliminate unnecessary abstraction layers and thin wrapper services -- Consolidate related services into cohesive domain modules -- Fix known bugs and code quality issues -- Add test coverage for critical untested services -- Keep main.ts lean without pushing complexity into pointless indirection - -## Guiding Principles - -- **Verify after every phase**: `pnpm run build && pnpm run test:config && pnpm run test:core` must pass -- **One concern per commit**: each commit should be a single logical change (can be multiple files as long as it makes sense logically) -- **Inline first, restructure second**: delete the wrapper, verify nothing breaks, then clean up -- **Don't create new abstractions**: the goal is fewer files, not different files - ---- - -## Phase 1: Delete Thin Wrappers (9 files + 7 test files) - -These services wrap a single function call or trivial operation. Inline their logic -into callers (mostly main.ts or the service that calls them) and delete both the -service file and its test file. - -### 1.1 Inline `config-warning-runtime-service.ts` (14 lines) - -Two pure string-formatting functions. Inline the format string directly where -`logConfigWarningRuntimeService` is called (should be in main.ts startup or -`app-logging-runtime-service.ts`). Delete both files. - -- Delete: `config-warning-runtime-service.ts`, `config-warning-runtime-service.test.ts` - -### 1.2 Inline `overlay-modal-restore-service.ts` (18 lines) - -Wraps `Set.add()` and a conditional `Set.delete()`. Inline the Set operations at -call sites in main.ts. - -- Delete: `overlay-modal-restore-service.ts`, `overlay-modal-restore-service.test.ts` - -### 1.3 Inline `runtime-options-manager-runtime-service.ts` (17 lines) - -Wraps `new RuntimeOptionsManager(...)`. Call the constructor directly. - -- Delete: `runtime-options-manager-runtime-service.ts`, `runtime-options-manager-runtime-service.test.ts` - -### 1.4 Inline `app-logging-runtime-service.ts` (28 lines) - -Creates an object with two methods. After inlining config-warning (1.1), this -becomes a trivial object literal. Inline where the logging runtime is created. - -- Delete: `app-logging-runtime-service.ts`, `app-logging-runtime-service.test.ts` - -### 1.5 Inline `overlay-send-service.ts` (26 lines) - -Wraps `window.webContents.send()` with a null/destroyed guard. This is a -one-liner pattern. Inline at call sites. - -- Delete: `overlay-send-service.ts` (no test file) - -### 1.6 Inline `startup-resource-runtime-service.ts` (26 lines) - -Two functions that call a constructor and a check method. Inline into the -app-ready startup flow. - -- Delete: `startup-resource-runtime-service.ts`, `startup-resource-runtime-service.test.ts` - -### 1.7 Inline `config-generation-runtime-service.ts` (26 lines) - -Simple conditional (if args say generate config, call generator, quit). Inline -into the startup bootstrap flow. - -- Delete: `config-generation-runtime-service.ts`, `config-generation-runtime-service.test.ts` - -### 1.8 Inline `app-shutdown-runtime-service.ts` (27 lines) - -Calls 10 cleanup functions in sequence. Inline into the willQuit handler in -main.ts. - -- Delete: `app-shutdown-runtime-service.ts`, `app-shutdown-runtime-service.test.ts` - -### 1.9 Inline `shortcut-ui-deps-runtime-service.ts` (24 lines) - -Single wrapper that unwraps 4 getters and calls `runOverlayShortcutLocalFallback`. -Inline at call site. - -- Delete: `shortcut-ui-deps-runtime-service.ts`, `shortcut-ui-deps-runtime-service.test.ts` - -### Phase 1 verification - -```bash -pnpm run build && pnpm run test:core -``` - -**Expected result**: 16 files deleted, ~260 lines of service code removed, -~350 lines of test code for trivial wrappers removed. `index.ts` barrel export -shrinks by ~10 entries. - ---- - -## Phase 2: Consolidate DI Adapter Services (4 files) - -These are 50-130 line files that map one interface shape to another with minimal -logic. They exist because the service they adapt has a different interface than -what main.ts provides. The fix is to align the interfaces so the adapter isn't -needed, or absorb the adapter into the service it feeds. - -### 2.1 Merge `cli-command-deps-runtime-service.ts` into `cli-command-service.ts` - -The deps adapter (132 lines) maps main.ts state into `CliCommandServiceDeps`. -Instead: make `handleCliCommandService` accept the same shape main.ts naturally -provides, or accept a smaller interface with the actual values rather than -getter/setter pairs. Move any null-guarding logic into the command handlers -themselves. - -- Delete: `cli-command-deps-runtime-service.ts`, `cli-command-deps-runtime-service.test.ts` -- Modify: `cli-command-service.ts` to accept deps directly - -### 2.2 Merge `ipc-deps-runtime-service.ts` into `ipc-service.ts` - -Same pattern as 2.1. The deps adapter (100 lines) maps main.ts state for IPC -handlers. Merge the defensive null checks and window guards into the IPC handlers -that need them. - -- Delete: `ipc-deps-runtime-service.ts`, `ipc-deps-runtime-service.test.ts` -- Modify: `ipc-service.ts` - -### 2.3 Merge `tokenizer-deps-runtime-service.ts` into `tokenizer-service.ts` - -The adapter (45 lines) has one non-trivial function (`tokenizeWithMecab` with null -checks and token merging). Move that logic into `tokenizer-service.ts`. - -- Delete: `tokenizer-deps-runtime-service.ts`, `tokenizer-deps-runtime-service.test.ts` -- Modify: `tokenizer-service.ts` - -### 2.4 Merge `app-lifecycle-deps-runtime-service.ts` into `app-lifecycle-service.ts` - -The adapter (57 lines) wraps Electron app events. Merge event binding into the -lifecycle service itself since it already knows about Electron's app lifecycle. - -- Delete: `app-lifecycle-deps-runtime-service.ts`, `app-lifecycle-deps-runtime-service.test.ts` -- Modify: `app-lifecycle-service.ts` - -### Phase 2 verification - -```bash -pnpm run build && pnpm run test:core -``` - -**Expected result**: 8 more files deleted. DI adapters absorbed into the services -they feed. `index.ts` shrinks further. - ---- - -## Phase 3: Consolidate Related Services - -Merge services that are split across multiple files but represent a single concern. - -### 3.1 Merge overlay visibility files - -`overlay-visibility-runtime-service.ts` (46 lines) is a thin orchestration layer -over `overlay-visibility-service.ts` (183 lines). Merge into one file: -`overlay-visibility-service.ts`. - -- Delete: `overlay-visibility-runtime-service.ts` -- Modify: `overlay-visibility-service.ts` — absorb the 3 exported functions - -### 3.2 Merge overlay broadcast files - -`overlay-broadcast-runtime-service.ts` (45 lines) contains utility functions for -window filtering and broadcasting. These are closely related to -`overlay-manager-service.ts` (49 lines) which manages the window references. -Merge broadcast functions into the overlay manager since it already owns the -window state. - -- Delete: `overlay-broadcast-runtime-service.ts`, `overlay-broadcast-runtime-service.test.ts` -- Modify: `overlay-manager-service.ts` — add broadcast methods - -### 3.3 Merge overlay shortcut files - -There are 4 shortcut-related files: - -- `overlay-shortcut-service.ts` (169 lines) — registration -- `overlay-shortcut-runtime-service.ts` (105 lines) — runtime handlers -- `overlay-shortcut-lifecycle-service.ts` (52 lines) — sync/refresh/unregister -- `overlay-shortcut-fallback-runner.ts` (114 lines) — local fallback execution - -Consolidate into 2 files: - -- `overlay-shortcut-service.ts` — registration + lifecycle (absorb lifecycle-service) -- `overlay-shortcut-handler.ts` — runtime handlers + fallback runner - -- Delete: `overlay-shortcut-lifecycle-service.ts`, `overlay-shortcut-fallback-runner.ts` - -### 3.4 Merge numeric shortcut files - -`numeric-shortcut-runtime-service.ts` (37 lines) and -`numeric-shortcut-session-service.ts` (99 lines) are two halves of one feature. -Merge into `numeric-shortcut-service.ts`. - -- Delete: `numeric-shortcut-runtime-service.ts`, `numeric-shortcut-runtime-service.test.ts` -- Modify: `numeric-shortcut-session-service.ts` → rename to `numeric-shortcut-service.ts` - -### 3.5 Merge startup/bootstrap files - -`startup-bootstrap-runtime-service.ts` (53 lines) and -`app-ready-runtime-service.ts` (77 lines) are both startup orchestration. -Merge into a single `startup-service.ts`. - -- Delete: `startup-bootstrap-runtime-service.ts`, `startup-bootstrap-runtime-service.test.ts` -- Modify: `app-ready-runtime-service.ts` → rename to `startup-service.ts`, absorb bootstrap - -### Phase 3 verification - -```bash -pnpm run build && pnpm run test:core -``` - -**Expected result**: ~10 more files deleted. Related services consolidated into -single cohesive modules. - ---- - -## Phase 4: Fix Bugs and Code Quality - -### 4.1 Remove debug `console.log` statements - -`overlay-visibility-service.ts` has 7+ raw `console.log`/`console.warn` calls -used for debugging. Remove them or replace with the app logger if the messages -have ongoing diagnostic value. - -### 4.2 Fix `as never` type cast - -`tokenizer-deps-runtime-service.ts` (or its successor after Phase 2) uses -`return mergeTokens(rawTokens as never)`. Investigate the actual type mismatch -and fix it properly. - -### 4.3 Guard `fieldGroupingResolver` against race conditions - -In main.ts, the `fieldGroupingResolver` is a single global variable. If two -field grouping requests arrive concurrently, the second overwrites the first's -resolver. Add a request ID or sequence number so stale resolutions are ignored. - -### 4.4 Audit async callbacks in CLI command handlers - -Verify that async functions passed as callbacks in the CLI command and IPC handler -wiring (main.ts lines ~697-707, ~1347, ~1360) are properly awaited or have -`.catch()` handlers so rejections aren't silently swallowed. - -### 4.5 Drop the `-runtime-service` naming convention - -After phases 1-3, rename remaining files to just `*-service.ts`. The "runtime" -prefix adds no meaning. Do this as a batch rename commit with no logic changes. - -### Phase 4 verification - -```bash -pnpm run build && pnpm run test:core -``` - -Manually smoke test: launch SubMiner, verify overlay renders, mine a card, toggle -field grouping. - ---- - -## Phase 5: Add Tests for Critical Untested Services - -These are the highest-risk untested modules. Add focused tests that verify -real behavior, not just that mocks were called. - -### 5.1 `mpv-service.ts` (761 lines, untested) - -Test the IPC protocol layer: - -- Socket message parsing (JSON line protocol) -- Property change event dispatch -- Request/response matching via request IDs -- Reconnection behavior on socket close -- Subtitle text extraction from property changes - -### 5.2 `subsync-service.ts` (427 lines, untested) - -Test: - -- Config resolution (alass vs ffsubsync path selection) -- Command construction for each sync engine -- Timeout and error handling for child processes -- Result parsing - -### 5.3 `tokenizer-service.ts` (305 lines, untested) - -Test: - -- Yomitan parser initialization and readiness -- Token extraction from parsed results -- Fallback behavior when parser unavailable -- Edge cases: empty text, CJK-only, mixed content - -### 5.4 `cli-command-service.ts` (204 lines, partially tested) - -Expand existing tests to cover: - -- All CLI command dispatch paths -- Error propagation from async handlers -- Second-instance argument forwarding - -### Phase 5 verification - -```bash -pnpm run test:core -``` - -All new tests pass. Aim for the 4 services above to each have 5-10 meaningful -test cases. - ---- - -## Phase 6 (Optional): Domain-Based Directory Structure - -After phases 1-5, the service count should be roughly 20-25 files down from 47. -If that still feels too flat, group by domain: - -``` -src/core/ - mpv/ - mpv-service.ts - mpv-render-metrics-service.ts - overlay/ - overlay-manager-service.ts - overlay-visibility-service.ts - overlay-window-service.ts - overlay-shortcut-service.ts - overlay-shortcut-handler.ts - mining/ - mining-runtime-service.ts - field-grouping-service.ts - field-grouping-overlay-service.ts - startup/ - startup-service.ts - app-lifecycle-service.ts - ipc/ - ipc-service.ts - ipc-command-service.ts - shortcuts/ - shortcut-service.ts - numeric-shortcut-service.ts - services/ - tokenizer-service.ts - subtitle-position-service.ts - subtitle-ws-service.ts - texthooker-service.ts - yomitan-extension-loader-service.ts - yomitan-settings-service.ts - secondary-subtitle-service.ts - runtime-config-service.ts - runtime-options-runtime-service.ts - cli-command-service.ts -``` - -Only do this if the flat directory still feels unwieldy after consolidation. -This is cosmetic and low-priority relative to phases 1-5. - ---- - -## Summary - -| Phase | Files Deleted | Files Modified | Risk | Effort | -| -------------------------- | ------------------- | ----------------- | ------ | ------ | -| 1. Delete thin wrappers | 16 (9 svc + 7 test) | main.ts, index.ts | Low | Small | -| 2. Consolidate DI adapters | 8 (4 svc + 4 test) | 4 services | Medium | Medium | -| 3. Merge related services | ~10 | ~5 services | Medium | Medium | -| 4. Fix bugs & rename | 0 | ~6 files | Low | Small | -| 5. Add critical tests | 0 | 4 new test files | Low | Medium | -| 6. Directory restructure | 0 | All imports | Low | Small | - -**Net result**: ~34 files deleted, service count from 47 → ~22, index.ts from -92 exports → ~45, and the remaining services each have a clear reason to exist.