mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-22 12:11:27 -07:00
Compare commits
3 Commits
youtube-su
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0317c7f011 | |||
|
13797b5005
|
|||
|
b24d9d7487
|
21
CHANGELOG.md
21
CHANGELOG.md
@@ -2,19 +2,20 @@
|
|||||||
|
|
||||||
## v0.8.0 (2026-03-22)
|
## v0.8.0 (2026-03-22)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Overlay: Added the subtitle sidebar feature with a new `subtitleSidebar` configuration surface.
|
||||||
|
- Overlay: Added a sidebar modal with cue list rendering, click-to-seek, active-cue highlighting, and embedded layout support.
|
||||||
|
- IPC: Added sidebar snapshot plumbing between renderer and main process for overlay/sidebar synchronization.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Docs: Refreshed the vendored Texthooker docs/index.html bundle to match the latest local build artifacts.
|
- Config: Added hot-reloadable sidebar options for enablement, layout, visibility, typography, opacity, sizing, and interaction behavior (`autoOpen`, `pauseOnHover`, `autoScroll`, toggle key).
|
||||||
|
- Docs: Added full `subtitleSidebar` documentation coverage, including sample config, option table, and toggle shortcut notes.
|
||||||
|
- Runtime: Improved subtitle prefetch and rendering flow so sidebar and overlay subtitle states are kept in sync across media transitions.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Anki: Known-word cache refreshes now reconcile Anki changes incrementally instead of wiping and rebuilding on startup, mined cards can append their word into the cache immediately through a new default-enabled config flag, and explicit refreshes now run through `subminer doctor --refresh-known-words`.
|
- Overlay: Kept sidebar cue tracking stable across playback transitions and timing edge cases.
|
||||||
- Subtitle: Restored known-word coloring and JLPT underlines for subtitle tokens like `大体` when the subtitle token is kanji but the known-word cache only matches the kana reading.
|
- Overlay: Improved sidebar resume/start behavior to jump directly to the first resolved active cue.
|
||||||
- Stats: Episode progress in the anime page now uses the last ended playback position instead of cumulative active watch time, avoiding distorted percentages after rewatches or repeated sessions.
|
- Overlay: Stopped stale subtitle refreshes from regressing active-cue and text state.
|
||||||
- Stats: Anime episode progress now keeps the latest known playback position through active-session checkpoints and stale-session recovery, so recently watched episodes no longer lose their progress percentage.
|
|
||||||
- Stats: Anime episode progress now falls back to the latest retained subtitle/event timing when a session is missing a persisted playback-position checkpoint, so older watch sessions no longer get stuck at `0%` progress.
|
|
||||||
- Overlay: Kept subtitle sidebar cue tracking stable across transitions by avoiding cue-line regression on subtitle timing edge cases and stale text updates.
|
|
||||||
- Overlay: Improved sidebar config by documenting and exposing layout mode and typography options (`layout`, `fontFamily`, `fontSize`) in the generated documentation flow.
|
|
||||||
- Overlay: Added `subtitleSidebar.autoOpen` (default `false`) to open the subtitle sidebar once during overlay startup when the sidebar feature is enabled.
|
|
||||||
- Overlay: Made subtitle sidebar resume/start positioning jump directly to the first resolved active cue instead of smooth-scrolling through the full list, while keeping smooth auto-follow for later cue changes.
|
|
||||||
|
|
||||||
## v0.7.0 (2026-03-19)
|
## v0.7.0 (2026-03-19)
|
||||||
|
|
||||||
|
|||||||
5
changes/2026-03-22-websocket-texthooker-docs.md
Normal file
5
changes/2026-03-22-websocket-texthooker-docs.md
Normal file
@@ -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.
|
||||||
@@ -95,6 +95,7 @@ export default {
|
|||||||
{ text: 'Building & Testing', link: '/development' },
|
{ text: 'Building & Testing', link: '/development' },
|
||||||
{ text: 'Architecture', link: '/architecture' },
|
{ text: 'Architecture', link: '/architecture' },
|
||||||
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
|
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
|
||||||
|
{ text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
|
||||||
{ text: 'Changelog', link: '/changelog' },
|
{ text: 'Changelog', link: '/changelog' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v0.8.0 (2026-03-22)
|
## v0.8.0 (2026-03-22)
|
||||||
- Refreshed the vendored Texthooker docs/index.html bundle to match the latest local build artifacts.
|
- Added a configurable subtitle sidebar feature (`subtitleSidebar`) with overlay/embedded rendering, click-to-seek cue list, and hot-reloadable visibility and behavior controls.
|
||||||
- Added incremental known-word cache refresh behavior so mined cards can append cache entries immediately and `subminer doctor --refresh-known-words` is now the explicit full refresh path.
|
- Added release docs updates for sidebar configuration, including options, sample config, and toggle shortcut behavior.
|
||||||
- Fixed known-word/JLPT subtitle styling so tokens like `大体` keep expected coloring even when only the kana reading is in cache.
|
- Synced sidebar and overlay subtitle states during playback transitions via IPC-backed snapshot plumbing.
|
||||||
- Fixed anime progress to use last ended playback position and keep latest known checkpoint across sessions, preventing stale or zero percent regressions.
|
- Fixed sidebar cue tracking to remain stable across timing edge cases and stale subtitle refreshes.
|
||||||
- Kept subtitle sidebar cue tracking stable across transitions and improved sidebar configuration documentation for `layout`, `fontFamily`, and `fontSize`.
|
- Improved sidebar resume/start behavior by jumping directly to the first resolved active cue.
|
||||||
- Added `subtitleSidebar.autoOpen` to open the subtitle sidebar at startup when enabled.
|
|
||||||
- Improved sidebar resume/start behavior to jump directly to the active cue on resume while preserving auto-follow smooth motion.
|
|
||||||
|
|
||||||
## v0.7.0 (2026-03-19)
|
## v0.7.0 (2026-03-19)
|
||||||
- Added a full local immersion dashboard release line with Overview, Library, Trends, Vocabulary, and Sessions drill-down views backed by SQLite tracking data.
|
- Added a full local immersion dashboard release line with Overview, Library, Trends, Vocabulary, and Sessions drill-down views backed by SQLite tracking data.
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
See `config.example.jsonc` for detailed configuration options.
|
||||||
|
|||||||
@@ -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.
|
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)
|
## 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).
|
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).
|
||||||
|
|||||||
357
docs-site/websocket-texthooker-api.md
Normal file
357
docs-site/websocket-texthooker-api.md
Normal file
@@ -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": "<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:
|
||||||
|
|
||||||
|
```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
|
||||||
|
<!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
|
||||||
|
|
||||||
|
```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)
|
||||||
@@ -95,6 +95,43 @@ test('writeChangelogArtifacts ignores README, groups fragments by type, writes r
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('writeChangelogArtifacts skips changelog prepend when release section already exists', async () => {
|
||||||
|
const { writeChangelogArtifacts } = await loadModule();
|
||||||
|
const workspace = createWorkspace('write-artifacts-existing-version');
|
||||||
|
const projectRoot = path.join(workspace, 'SubMiner');
|
||||||
|
const existingChangelog = [
|
||||||
|
'# Changelog',
|
||||||
|
'',
|
||||||
|
'## v0.4.1 (2026-03-07)',
|
||||||
|
'### Added',
|
||||||
|
'- Existing release bullet.',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
fs.mkdirSync(projectRoot, { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(projectRoot, 'CHANGELOG.md'), existingChangelog, 'utf8');
|
||||||
|
fs.writeFileSync(path.join(projectRoot, 'changes', '001.md'), ['type: added', 'area: overlay', '', '- Stale release fragment.'].join('\n'), 'utf8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = writeChangelogArtifacts({
|
||||||
|
cwd: projectRoot,
|
||||||
|
version: '0.4.1',
|
||||||
|
date: '2026-03-08',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result.deletedFragmentPaths, [path.join(projectRoot, 'changes', '001.md')]);
|
||||||
|
assert.equal(fs.existsSync(path.join(projectRoot, 'changes', '001.md')), false);
|
||||||
|
|
||||||
|
const changelog = fs.readFileSync(path.join(projectRoot, 'CHANGELOG.md'), 'utf8');
|
||||||
|
assert.equal(changelog, existingChangelog);
|
||||||
|
const releaseNotes = fs.readFileSync(path.join(projectRoot, 'release', 'release-notes.md'), 'utf8');
|
||||||
|
assert.match(releaseNotes, /## Highlights\n### Added\n- Existing release bullet\./);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(workspace, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('verifyChangelogReadyForRelease ignores README but rejects pending fragments and missing version sections', async () => {
|
test('verifyChangelogReadyForRelease ignores README but rejects pending fragments and missing version sections', async () => {
|
||||||
const { verifyChangelogReadyForRelease } = await loadModule();
|
const { verifyChangelogReadyForRelease } = await loadModule();
|
||||||
const workspace = createWorkspace('verify-release');
|
const workspace = createWorkspace('verify-release');
|
||||||
|
|||||||
@@ -341,12 +341,34 @@ export function writeChangelogArtifacts(options?: ChangelogOptions): {
|
|||||||
const version = resolveVersion(options ?? {});
|
const version = resolveVersion(options ?? {});
|
||||||
const date = resolveDate(options?.date);
|
const date = resolveDate(options?.date);
|
||||||
const fragments = readChangeFragments(cwd, options?.deps);
|
const fragments = readChangeFragments(cwd, options?.deps);
|
||||||
const releaseSection = buildReleaseSection(version, date, fragments);
|
|
||||||
const existingChangelogPath = path.join(cwd, 'CHANGELOG.md');
|
const existingChangelogPath = path.join(cwd, 'CHANGELOG.md');
|
||||||
const existingChangelog = existsSync(existingChangelogPath)
|
const existingChangelog = existsSync(existingChangelogPath)
|
||||||
? readFileSync(existingChangelogPath, 'utf8')
|
? readFileSync(existingChangelogPath, 'utf8')
|
||||||
: '';
|
: '';
|
||||||
const outputPaths = resolveChangelogOutputPaths({ cwd });
|
const outputPaths = resolveChangelogOutputPaths({ cwd });
|
||||||
|
const existingReleaseSection = extractReleaseSectionBody(existingChangelog, version);
|
||||||
|
if (existingReleaseSection !== null) {
|
||||||
|
log(`Existing section found for v${version}; skipping changelog prepend.`);
|
||||||
|
for (const fragment of fragments) {
|
||||||
|
rmSync(fragment.path);
|
||||||
|
log(`Removed ${fragment.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseNotesPath = writeReleaseNotesFile(
|
||||||
|
cwd,
|
||||||
|
existingReleaseSection,
|
||||||
|
options?.deps,
|
||||||
|
);
|
||||||
|
log(`Generated ${releaseNotesPath}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletedFragmentPaths: fragments.map((fragment) => fragment.path),
|
||||||
|
outputPaths,
|
||||||
|
releaseNotesPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseSection = buildReleaseSection(version, date, fragments);
|
||||||
const nextChangelog = prependReleaseSection(existingChangelog, releaseSection, version);
|
const nextChangelog = prependReleaseSection(existingChangelog, releaseSection, version);
|
||||||
|
|
||||||
for (const outputPath of outputPaths) {
|
for (const outputPath of outputPaths) {
|
||||||
|
|||||||
Reference in New Issue
Block a user