subocr service
Service Overview: SubOCR (https://subocr.drheck.dev/) is an end-to-end Japanese burned-in video subtitle (telop / テロップ) extraction, 漢字 振り仮名 annotation, offline JMdict + JLPT (N5–N1) morphological dictionary lookup, 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 \text{PP-OCRv5 (MIGraphX FP32)} \longrightarrow \text{Qwen2.5-32B (ROCm)} \longrightarrow \text{UniDic + JMdict/JLPT 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/ - Databases:
- Application Database:
/data/docker/subocr/data/subocr.db(SQLite 3 in WAL mode; stores jobs, timed cues, readings, translations, and custom edits) - Offline Dictionary & JLPT Database:
/data/docker/subocr/data/jmdict_jlpt.db(SQLite 3, 63 MB; 218,856 EDRDG JMdict entries, 499,325 indexed kanji/kana forms, and 7,972 JLPT N5–N1 vocabulary entries)
- Application Database:
- 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 & Morphological Dictionary 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) followed by deterministic morphological and dictionary analysis:
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, Kanji-Only Furigana & Offline Dictionary Tagging
- 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 contextual idiom/slang notes (vocab).
- 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):- Morphological tokenization (
fugashi+unidic-lite+jaconv) ingenerate_kanji_furigana()strips leading, trailing, and internal okurigana and attaches<ruby>annotations strictly above Kanji characters, leaving hiragana and katakana unannotated: - Example Render: 世界の違いに6カ国が大混乱 — それ考えたらすごい日本っぽいっていうか
- Morphological tokenization (
- Exhaustive Per-Word Tokenization, Grammar Categorization & Offline JMdict / JLPT Lookup (
analyze_cue_tokens):- Every word in every subtitle cue is tokenized by
fugashi(MECAB_TAGGER), lemmatized to its dictionary base form (orthBase), and matched against the local read-only/data/docker/subocr/data/jmdict_jlpt.dbSQLite database and a curatedGRAMMAR_GLOSS_MAPfor Japanese particles and colloquial auxiliaries (てる/てん,ちゃう,たら,じゃん, Kansaiねん/やん/へん, etc.). - Each token is assigned:
- JLPT Level Badge (
jlpt):N5,N4,N3,N2,N1, orOther GrammarPart-of-SpeechCategoryFilter & Chip Tag (pos_cat/pos_label): Grouped under the 6filter buttons (Noun品詞,Noun名詞,Pronoun動詞,Proper Noun形容詞Counter),Verb(,Ichidan Verb副詞,Godan Verb表現Suru Verb,Kuru Verb, displayed assurface (base)when conjugated),Adj(i-Adj,)na-Adj助詞,Adv(Adverb),Expr(Interjection,Conjunction,Idiom / Context),andlabeled on each vocabulary chip with its specific Japanese grammatical category (ParticleCaseseeParticle,theTopiccompleteParticle,referenceEndtableParticle,Aux Verb,Suffix)below)- English Gloss (
meaning): Offline JMdict definition or grammatical function
- JLPT Level Badge (
- Every word in every subtitle cue is tokenized by
Reference Table: Japanese Part-of-Speech (品詞) Filter Groups & Vocabulary Chip Tags
pos_label)
Reading (振り仮名)
Filter Button (品詞)
English Translation
Grammatical Role & Subtitle Examples
名詞
名詞 (meishi)
名詞 (Noun)
Common Noun
General nouns and suru-noun stems (e.g., 世界, 違い, 大混乱)
固有名詞
固有名詞 (koyū meishi)
名詞 (Noun)
Proper Noun
Places, countries, people, and brand names (e.g., 日本, 関西, フランス)
代名詞
代名詞 (daimeishi)
名詞 (Noun)
Pronoun
Personal and demonstrative pronouns (e.g., 私, 僕, これ, それ, どこ)
助数詞
助数詞 (josūshi)
名詞 (Noun)
Counter
Numerical counter suffixes (e.g., カ国, 人, 回, つ, 個)
動詞
動詞 (dōshi)
動詞 (Verb)
Verb
All main verbs (Godan, Ichidan, Suru, Kuru); displayed as conjugated (dictionary form) when inflected (e.g., 言った (言う), 考えたら (考える))
形容詞
形容詞 (keiyōshi)
形容詞 (Adjective)
i-Adjective
Inflecting adjectives ending in -i (e.g., すごい, 面白い, 無い)
形容動詞
形容動詞 (keiyōdōshi)
形容詞 (Adjective)
na-Adjective
Adjectival nouns that take な before nouns (e.g., 好き, 大変, 複雑)
連体詞
連体詞 (rentaishi)
形容詞 (Adjective)
Pre-noun Adjectival
Non-inflecting words that directly modify nouns (e.g., この, その, あんな, いろんな)
副詞
副詞 (fukushi)
副詞 (Adverb)
Adverb
Words modifying verbs, adjectives, or clauses (e.g., ちょっと, めっちゃ, 全然, やっぱり)
表現
表現 (hyōgen)
表現 (Expression)
Expression / Idiom
Multi-word idioms, set phrases, and contextual slang compounds
感動詞
感動詞 (kandōshi)
表現 (Expression)
Interjection
Exclamations, greetings, and conversational fillers (e.g., えっ, はい, うわ, なるほど)
接続詞
接続詞 (setsuzokushi)
表現 (Expression)
Conjunction
Sentence/clause connectors (e.g., でも, だから, そして, ただ)
助詞
助詞 (joshi)
助詞 (Particle)
Particle (General)
General or adverbial/quotative particles (e.g., って, とか, だけ, くらい)
格助詞
格助詞 (kakujoshi)
助詞 (Particle)
Case Particle
Marks grammatical case relationships (e.g., が, を, に, で, と, から)
係助詞
係助詞 (kakarijoshi)
助詞 (Particle)
Topic / Binding Particle
Marks topic, contrast, or inclusion (e.g., は, も, こそ)
終助詞
終助詞 (shūjoshi)
助詞 (Particle)
Sentence-Ending Particle
Adds tone, question, or emphasis at clause end (e.g., ね, よ, な, か, じゃん)
接続助詞
接続助詞 (setsuzokujoshi)
助詞 (Particle)
Conjunctive Particle
Connects clauses (e.g., て/で, けど, から, ば, のに, し)
助動詞
助動詞 (jodōshi)
助詞 (Particle)
Auxiliary Verb
Inflecting verbal/copular endings and colloquial contractions (e.g., だ/です, た, ない, ます, てる/てん, ちゃう, ねん, やん, へん)
接頭辞
接頭辞 (settōji)
助詞 (Particle)
Prefix
Honorific or intensifying prefixes attached to stems (e.g., お, ご, 大, 超)
接尾辞
接尾辞 (setsubiji)
助詞 (Particle)
Suffix
Meaning-modifying suffixes attached to stems (e.g., さん, たち, っぽい, 的, 目)
3. Web Application Layout & Study Controls
SubOCR uses a sharp-cornered (border-radius: 0) layout 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.
- Provides direct export and download links (
JA .SRT,Bilingual .SRT,WebVTT .VTT,Anki .TSV,Study .MD,JSON, andSave to /data) on each OCR job card. - Supports pinning (
★), category filtering, searching, sorting (Pinned & Newest,Oldest First,Title (A–Z),Most Cues), inline renaming/re-categorization (Organize), and job deletion (Delete).
- 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}):- Full-viewport two-column workspace without a top header bar: compact
← Librarylink, category badge, and small muted video title above the sticky HTML5 video player on the left, paired with a full-height synchronized subtitle cue list on the right (native video player WebVTT overlays are disabled by default so burned-in subtitles remain unobstructed). - 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). - Fixed-Width Display & Vocabulary Filter Controls (under the player controls on the left, persisted in
localStoragewith fixed-widthON/OFFlabels that never shift button size):- Display Toggles:
Japanese Subs,Furigana,English Sub,Vocab, andOriginal OCR - JLPT Level Filters:
N5,N4,N3,N2,N1,Other Grammar品詞Type(Part-of-Speech) Filters:(Noun),Noun名詞(Verb),Verb動詞(Adjective),Adj形容詞(Adverb),Adv副詞(Expression),Expr表現助詞(Particle / Auxiliary)
- Display Toggles:
- 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), re-runs offlineJMdict + JLPTmorphological token analysis, updates the card in place, and writes updated.srt,.bilingual.srt,.vtt,.anki.tsv, and.study.mdfiles on disk.
- Full-viewport two-column workspace without a top header bar: compact
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/), the static JMdict reference database (data/jmdict_jlpt.db), 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