This commit is contained in:
Poison Flower
2026-09-18 01:00:58 +03:00
parent 02da1023ee
commit 41c994277b
10 changed files with 843 additions and 456 deletions
+76 -17
View File
@@ -12,7 +12,16 @@ and cross-page continuity context:
| 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 |
| 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`](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`](../epub_builder/README.md), which turns the resulting
@@ -25,9 +34,12 @@ targets prose light novels and doesn't lay out manga pages.
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.
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`](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.
```bash
pip install openai pillow natsort tqdm
@@ -39,32 +51,57 @@ pip install openai pillow natsort tqdm
identifier. This same file is shared with `manga_ocr_llm.py` and the
`epub_builder` module. `temperature`/`max_tokens`/`top_p`/
`reasoning_effort` are optional — see [API parameters](#api-parameters).
2. Optionally edit `prompt_novel.txt` — plain English text, no need to
touch any code to tweak the instructions given to the model.
2. Optionally edit `prompt_novel.txt` (transcription) or
`prompt_novel_translate.txt` (`--translate`) — plain English text, no
need to touch any code to tweak the instructions given to the model.
```bash
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, so
`page2.jpg` and `page10.jpg` sort correctly too).
- `--output`: folder for results. Creates `pages_txt/<name>.txt` (one file
per page) plus a `combined.md` preview of the whole run.
- `--output`: folder for results. Creates `pages_txt/<name>.txt` (one
file per page), a `combined.md` preview of the whole run, and a
`state.json` manifest (see below).
- If interrupted, just re-run with the same `--output` — pages that already
have a `.txt` file 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 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. The
`epub_builder` module 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 `--redo` to re-OCR/
re-translate just a few pages, e.g. after tweaking the prompt or
glossary, without redoing the whole book.
- `--redo` re-runs pages even if a `.txt` already exists for them.
- `--start-page N` skips everything before page N entirely (not just a
relabelling — pages before it are never sent).
- `--no-state` ignores `state.json` and falls back to plain "file exists =
done" behaviour.
- `--sleep N` adds a delay (seconds) between requests if you're hitting
rate limits.
- `--config` overrides the config.json path (defaults to the repo root).
- `--prompt-file` overrides the prompt file (defaults to `prompt_novel.txt`).
- `--prompt-file` overrides the prompt file (defaults to `prompt_novel.txt`,
or `prompt_novel_translate.txt` with `--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)
@@ -100,7 +137,8 @@ Output format is a numbered list per page:
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`/
THOUGHT, NARRATION, or SFX. Same `--input`/`--output`/`--pages`/`--redo`/
`--start-page`/`--no-state`/`--sleep`/`--config`/
`--prompt-file` conventions as `novel_ocr.py`.
### Translation (`--translate`)
@@ -114,16 +152,24 @@ THOUGHT, NARRATION, or SFX. Same `--input`/`--output`/`--sleep`/`--config`/
- Use a different `--output` folder than your transcription run — both
write to `pages_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.
return the old transcription instead of translating. (`--redo` re-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-lang` flags are the exact same flags,
implemented once in [`common.py`](common.py). `--context-pages-ahead`
is 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`](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.:
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.:
```markdown
## Names
@@ -257,6 +303,19 @@ 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