- Consolidate module configs into root config.example.json with ocr, vision, and epub sections - Split LLM OCR workflows into novel_ocr.py (prose) and manga_ocr_llm.py (manga) - Remove gemini_direct_ocr.py in favor of OpenAI-compatible API endpoints - Support direct manga translation via --translate, --target-lang, and glossary.md - Add bidirectional context support: past translations (--context-pages) and lookahead Japanese text (--context-pages-ahead) - Add per-page JSON audit logging under logs/ and expose OpenAI sampling parameters
14 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 |
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. Kept deliberately simple — transcription only, no translate/glossary/context — since that's genuinely all a novel page needs.
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— 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
--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) plus acombined.mdpreview of the whole run.- 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, etc.) does not get an empty 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". --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).
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/--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.
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, which also
makes it 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" } }
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.