Files
SubMiner/docs-site/websocket-texthooker-api.md

12 KiB

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.

{
  "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, 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

{
  "version": 1,
  "text": "無事",
  "sentence": "<span class=\"word word-known word-jlpt-n2\" data-reading=\"ぶじ\" data-headword=\"無事\" data-frequency-rank=\"745\" data-jlpt-level=\"N2\">無事</span>",
  "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 <span> 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:

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:

<!doctype html>
<meta charset="utf-8" />
<title>SubMiner client</title>
<div id="subtitle">Waiting for subtitles...</div>
<script>
  const subtitle = document.getElementById('subtitle');
  const ws = new WebSocket('ws://127.0.0.1:6678');

  ws.addEventListener('message', (event) => {
    const payload = JSON.parse(event.data);
    subtitle.innerHTML = payload.sentence || payload.text;
  });

  ws.addEventListener('close', () => {
    subtitle.textContent = 'Connection closed; reload or reconnect.';
  });
</script>

Build a custom Node client

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:

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:

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

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

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 }),
  });
});
  • 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