subocr service
Service Overview: SubOCR (https://subocr.drheck.dev/) is an end-to-end Japanese burned-in video subtitle (telop / テロップ) extraction, 漢字 振り仮名 annotation, LLM study-note refinement, and interactive video study platform running on host ul, accelerated by the AMD Radeon AI PRO R9700 (gfx1201, RDNA 4, 32 GB VRAM) GPU.
1. Architecture & Network Topology
SubOCR is deployed as a containerized FastAPI + ONNX Runtime (ROCm 7.2 MIGraphX) service bound to a dedicated IPv6 ULA address on eth77, reverse-proxied through Nginx with Authentik ForwardAuth single sign-on, and paired with the local ollama-rocm (qwen2.5:32b) LLM engine on the same GPU.
$$\text{Video Frame } I(t) ;\longrightarrow;longrightarrow \text{PP-OCRv5 (MIGraphX FP32)} ;\longrightarrow;longrightarrow \text{Qwen2.5-32B (ROCm)} ;\longrightarrow;longrightarrow \text{UniDic }\langle\text{ruby}\rangle\text{ Study View}$$
- Public URL: https://subocr.drheck.dev/
- Dedicated ULA IPv6 Address:
fd77:7777:5010:0:1337:f1ac:d00d:0c70(eth77, port8000) - Host Directory:
/data/docker/subocr/ - Database:
/data/docker/subocr/data/subocr.db(SQLite 3 in WAL mode) - Export & Sidecar Storage:
- Per-job exports:
/data/docker/subocr/data/jobs/<job_id>/ - Automatic sidecar writes next to local videos:
/data/language/Japanese/Video/...(mounted read-write at/videosinside the container)
- Per-job exports:
- Authentication: Authentik ForwardAuth (
subocrProxyProvider, 30-day token validity) via/etc/nginx/sites-available/subocr - DNS Resolution:
- Local Unbound (
danknasty,@10.0.0.1):A 10.0.0.100,AAAA fd77:7777:5010::1337:f1ac:d00d:aaaa - Public Authoritative DNS (deSEC):
AAAA fd77:7777:5010::1337:f1ac:d00d:aaaa
- Local Unbound (
2. Two-Stage GPU Processing Pipeline
When a job is submitted from https://subocr.drheck.dev/new (either by selecting a local video under /data/language/Japanese/Video, uploading a video file, or providing a YouTube URL fetched via yt-dlp), SubOCR executes a two-stage GPU pipeline on the AMD Radeon AI PRO R9700 (gfx1201):
Stage 1: ROCm 7.2 MIGraphX Visual Subtitle OCR (PP-OCRv5)
- Frame Sampling & Spatial Crop:
- OpenCV samples the video at a configurable rate $f_s$ (default $2.0\text{ fps}$, or frame interval $\Delta t = 0.5\text{ s}$).
- To isolate burned-in subtitles and ignore top-of-screen variety-show topic banners by default, frames are cropped to the lower subtitle region $y \in [0.58H, 0.98H]$.
- GPU Inference (
MIGraphXExecutionProviderFP32):- Detection (
PP-OCRv5_server_det_infer.onnx) and recognition (japanese_PP-OCRv5_mobile_rec_infer.onnx) run viaonnxruntime-rocmwithMIGraphXExecutionProvider. - Compiled MIGraphX kernel plans (
.mxr) are cached in/data/docker/subocr/cache/migraphxso subsequent container restarts bypass ahead-of-time compilation. - Precision Note:
migraphx_fp16_enableis explicitly set to0(FP32) because FP16 causes bounding-box coordinate underflow inPP-OCRv5detection ongfx1201.
- Detection (
- Bilingual Line Classification & Spatial Role Separation:
- Detected text boxes within each frame are sorted vertically and classified into Japanese (
ja_lines) or English (en_lines) based on character script ratios. - Within the lower subtitle band, higher-positioned smaller Japanese commentary boxes ($y < 0.66H$) are separated from the primary bottom speaker dialogue ($y \ge 0.66H$) so simultaneous on-screen editor commentary and speaker subtitles are preserved cleanly.
- Detected text boxes within each frame are sorted vertically and classified into Japanese (
- Temporal Deduplication:
- Consecutive frames whose normalized Japanese string similarity satisfies:
$$\text{sim}(A, B) = \frac{2 \cdot |M|}{|A| + |B|} \ge 0.78$$
are merged into a single timed subtitle cue $[t_0, t_1]$, selecting the highest-confidence OCR recognition frame as the canonical raw text (
ja_raw) and storing the initial English line (en_raw).
- Consecutive frames whose normalized Japanese string similarity satisfies:
$$\text{sim}(A, B) = \frac{2 \cdot |M|}{|A| + |B|} \ge 0.78$$
are merged into a single timed subtitle cue $[t_0, t_1]$, selecting the highest-confidence OCR recognition frame as the canonical raw text (
Hardware & ROCm Note: Always keep MIGRAPHX_DISABLE_MIOPEN_FUSION=1 and migraphx_fp16_enable=0 enabled in /data/docker/subocr/docker-compose.yml. Enabling FP16 on gfx1201 degrades bounding-box detection recall in PP-OCRv5.
Stage 2: Contextual LLM Refinement & Deterministic Furigana
- Batched Contextual Refinement (
qwen2.5:32bonollama-rocm):- Extracted cues are processed in batches of 15 cues against
http://172.28.0.1:11434/api/chat(qwen2.5:32b). - The LLM receives both the raw Japanese OCR (
ja_raw) and any burned-in English subtitle (en_raw), repairing single-character visual OCR confusions (e.g., katakanaニ/二,カ/力, or visually degraded glyphs) while preserving the original spoken phrasing, slang, and dialect. - If
en_rawis present and accurate, it is cleaned; if missing or garbled, a natural English translation (en_text) is synthesized alongside 1–3 key vocabulary/grammar notes (vocab) per cue.
- Extracted cues are processed in batches of 15 cues against
- Punctuation & On-Screen Symbol Restoration (
restore_raw_punctuation):- Post-processing compares
ja_rawandja_textto ensure the LLM never strips on-screen quotation marks ("...",「...」,『...』), wave dashes (~,〜), exclamation/question marks (!,?), ellipses (…), or counter katakana (何カ国,6カ国). - Example preserved cue:
"スペシャルゲスト~!"が<ruby>無<rt>な</rt></ruby>くなり<ruby>皆<rt>みな</rt></ruby>メンバーに
- Post-processing compares
- Deterministic Kanji-Only Furigana (
fugashi+unidic-lite):- Rather than relying on LLM-generated furigana (which can hallucinate readings or attach redundant readings to hiragana/katakana), SubOCR uses morphological tokenization (
fugashi+unidic-lite+jaconv) ingenerate_kanji_furigana()to strip leading, trailing, and internal okurigana and attach<ruby>annotations strictly above Kanji characters, leaving hiragana and katakana unannotated: - Example Render: 世界の違いに6カ国が大混乱 — それ考えたらすごい日本っぽいっていうか
- Rather than relying on LLM-generated furigana (which can hallucinate readings or attach redundant readings to hiragana/katakana), SubOCR uses morphological tokenization (
3. Web Application Layout & Study Controls
SubOCR is organized into three focused pages:
- Main OCR Library (https://subocr.drheck.dev/):
- Displays all OCR jobs with status badges, cue counts, video durations, and category folders.
- Supports pinning (
📌), category filtering, searching, sorting (Newest First,Oldest First,Title (A–Z),Most Cues), inline renaming/re-categorization (✏️ Organize), and job deletion (🗑).
- New Subtitle OCR (https://subocr.drheck.dev/new):
- Dedicated job submission form for local videos under
/data/language/Japanese/Video, direct file uploads, or YouTube URLs, with optional start/end timestamps (start_sec,end_sec), frame sampling FPS, and category assignment.
- Dedicated job submission form for local videos under
- Dedicated OCR Study View (
https://subocr.drheck.dev/ocr/{job_id}):- Sticky HTML5 video player on the left paired with a full-height synchronized subtitle cue list on the right.
- Clicking any subtitle card seeks the video to that cue's start timestamp and highlights the active card in high-contrast amber/gold (
#fbbf24border/pill and#fef08atext). - Keyboard Shortcuts (active whenever focus is outside text inputs):
SpaceorK: Play / Pause video without scrolling the pageAor←: Jump to previous subtitle cueDor→: Jump to next subtitle cueR: Replay current subtitle cue from its start timestampL: Toggle single-cue A/B loop mode (Loop: ON/OFF)
- Inline Cue Editing:
- Clicking Edit on any cue card opens an inline editor for Japanese text, optional custom furigana reading override, English meaning, and vocabulary notes.
- Editing the Japanese text automatically clears stale readings, regenerates Kanji-only furigana (
readingandruby_html), updates the card in place, and writes updated.srt,.bilingual.srt,.vtt,.anki.tsv, and.study.mdfiles on disk.
4. Operations, Maintenance & Backup / Restore
Container Management
Click to expand Docker Compose & Service Management Commands
# Check container status and logs
$ docker ps --filter name=subocr
$ docker logs -f --tail 100 subocr
# Restart after modifying /data/docker/subocr/app.py
$ docker restart subocr
# Rebuild container image if Dockerfile dependencies change
$ docker compose -f /data/docker/subocr/docker-compose.yml up -d --build
Automated System Backup & Restore
Backup Integration: SubOCR is integrated into the standard host ul backup and restore suite under /home/luke/git/backups/ul/scripts/ and runs automatically during sudo /home/luke/git/backups/ul/scripts/run_all_backups.
- Backup Script (
/home/luke/git/backups/ul/scripts/backup/subocr):- Rotates any existing backup in
/data/backups/services/current/subocr/to/data/backups/services/archive/subocr/. - Executes a non-blocking SQLite WAL checkpoint (
PRAGMA wal_checkpoint(TRUNCATE);) on/data/docker/subocr/data/subocr.dbso the database file is completely self-contained. - Creates a compressed archive (
subocr_data.tar.xz, ~325 KB) of/data/docker/subocr/includingapp.py,Dockerfile,docker-compose.yml,static/,data/subocr.db, and all exported job subtitle/study files (data/jobs/*/*.srt,.vtt,.anki.tsv,.study.md), while excluding transient model/kernel caches (cache/) and uploaded raw video files (data/jobs/*/*.mp4). - Generates 10% Reed-Solomon parity protection archives (
subocr_data.tar.xz.par2) viapar2 create -r10.
- Rotates any existing backup in
- Restore Script (
/home/luke/git/backups/ul/scripts/restore/subocr):- Verifies archive integrity against the
par2parity data (par2 verify). - Extracts
subocr_data.tar.xzinto/data/docker/subocr/. - Restarts the
subocrcontainer so the restored database and configuration take effect immediately.
- Verifies archive integrity against the
# Run a standalone SubOCR backup
$ sudo /home/luke/git/backups/ul/scripts/backup/subocr
# Restore from the latest backup in /data/backups/services/current/subocr/
$ sudo /home/luke/git/backups/ul/scripts/restore/subocr
# Restore from a specific timestamped backup
$ sudo /home/luke/git/backups/ul/scripts/restore/subocr 2026-10-03_0845.06