18 KiB
OCR module
Batch-transcribes (or translates) scanned pages into per-page .txt
files. Light novel and manga pages are handled by separate scripts,
since their layouts need genuinely different logic — dense running prose
vs. scattered speech bubbles that benefit from translation, a glossary,
and cross-page continuity context:
| Light novel | Manga | |
|---|---|---|
| Recommended script | novel_ocr.py |
manga_ocr_llm.py |
| Pure-OCR alternative | google_vision_ocr.py --mode novel |
google_vision_ocr.py --mode manga, local_mangaocr_ocr.py |
| Layout assumed | Dense running prose, read top-to-bottom then right-to-left | Speech bubbles / narration boxes / SFX scattered across panels |
| Output | Continuous transcribed text per page | Numbered list, one entry per bubble, in manga reading order |
| Translation | --translate, with glossary + cross-page context |
--translate, with glossary + cross-page context |
novel_ocr.py and manga_ocr_llm.py share their translate/state/logging
infrastructure via common.py — the only thing genuinely
different between them is layout (reading order, output format), which is
exactly why they're still two scripts rather than split along an
OCR-vs-translate axis: translating manga still needs to know about panels
and bubble types, and translating a novel would still want the same
glossary/context machinery, so the layout split is what actually avoids
duplicated logic.
For a light novel, this is step 1 of the pipeline — step 2 is
epub_builder, which turns the resulting
.txt files into a finished .epub. For manga, these .txt files are
meant as reference for your own typesetting workflow; epub_builder
targets prose light novels and doesn't lay out manga pages.
novel_ocr.py (recommended for light novels)
Reads each page with a multimodal LLM via any OpenAI-compatible API
(OpenRouter, a direct provider endpoint, a self-hosted proxy, etc. —
including Gemini, GPT-4V-class models, or anything else exposed through
such an endpoint) and transcribes the vertical Japanese prose — or
translates it directly with --translate (same glossary/context
machinery as manga_ocr_llm.py, via the shared common.py).
Kept deliberately simpler than manga_ocr_llm.py on the transcription
side — no bubble types/reading-order logic — since a novel page is one
linear column of prose, not scattered panels.
pip install openai pillow natsort tqdm
- From the repo root, copy
config.example.jsontoconfig.jsonand fill in theocrsection'sapi_key,base_url(your provider's OpenAI-compatible endpoint, usually ending in/v1), andmodelidentifier. This same file is shared withmanga_ocr_llm.pyand theepub_buildermodule.temperature/max_tokens/top_p/reasoning_effortare optional — see API parameters. - Optionally edit
prompt_novel.txt(transcription) orprompt_novel_translate.txt(--translate) — plain English text, no need to touch any code to tweak the instructions given to the model.
python novel_ocr.py --input ./pages --output ./out
# ...or translate directly instead of transcribing:
python novel_ocr.py --input ./pages --output ./out --translate --target-lang Russian
--input: folder with scanned page images (jpg/png/...), named so that alphabetical sorting matches page order (natural sort is used, sopage2.jpgandpage10.jpgsort correctly too).--output: folder for results. Createspages_txt/<name>.txt(one file per page), acombined.mdpreview of the whole run, and astate.jsonmanifest (see below).- If interrupted, just re-run with the same
--output— pages that already have a.txtfile are skipped, so nothing already done gets re-sent (and re-billed). - A page that fails to OCR (network error, rate limit, truncated/empty response, etc.) does not get a file written for it, specifically so the next run retries it instead of silently treating it as done.
- A page the model judges to be empty (illustration-only, blank, or a
cover/technical page) gets a file containing exactly the literal text
[NO_TEXT]— this is a deliberate marker, not an OCR failure. Theepub_buildermodule knows to treat it as "no text on this page". --pages "3,7-12,20"limits the run to just those pages (1-based positions in the sorted page list) — combine with--redoto re-OCR/ re-translate just a few pages, e.g. after tweaking the prompt or glossary, without redoing the whole book.--redore-runs pages even if a.txtalready exists for them.--start-page Nskips everything before page N entirely (not just a relabelling — pages before it are never sent).--no-stateignoresstate.jsonand falls back to plain "file exists = done" behaviour.--sleep Nadds a delay (seconds) between requests if you're hitting rate limits.--configoverrides the config.json path (defaults to the repo root).--prompt-fileoverrides the prompt file (defaults toprompt_novel.txt, orprompt_novel_translate.txtwith--translate).
state.json
Both novel_ocr.py and manga_ocr_llm.py write a state.json manifest to
--output, recording (per page) the model, a hash of the exact prompt
used, token usage, and a timestamp. This is what makes --redo/--pages
targeted re-runs possible, and it's also how the script notices (and warns
about, non-fatally) pages that were transcribed/translated under a prompt
or glossary that's since changed — it won't re-OCR them on its own, but
tells you to consider --redo for them.
manga_ocr_llm.py (recommended for manga)
Same idea as novel_ocr.py — any OpenAI-compatible API, whole page in one
request — but built around what a manga page actually needs: bubbles
transcribed as a numbered, typed, reading-order list, optional direct
translation, a glossary for consistent names/terms, and continuity context
from previous pages.
pip install openai pillow natsort tqdm
Setup is identical to novel_ocr.py (same config.json, same ocr
section) — just point at prompt_manga.txt / prompt_manga_translate.txt
if you want to edit the instructions.
# transcribe
python manga_ocr_llm.py --input ./pages --output ./out
# translate directly instead
python manga_ocr_llm.py --input ./pages --output ./out --translate --target-lang Russian
Output format is a numbered list per page:
1. [DIALOGUE] ...
2. [SFX] ...
3. [NARRATION] ...
where entries are ordered manga-style (panels right-to-left top-to-bottom;
bubbles within a panel the same way), and TYPE is one of DIALOGUE,
THOUGHT, NARRATION, or SFX. Same --input/--output/--pages/--redo/
--start-page/--no-state/--sleep/--config/
--prompt-file conventions as novel_ocr.py.
Translation (--translate)
--target-lang: written out as a plain language name (Russian,English,Spanish, ...) — it's dropped directly into the prompt as "translate all text on the page into {target-lang}". Defaults toEnglish.- This is a plain LLM translation, not a professional-quality localization pass — treat it as a strong first draft to edit, not a final one.
- Use a different
--outputfolder than your transcription run — both write topages_txt/<name>.txt, and the script skips a page whose output file already exists, so reusing the same folder would just return the old transcription instead of translating. (--redore-runs it in place if you'd rather overwrite than use a separate folder.) - Everything under Glossary/Continuity context/Lookahead context below
applies identically to
novel_ocr.py --translate— the--glossary/--context-pages/--target-langflags are the exact same flags, implemented once incommon.py.--context-pages-aheadis manga-only for now (see that section for why light novel prose doesn't need it as much).
Glossary
For a consistent translation of recurring names and terms, copy
glossary.example.md to glossary.md right here in
the ocr/ folder (next to the scripts, not with your page images — a
single stable file you keep building up across books/chapters, shared by
both novel_ocr.py and manga_ocr_llm.py, and a natural fit for an
editing UI later). It's auto-detected and sent along with every
--translate request, e.g.:
## Names
- 澤村・スペンサー・英梨々: Савамура Спенсер Эрири
## Terms
- ビジュアルノベル: Визуальная новелла
## Notes
- Keep Japanese suffixes (e.g. -chan, -san).
The file isn't parsed — it's appended to the prompt more or less as-is —
so feel free to add/reorder/rename sections, drop entries, or write extra
freeform notes for the model. Override the auto-detected path with
--glossary /path/to/glossary.md if you keep it somewhere else.
Continuity context from previous pages
--context-pages N (default 2) also sends along the translations of the
last N pages, so the model can keep character voice consistent and resolve
things that only make sense given what was just said — an ongoing
exchange, a pronoun referring back to something on the previous page, a
punchline that depends on the setup a page earlier. Manga bubbles are
terse and full of exactly this kind of dependency, which is why this
exists here and not in novel_ocr.py.
python manga_ocr_llm.py --input ./pages --output ./out --translate --context-pages 4
- Only kicks in with
--translate(there's nothing to carry forward when just transcribing). - Pulls from
pages_txt/in your--outputfolder — the translations already produced earlier in this same run (or a previous run you're resuming). Blank/[NO_TEXT]pages are skipped when building context, since they add nothing. --context-pages 0disables it.- Token cost is minimal — manga dialogue is short — but the log for each page (see below) records exactly which previous pages were included, if you want to check.
Lookahead context from upcoming pages
--context-pages-ahead N (default 0) goes the other direction: it sends
along the original Japanese (never a translation) of the next N pages,
for things that only make sense once you know what happens next — a
pronoun whose gender only becomes clear a page later, a line whose real
addressee is revealed afterward, a joke whose setup pays off on the
following page. Professional manga translators read a whole chapter before
translating any of it for exactly this reason.
python manga_ocr_llm.py --input ./pages --output ./out --translate --context-pages-ahead 1
Deliberately the original Japanese, never a draft translation. An earlier version of this feature was going to run a cheap first-pass translation of upcoming pages and feed that forward as context. Don't do that — a translation is someone's (or something's) interpretation, not raw fact, and even with a "treat this as an unreliable draft" instruction, a model given a wrong reading in the context tends to partially inherit it. A rushed, context-blind draft translation of the next page is exactly as likely to be wrong as a rushed translation of the current one — so this would be laundering a coin flip's worth of noise into looking like ground truth. Raw Japanese has no such failure mode: it's just data, correctly transcribed once and reused, and the model draws its own conclusions from it exactly the same way it would from the current page.
- Only kicks in with
--translate. - Before translating a page, this makes sure the next N pages' Japanese
text is available:
- Hand-prepared or previously-cached
.txt— if--context-src-dir(default<output>/context_src/) already has a file named exactly like that page's image stem (e.g.page005.txtforpage005.jpg), it's used as-is, no OCR call made. You can drop hand-corrected transcriptions in here yourself ahead of time if you want full control — nothing requires them to come from a script. - Otherwise, OCR'd on the fly via
--ahead-ocr-backend(llmdefault — the same OpenAI-compatible API, transcription prompt, no translation; orlocal— offlinemanga-ocr, needs themanga-ocrandopencv-pythonpackages) and cached intocontext_src/for reuse — a page used as lookahead context is only ever OCR'd once, even though it'll come up again as the "current" page (or as another page's lookahead) later in the run.
- Hand-prepared or previously-cached
--context-pages-ahead 0(default) disables it entirely — no extra OCR calls, nocontext_src/folder created.- Extra OCR calls mean extra latency/cost on pages that need lookahead —
small in absolute terms (manga bubbles are short), but worth knowing
it's there. The log for each page records exactly which upcoming pages'
Japanese was included (
context_pages_ahead_used). - Combine freely with
--context-pages— a page's prompt can include both past translations and future Japanese at once, kept in clearly separate, clearly labeled sections so the model doesn't confuse the two.
API parameters
novel_ocr.py and manga_ocr_llm.py (the two OpenAI-compatible-API
scripts) read request parameters from the ocr section of config.json,
each overridable with a matching CLI flag:
| config.json field | CLI flag | Notes |
|---|---|---|
temperature |
--temperature |
Default 0 (deterministic — you want the same page OCR'd the same way every time). |
max_tokens |
--max-tokens |
Response length cap. Omitted from the request unless set. |
top_p |
--top-p |
Omitted from the request unless set. |
reasoning_effort |
--reasoning-effort |
e.g. low/medium/high. Passed through as extra_body, since support varies by model/provider — if your model/provider ignores it, it's simply a no-op rather than an error. |
google_vision_ocr.py doesn't take any of these (classic OCR, no model
parameters); local_mangaocr_ocr.py doesn't either (a fixed local model).
Pure-OCR alternatives (no translation)
| Script | Backend | Setup needed | Notes |
|---|---|---|---|
google_vision_ocr.py |
Google Cloud Vision (classic OCR) | Google Cloud project + billing enabled | No LLM context understanding, but solid on clean scans. --mode novel/--mode manga (see below). Free tier covers a typical volume, but Google still requires a billing account to be linked. |
local_mangaocr_ocr.py |
manga-ocr, fully offline | None — no account, no API key | --mode novel (column segmentation) or --mode manga (speech-bubble detection, closer to what manga-ocr was actually trained on). |
Both are transcription-only (no --translate) and follow the same
--input/--output/--mode convention as the LLM scripts, writing to
the same pages_txt/*.txt + combined.md layout. See each script's own
docstring for setup and manga-mode caveats (local_mangaocr_ocr.py in
particular — bubble detection is a geometric heuristic, so pass --debug
to sanity-check the detected reading order).
google_vision_ocr.py reads its credentials path from config.json's
vision section (not ocr — Google Cloud Vision has nothing to do
with the OpenAI-compatible API the other scripts use):
{ "vision": { "credentials": "/path/to/service-account-key.json" } }
System/user prompt split (caching)
For novel_ocr.py and manga_ocr_llm.py, every request is sent as a
system message (the format instructions, and, with --translate, the
glossary — identical on every page of a run) plus a user message (the
page image, and, with --context-pages/--context-pages-ahead, that
page's sliding-window context). Most OpenAI-compatible providers cache a
repeated system prefix, so a 300-page book isn't re-billed/re-processed
for the same instructions on every single page — only the truly per-page
part (image + context) is new each time. This is implemented once in
common.py's ocr_image()/build_system_prompt()/build_context_block()
and used identically by both scripts.
Request/response logs
novel_ocr.py, manga_ocr_llm.py, and google_vision_ocr.py each write
one JSON log file per page to logs/ at the repo root (created
automatically) — the full prompt/request sent, every retry attempt, and
the full raw API response (or the error, if it failed). Handy for
debugging a bad transcription/translation, checking token usage, or just
seeing exactly what was sent.
logs/
20260901_153012_042311_page003.json
20260901_153034_198822_page004.json
...
- The image itself is never embedded in the log (only its filename/size) — everything else about the request is recorded as-is.
--log-dirpoints logging at a different folder;--no-logdisables it.- Logs accumulate across every run (nothing is deleted automatically) —
clear out
logs/periodically if it grows large. local_mangaocr_ocr.pymakes no network requests (fully offline), so there's no "response" to log for it.
Next step
Once you have a pages_txt/ folder full of .txt files:
- Light novel: manually sort the pages into the folder structure
epub_builderexpects (seeepub_builder/README.md), then run the epub builder. - Manga: these
.txtfiles (one numbered bubble list per page) are meant as transcription/translation reference for your own typesetting workflow —epub_buildertargets prose light novels and doesn't lay out manga pages.