diff --git a/changes/2026-03-22-websocket-texthooker-docs.md b/changes/2026-03-22-websocket-texthooker-docs.md new file mode 100644 index 0000000..6322acc --- /dev/null +++ b/changes/2026-03-22-websocket-texthooker-docs.md @@ -0,0 +1,5 @@ +type: docs +area: docs + +- Added a new WebSocket / Texthooker API and integration guide covering websocket payloads, custom client patterns, mpv plugin automation, and webhook-style relay examples. +- Linked the new integration guide from configuration and mining workflow docs for easier discovery. diff --git a/docs-site/.vitepress/config.ts b/docs-site/.vitepress/config.ts index f40a3c1..8a1bb2f 100644 --- a/docs-site/.vitepress/config.ts +++ b/docs-site/.vitepress/config.ts @@ -95,6 +95,7 @@ export default { { text: 'Building & Testing', link: '/development' }, { text: 'Architecture', link: '/architecture' }, { text: 'IPC + Runtime Contracts', link: '/ipc-contracts' }, + { text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' }, { text: 'Changelog', link: '/changelog' }, ], }, diff --git a/docs-site/configuration.md b/docs-site/configuration.md index be38853..99788bd 100644 --- a/docs-site/configuration.md +++ b/docs-site/configuration.md @@ -195,6 +195,8 @@ Defaults warm everything (`true` for all toggles, `lowPowerMode: false`). Settin The overlay includes a built-in WebSocket server that broadcasts subtitle text to connected clients (such as texthooker-ui) for external processing. +For endpoint details, payload examples, and client patterns, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api). + By default, the server uses "auto" mode: it starts automatically unless [mpv_websocket](https://github.com/kuroahna/mpv_websocket) is detected at `~/.config/mpv/mpv_websocket`. If you have mpv_websocket installed, the built-in server is skipped to avoid conflicts. See `config.example.jsonc` for detailed configuration options. diff --git a/docs-site/mining-workflow.md b/docs-site/mining-workflow.md index 65e20d2..4d1a500 100644 --- a/docs-site/mining-workflow.md +++ b/docs-site/mining-workflow.md @@ -176,6 +176,8 @@ SubMiner runs a local HTTP server at `http://127.0.0.1:5174` (configurable port) 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. +If you want to build your own browser client, websocket consumer, or automation relay, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api). + ## 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). diff --git a/docs-site/websocket-texthooker-api.md b/docs-site/websocket-texthooker-api.md new file mode 100644 index 0000000..01a22f7 --- /dev/null +++ b/docs-site/websocket-texthooker-api.md @@ -0,0 +1,357 @@ +# WebSocket / Texthooker API & Integration + +SubMiner exposes a small set of local integration surfaces for browser tools, automation helpers, and mpv-driven workflows: + +- **Subtitle WebSocket** at `ws://127.0.0.1:6677` by default for plain subtitle pushes. +- **Annotation WebSocket** at `ws://127.0.0.1:6678` by default for token-aware clients. +- **Texthooker HTTP UI** at `http://127.0.0.1:5174` by default for browser-based subtitle consumption. +- **mpv plugin script messages** for in-player automation and extension. + +This page documents those integration points and shows how to build custom consumers around them. + +## Quick Reference + +| Surface | Default | Purpose | +| --- | --- | --- | +| `websocket` | `ws://127.0.0.1:6677` | Basic subtitle broadcast stream | +| `annotationWebsocket` | `ws://127.0.0.1:6678` | Structured stream with token metadata | +| `texthooker` | `http://127.0.0.1:5174` | Local texthooker UI with injected websocket config | +| mpv plugin | `script-message subminer-*` | Start/stop/toggle/status automation inside mpv | + +## Enable and Configure the Services + +SubMiner's integration ports are configured in `config.jsonc`. + +```jsonc +{ + "websocket": { + "enabled": "auto", + "port": 6677 + }, + "annotationWebsocket": { + "enabled": true, + "port": 6678 + }, + "texthooker": { + "launchAtStartup": true, + "openBrowser": true + } +} +``` + +### How startup behaves + +- `websocket.enabled: "auto"` starts the basic subtitle websocket unless SubMiner detects the external `mpv_websocket` plugin. +- `annotationWebsocket` is independent from `websocket` and stays enabled unless you explicitly disable it. +- `texthooker.launchAtStartup` starts the local HTTP UI automatically. +- `texthooker.openBrowser` controls whether SubMiner opens the texthooker page in your browser when it starts. + +If you use the [mpv plugin](/mpv-plugin), it can also start a texthooker-only helper process and override the texthooker port in `subminer.conf`. + +## Developer API Documentation + +### 1. Subtitle WebSocket + +Use the basic subtitle websocket when you only need the current subtitle line and a ready-to-render HTML sentence string. + +- **Default URL:** `ws://127.0.0.1:6677` +- **Transport:** local WebSocket server bound to `127.0.0.1` +- **Direction:** server push only +- **Client auth:** none +- **Reconnects:** client-managed + +When a client connects, SubMiner immediately sends the latest subtitle payload if one is available. After that, it pushes a new message each time the current subtitle changes. + +#### Message shape + +```json +{ + "version": 1, + "text": "無事", + "sentence": "無事", + "tokens": [ + { + "surface": "無事", + "reading": "ぶじ", + "headword": "無事", + "startPos": 0, + "endPos": 2, + "partOfSpeech": "other", + "isMerged": false, + "isKnown": true, + "isNPlusOneTarget": false, + "isNameMatch": false, + "jlptLevel": "N2", + "frequencyRank": 745, + "className": "word word-known word-jlpt-n2", + "frequencyRankLabel": "745", + "jlptLevelLabel": "N2" + } + ] +} +``` + +#### Field reference + +| Field | Type | Notes | +| --- | --- | --- | +| `version` | number | Current websocket payload version. Today this is `1`. | +| `text` | string | Raw subtitle text. | +| `sentence` | string | HTML string with `` wrappers and `data-*` attributes for client rendering. | +| `tokens` | array | Token metadata; empty when the subtitle is not tokenized yet. | + +Each token may include: + +| Token field | Type | Notes | +| --- | --- | --- | +| `surface` | string | Display text for the token | +| `reading` | string | Kana reading when available | +| `headword` | string | Dictionary headword when available | +| `startPos` / `endPos` | number | Character offsets in the subtitle text | +| `partOfSpeech` | string | SubMiner token POS label | +| `isMerged` | boolean | Whether this token represents merged content | +| `isKnown` | boolean | Marked known by SubMiner's known-word logic | +| `isNPlusOneTarget` | boolean | True when the token is the sentence's N+1 target | +| `isNameMatch` | boolean | True for prioritized character-name matches | +| `frequencyRank` | number | Frequency rank when available | +| `jlptLevel` | string | JLPT level when available | +| `className` | string | CSS-ready class list derived from token state | +| `frequencyRankLabel` | string or `null` | Preformatted rank label for UIs | +| `jlptLevelLabel` | string or `null` | Preformatted JLPT label for UIs | + +### 2. Annotation WebSocket + +Use the annotation websocket for custom clients that want the same structured token payload the bundled texthooker UI consumes. + +- **Default URL:** `ws://127.0.0.1:6678` +- **Payload shape:** same JSON contract as the basic subtitle websocket +- **Primary difference:** this stream is intended to stay on even when the basic websocket auto-disables because `mpv_websocket` is installed + +In practice, if you are building a new client, prefer `annotationWebsocket` unless you specifically need compatibility with an existing `websocket` consumer. + +### 3. HTML markup conventions + +The `sentence` field is pre-rendered HTML generated by SubMiner. Depending on token state, it can include classes such as: + +- `word` +- `word-known` +- `word-n-plus-one` +- `word-name-match` +- `word-jlpt-n1` through `word-jlpt-n5` +- `word-frequency-single` +- `word-frequency-band-1` through `word-frequency-band-5` + +SubMiner also adds tooltip-friendly data attributes when available: + +- `data-reading` +- `data-headword` +- `data-frequency-rank` +- `data-jlpt-level` + +If you need a fully custom UI, ignore `sentence` and render from `tokens` instead. + +## Texthooker Integration Guide + +### When to use the bundled texthooker page + +Use texthooker when you want a browser tab that: + +- updates live from current subtitles +- works well with browser-based Yomitan setups +- inherits SubMiner's coloring preferences and websocket URL automatically + +Start it with either: + +```bash +subminer texthooker +``` + +or by leaving `texthooker.launchAtStartup` enabled. + +### What SubMiner injects into the page + +When SubMiner serves the local texthooker UI, it injects bootstrap values into `window.localStorage`, including: + +- `bannou-texthooker-websocketUrl` +- coloring toggles for known/N+1/name/frequency/JLPT styling +- CSS custom properties for SubMiner's token colors + +That means the bundled page already knows which websocket to connect to and which color palette to use. + +### Build a custom websocket client + +Here is a minimal browser client for the annotation stream: + +```html + + +SubMiner client +
Waiting for subtitles...
+ +``` + +### Build a custom Node client + +```js +import WebSocket from 'ws'; + +const ws = new WebSocket('ws://127.0.0.1:6678'); + +ws.on('message', (raw) => { + const payload = JSON.parse(String(raw)); + console.log({ + text: payload.text, + tokens: payload.tokens.length, + firstToken: payload.tokens[0]?.surface ?? null, + }); +}); +``` + +### Integration tips + +- Bind only to `127.0.0.1`; these services are local-only by design. +- Handle empty `tokens` arrays gracefully because subtitle text can arrive before tokenization completes. +- Reconnect on disconnect; SubMiner does not manage client reconnects for you. +- Prefer `payload.text` for logging/automation and `payload.sentence` or `payload.tokens` for UI rendering. + +## Plugin Development + +SubMiner does **not** currently expose a general-purpose third-party plugin SDK inside the app itself. Today, the supported extension surfaces are: + +1. the local websocket streams +2. the local texthooker UI +3. the mpv Lua plugin's script-message API +4. the launcher CLI + +### mpv script messages + +The mpv plugin accepts these script messages: + +```text +script-message subminer-start +script-message subminer-stop +script-message subminer-toggle +script-message subminer-menu +script-message subminer-options +script-message subminer-restart +script-message subminer-status +script-message subminer-autoplay-ready +script-message subminer-aniskip-refresh +script-message subminer-skip-intro +``` + +The start command also accepts inline overrides: + +```text +script-message subminer-start backend=hyprland socket=/custom/path texthooker=no log-level=debug +``` + +### Practical extension patterns + +#### Add another mpv script that coordinates with SubMiner + +Examples: + +- send `subminer-start` after your own media-selection script chooses a file +- send `subminer-status` before running follow-up automation +- send `subminer-aniskip-refresh` after you update title/episode metadata + +#### Build a launcher wrapper + +Examples: + +- open a media picker, then call `subminer /path/to/file.mkv` +- launch browser-only subtitle tooling with `subminer texthooker` +- disable the helper UI for a session with `subminer --no-texthooker video.mkv` + +#### Build an overlay-adjacent client + +Examples: + +- browser widget showing current subtitle + token breakdown +- local vocabulary capture helper that writes interesting lines to a file +- bridge service that forwards websocket events into your own workflow engine + +## Webhook Examples + +SubMiner does **not** currently send outbound webhooks by itself. The supported pattern is to consume the websocket locally and relay events into another system. + +That still makes webhook-style automation straightforward. + +### Example: forward subtitle lines to a local webhook receiver + +```js +import WebSocket from 'ws'; + +const ws = new WebSocket('ws://127.0.0.1:6678'); + +ws.on('message', async (raw) => { + const payload = JSON.parse(String(raw)); + + await fetch('http://127.0.0.1:5678/subminer/subtitle', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + text: payload.text, + tokens: payload.tokens, + receivedAt: new Date().toISOString(), + }), + }); +}); +``` + +### Automation ideas + +- **n8n / Make / Zapier relay:** send each subtitle line into an automation workflow for logging, translation, or summarization. +- **Discord / Slack notifier:** post only lines that contain unknown words or N+1 targets. +- **Obsidian / Markdown capture:** append subtitle lines plus token metadata to a daily immersion note. +- **Local LLM pipeline:** trigger a glossary, translation, or sentence-mining workflow whenever a new line arrives. + +### Filtering example: only forward N+1 lines + +```js +import WebSocket from 'ws'; + +const ws = new WebSocket('ws://127.0.0.1:6678'); + +ws.on('message', async (raw) => { + const payload = JSON.parse(String(raw)); + const hasNPlusOne = payload.tokens.some((token) => token.isNPlusOneTarget); + + if (!hasNPlusOne) return; + + await fetch('http://127.0.0.1:5678/subminer/n-plus-one', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ text: payload.text, tokens: payload.tokens }), + }); +}); +``` + +## Recommended Integration Combinations + +- **Browser Yomitan client:** `texthooker` + `annotationWebsocket` +- **Custom dashboard:** `annotationWebsocket` only +- **Lightweight subtitle mirror:** `websocket` only +- **mpv-side automation:** mpv plugin script messages + optional websocket relay +- **Webhook-style workflows:** `annotationWebsocket` + your own local relay service + +## Related Pages + +- [Configuration](/configuration#websocket-server) +- [Mining Workflow — Texthooker](/mining-workflow#texthooker) +- [MPV Plugin](/mpv-plugin) +- [Launcher Script](/launcher-script) +- [Anki Integration](/anki-integration#proxy-mode-setup-yomitan--texthooker)