# 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)