subocr service
Service Overview: SubOCR (https://subocr.drheck.dev/) is an end-to-end Japanese burned-in video subtitle (telop / テロップ) extraction, Kanji furigana 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.
- 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 $f_s = 2.0\text{ fps}$, or 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 AOT compilation. - Precision Note:
migraphx_fp16_enableis explicitly set to0(FP32) because FP16 causes bounding-box coordinate underflow inPP-OCRv5_server_detongfx1201.
- Detection (
- Bilingual Line Classification & Spatial Role Separation:
- Detected text boxes within each frame are sorted vertically and classified as Japanese (
ja_lines) or English (en_lines) based on character script ratios. - Within the lower subtitle band, higher-positioned smaller Japanese commentary boxes ($y_{\text{rel}} < 0.66$) are separated from the primary bottom speaker dialogue ($y_{\text{rel}} \ge 0.66$) so simultaneous on-screen editor commentary and speaker subtitles are preserved cleanly.
- Detected text boxes within each frame are sorted vertically and classified as Japanese (
- Temporal Deduplication:
- Consecutive frames whose normalized Japanese string similarity satisfies:
$$\text{sim}(S_{t}, S_{t-1}) = \frac{2 \cdot |M|}{|S_t| + |S_{t-1}|} \ge 0.78$$
are merged into a single timed subtitle cue $[t_{\text{start}}, t_{\text{end}}]$, 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}(S_{t}, S_{t-1}) = \frac{2 \cdot |M|}{|S_t| + |S_{t-1}|} \ge 0.78$$
are merged into a single timed subtitle cue $[t_{\text{start}}, t_{\text{end}}]$, 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カ国).
- Post-processing compares
- Deterministic Kanji-Only Furigana (
fugashi+unidic-lite):- Rather than relying on LLM-generated furigana (which can hallucinate or attach 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>漢字<rt>かんじ</rt></ruby>annotations strictly above Kanji characters, leaving hiragana and katakana unannotated.
- Rather than relying on LLM-generated furigana (which can hallucinate or attach 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.
- 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 regenerates Kanji-only furigana (
readingandruby_html) and updates the exported.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 fully 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) 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_0844.30