manga-translator 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Indra Gunanda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,356 @@
1
+ Metadata-Version: 2.4
2
+ Name: manga-translator
3
+ Version: 0.3.0
4
+ Summary: Offline-first manga translation CLI with optional LLM post-editing
5
+ Author: Indra Gunanda
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/igun997/manga-translator
8
+ Project-URL: Repository, https://github.com/igun997/manga-translator
9
+ Project-URL: Issues, https://github.com/igun997/manga-translator/issues
10
+ Keywords: manga,comic,translation,ocr,tesseract,argos,offline
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Multimedia :: Graphics
20
+ Classifier: Topic :: Text Processing :: Linguistic
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.26
25
+ Requires-Dist: opencv-python-headless>=4.11
26
+ Requires-Dist: Pillow>=10
27
+ Requires-Dist: argostranslate<2,>=1.11
28
+ Requires-Dist: PyYAML>=6
29
+ Requires-Dist: onnxruntime<2,>=1.17
30
+ Dynamic: license-file
31
+
32
+ # manga-translator
33
+
34
+ [![CI](https://github.com/igun997/manga-translator/actions/workflows/ci.yml/badge.svg)](https://github.com/igun997/manga-translator/actions/workflows/ci.yml)
35
+ [![PyPI](https://img.shields.io/pypi/v/manga-translator)](https://pypi.org/project/manga-translator/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/manga-translator)](https://pypi.org/project/manga-translator/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
38
+
39
+ Translate a folder of manga pages, or a few selected pages, from any supported
40
+ language to any other. The CLI finds speech balloons and light narration boxes,
41
+ reads them with Tesseract, translates with locally installed Argos models, and
42
+ reletters the page in your target font.
43
+
44
+ - **Offline by default.** Detection, OCR and translation all run on your
45
+ machine. Nothing is uploaded, and no model is downloaded during translation.
46
+ - **LLM post-editing is optional.** An OpenAI-compatible model can polish the
47
+ offline drafts when you ask for it with `--llm`; otherwise no LLM credential
48
+ is read and no network request is made.
49
+ - **Nothing is overwritten blind.** Outputs go to a separate directory, and every
50
+ region that fails to translate or fit is reported instead of clipped.
51
+
52
+ Two commands ship in the package: `manga-translate` (detect → OCR → translate →
53
+ reletter) and `manga-bubbles` (analyze and reletter by hand, no translation).
54
+
55
+ ---
56
+
57
+ ## Requirements
58
+
59
+ | Requirement | Why | Notes |
60
+ | --- | --- | --- |
61
+ | Python 3.10+ | runtime | |
62
+ | Tesseract OCR | reads the original text | plus a language pack **per source language** |
63
+ | Argos package for your language pair | offline translation | installed once by `manga-translate models` |
64
+ | A font with your **target** script | reletters the page | CJK needs a CJK font; Latin/Cyrillic/Greek work with DejaVu |
65
+
66
+ OCR language packs, Argos translation packages and target fonts are three
67
+ independent things. Changing the target language usually means installing a
68
+ font; changing the source language usually means installing an OCR pack.
69
+
70
+ ## Installation
71
+
72
+ ### 1. System packages (Debian/Ubuntu)
73
+
74
+ ```sh
75
+ sudo apt-get install tesseract-ocr tesseract-ocr-eng # English source
76
+ # Japanese source also needs:
77
+ # tesseract-ocr-jpn tesseract-ocr-jpn-vert
78
+ # A CJK font is only needed when the TARGET is Japanese/Chinese/Korean:
79
+ # fonts-noto-cjk
80
+ ```
81
+
82
+ ### 2. The CLI
83
+
84
+ With `pip`:
85
+
86
+ ```sh
87
+ python3 -m venv .venv
88
+ .venv/bin/python -m pip install -e .
89
+ ```
90
+
91
+ With [uv](https://docs.astral.sh/uv/) (faster, and what CI uses):
92
+
93
+ ```sh
94
+ uv venv .venv
95
+ uv pip install --python .venv/bin/python -e .
96
+ ```
97
+
98
+ Or straight from PyPI, once published:
99
+
100
+ ```sh
101
+ pip install manga-translator
102
+ # or
103
+ uv tool install manga-translator
104
+ ```
105
+
106
+ > `argostranslate` depends on PyTorch. On a GPU-less machine, install the CPU
107
+ > build first to avoid pulling several gigabytes of CUDA wheels:
108
+ > ```sh
109
+ > uv pip install --python .venv/bin/python torch --index-url https://download.pytorch.org/whl/cpu
110
+ > uv pip install --python .venv/bin/python -e .
111
+ > ```
112
+
113
+ Verify:
114
+
115
+ ```sh
116
+ .venv/bin/manga-translate --help
117
+ ```
118
+
119
+ ## Usage
120
+
121
+ ### Step 1 — install the translation route (once, needs internet)
122
+
123
+ Translation needs an Argos package for the pair you want. `models` resolves the
124
+ shortest route, direct or through a pivot such as Japanese → English →
125
+ Indonesian, and downloads only the missing packages:
126
+
127
+ ```sh
128
+ .venv/bin/manga-translate models --source ja --target id
129
+ ```
130
+
131
+ ```text
132
+ Installed offline route: ja->en, en->id
133
+ ```
134
+
135
+ This is the **only** command that touches the network. Run it again for each new
136
+ pair; it is a no-op when the route is already installed.
137
+
138
+ ### Step 2 — translate
139
+
140
+ ```sh
141
+ # A whole folder, subfolders included
142
+ .venv/bin/manga-translate translate ./pages --source ja --target id --output ./translated-id
143
+
144
+ # Just a few pages
145
+ .venv/bin/manga-translate translate cover.jpeg page-01.jpeg \
146
+ --source ja --target id --output ./translated-id
147
+ ```
148
+
149
+ Outputs are PNGs under `--output` (default `translated-TARGET`), keeping the
150
+ input folder's relative paths. Existing outputs are left alone unless you pass
151
+ `--overwrite`.
152
+
153
+ ### Language codes
154
+
155
+ `--source` / `--target` are **Argos** codes; `--ocr-lang` is a **Tesseract**
156
+ code. The Tesseract code is inferred from `--source` for the common languages,
157
+ and can always be overridden:
158
+
159
+ | | | |
160
+ | --- | --- | --- |
161
+ | `ja` → `jpn` | `en` → `eng` | `id` → `ind` |
162
+ | `zh` → `chi_sim` | `ko` → `kor` | `es` → `spa` |
163
+
164
+ ```sh
165
+ # Vertical Japanese lettering
166
+ .venv/bin/manga-translate translate ./pages --source ja --target id \
167
+ --ocr-lang jpn_vert --psm 5
168
+ ```
169
+
170
+ Use `--font /path/to/font.ttf` whenever the default target font cannot render
171
+ your script; Japanese/Chinese/Korean default to
172
+ `/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc` and fail with an
173
+ actionable message if it is absent.
174
+
175
+ ### Reading the report
176
+
177
+ Every run writes `translation-report.json` next to the images:
178
+
179
+ ```json
180
+ {
181
+ "source": "en", "target": "id", "llm": false,
182
+ "pages": [{
183
+ "input": "pages/panel-1.jpeg",
184
+ "output": "translated-id/panel-1.png",
185
+ "status": "ok",
186
+ "regions": [{
187
+ "id": 1,
188
+ "bbox": [576, 4, 856, 436],
189
+ "source": "AT THE MOMENT OF MY DEATH, ...",
190
+ "translation": "Pada saat kematianku, ...",
191
+ "status": "translated"
192
+ }]
193
+ }]
194
+ }
195
+ ```
196
+
197
+ | Status | Meaning |
198
+ | --- | --- |
199
+ | `translated` | OCR'd, translated and reletted |
200
+ | `offline_fallback` | LLM text did not fit; the offline draft was used instead |
201
+ | `untranslated` | translator returned nothing new; the balloon was left untouched |
202
+ | `unfittable` | translation cannot fit at the minimum font size; nothing erased |
203
+ | `no_text` | detector found no text in this region |
204
+ | `failed` | OCR or render error; artwork untouched |
205
+
206
+ Page statuses are `ok`, `partial` (any region failed) or `no_text`. The command
207
+ exits `0` when every page is `ok`, `1` when any page is `partial`/`failed`, and
208
+ `2` on a configuration error. **Always eyeball the output**: OCR, detection and
209
+ inpainting are all approximate.
210
+
211
+ ### Missed a balloon? Annotate it
212
+
213
+ Open the image, read the coordinates, and add a region the detectors did not
214
+ find. Give points *inside* the balloon outline:
215
+
216
+ ```json
217
+ {"panel-1.jpeg": [{"polygon": [[903, 1276], [948, 1281], [998, 1298], [999, 1362], [986, 1465]]}]}
218
+ ```
219
+
220
+ ```sh
221
+ .venv/bin/manga-translate translate panel-1.jpeg --source en --target id \
222
+ --regions regions.json --output ./translated-id
223
+ ```
224
+
225
+ Keys are the input filename, or the relative path inside a folder input. A
226
+ `bbox: [left, top, right, bottom]` works instead of a polygon. Keep the shape
227
+ inset: it masks both the erase step and the fitted text, so a polygon that
228
+ spans artwork can erase art.
229
+
230
+ ### Optional: LLM post-editing
231
+
232
+ The LLM receives the recognized source text and its offline draft — **not the
233
+ image** — and rewrites the draft into more natural target language. It never
234
+ replaces a translation that does not fit; the offline draft is used then.
235
+
236
+ ```sh
237
+ .venv/bin/manga-translate translate ./pages --source ja --target id \
238
+ --llm --provider netra --model deepseek/deepseek-v4-flash-0731 \
239
+ --output ./translated-id
240
+ ```
241
+
242
+ Credentials, in order of priority:
243
+
244
+ 1. **A provider file.** Defaults to `~/.omp/agent/models.yml`:
245
+
246
+ ```yaml
247
+ providers:
248
+ netra:
249
+ baseUrl: https://your-endpoint/v1
250
+ apiKey: sk-...
251
+ models:
252
+ - id: deepseek/deepseek-v4-flash-0731
253
+ ```
254
+
255
+ Point elsewhere with `--models-file ./models.yml`; pick an entry with
256
+ `--provider` and `--model`.
257
+
258
+ 2. **Environment variables**, which skip the file entirely and are convenient
259
+ for CI:
260
+
261
+ | Variable | Purpose |
262
+ | --- | --- |
263
+ | `MANGA_LLM_BASE_URL` | OpenAI-compatible base URL |
264
+ | `MANGA_LLM_API_KEY` | key for any provider |
265
+ | `MANGA_LLM_API_KEY_<PROVIDER>` | key for one provider, e.g. `MANGA_LLM_API_KEY_NETRA` |
266
+ | `MANGA_LLM_MODEL` | model id |
267
+ | `OPENAI_API_KEY` | last-resort fallback |
268
+
269
+ Remote endpoints must be `https`; plain `http` is accepted only for loopback.
270
+ `--llm` makes network requests and may cost money. Without it, no key is read,
271
+ no config file is opened, and no request is made.
272
+
273
+ ### Manual relettering without translation
274
+
275
+ `manga-bubbles` finds balloons and gives each an ID you can translate by hand.
276
+
277
+ ```sh
278
+ .venv/bin/manga-bubbles analyze panel-1.jpeg > detections.json
279
+ ```
280
+
281
+ ```json
282
+ [{"id": 1, "bbox": [576, 4, 856, 436], "text": "I WANT TO DO IT!", "words": [ ... ]}]
283
+ ```
284
+
285
+ Write a JSON map of only the IDs you want changed, then apply it:
286
+
287
+ ```json
288
+ {"1": "I WILL DO IT TODAY, EVEN IF IT TAKES ALL DAY!"}
289
+ ```
290
+
291
+ ```sh
292
+ .venv/bin/manga-bubbles replace panel-1.jpeg translations.json out.png \
293
+ --font /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
294
+ ```
295
+
296
+ Use the same `--lang` and `--psm` for `analyze` and `replace` so the IDs line
297
+ up. Balloons you do not list, and everything outside the balloon interiors,
298
+ stay pixel-identical. Text too long to fit raises an error rather than clipping.
299
+
300
+ ## How detection works
301
+
302
+ 1. **Bright-region geometry** finds enclosed balloons and light rectangular
303
+ narration boxes by thresholding, filling, and filtering on shape.
304
+ 2. **A bundled ONNX balloon segmenter** adds balloons the geometric pass cannot
305
+ see — notably open balloons whose white interior flows into the page
306
+ background. Its mask is inset by a few pixels before OCR, so lettering
307
+ sitting outside the outline is never read as dialogue.
308
+ 3. Overlapping proposals are deduplicated, so a balloon is never OCR'd twice.
309
+
310
+ The segmenter is `huyvux3005/manga109-segmentation-bubble` (Apache-2.0),
311
+ exported to a fixed 1600×1600 ONNX graph and run with `onnxruntime` only — no
312
+ Ultralytics install, no download at run time. See `manga_models/NOTICE` and
313
+ `manga_models/LICENSE`.
314
+
315
+ ## Scope and limits
316
+
317
+ - Targets speech balloons and light narration boxes. Dark balloons, heavy
318
+ screentone, and unusual shapes may be missed — annotate them with `--regions`.
319
+ - Erasure covers OCR word boxes, so stylized lettering or punctuation the OCR
320
+ missed can need manual retouching.
321
+ - Right-to-left and vertical reading order is not reconstructed for you; the
322
+ report lists regions top-to-bottom, left-to-right.
323
+ - Detection is not a general artwork segmenter, and no OCR result is guaranteed.
324
+ - Inspect before publishing anything.
325
+
326
+ ## Development
327
+
328
+ ```sh
329
+ uv venv .venv
330
+ uv pip install --python .venv/bin/python -e .
331
+ uv pip install --python .venv/bin/python "numpy>=1.26" "opencv-python-headless>=4.11" "Pillow>=10" "PyYAML>=6" "onnxruntime>=1.17,<2"
332
+
333
+ .venv/bin/python -m unittest discover -s tests -v
334
+ ```
335
+
336
+ Tests need Tesseract with the `eng` pack, DejaVu fonts, and the bundled
337
+ `manga_models/bubble.onnx` (it ships in the wheel, so an editable install has
338
+ it). The suite is deterministic, runs offline, and draws its own pages with
339
+ Pillow — including the open balloon whose interior joins the page background,
340
+ which geometry alone cannot separate. No third-party manga page is redistributed
341
+ with this project.
342
+
343
+ `examples/` is gitignored: it is the place for your own scans.
344
+
345
+ ## License
346
+
347
+ MIT — see [LICENSE](LICENSE). The bundled balloon model weights are Apache-2.0;
348
+ see [manga_models/NOTICE](manga_models/NOTICE).
349
+
350
+ ### Research basis
351
+
352
+ - [OpenCV contour extraction](https://docs.opencv.org/4.13.0/df/d0d/tutorial_find_contours.html): contour-derived interior masks.
353
+ - [Pillow text measurement and drawing](https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html): measured line layout and mask rendering.
354
+ - [huyvux3005/manga109-segmentation-bubble](https://huggingface.co/huyvux3005/manga109-segmentation-bubble) (Apache-2.0): bundled YOLO11n balloon segmenter, exported to ONNX.
355
+ - [Dubray and Laubrock, speech balloon segmentation](https://arxiv.org/abs/1902.08137): open balloons need learned segmentation rather than thresholding alone.
356
+ - [comic-text-detector](https://github.com/dmMaze/comic-text-detector): learned text masks as an alternative to OCR rectangles; its GPL-3.0 code is not included.
@@ -0,0 +1,325 @@
1
+ # manga-translator
2
+
3
+ [![CI](https://github.com/igun997/manga-translator/actions/workflows/ci.yml/badge.svg)](https://github.com/igun997/manga-translator/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/manga-translator)](https://pypi.org/project/manga-translator/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/manga-translator)](https://pypi.org/project/manga-translator/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ Translate a folder of manga pages, or a few selected pages, from any supported
9
+ language to any other. The CLI finds speech balloons and light narration boxes,
10
+ reads them with Tesseract, translates with locally installed Argos models, and
11
+ reletters the page in your target font.
12
+
13
+ - **Offline by default.** Detection, OCR and translation all run on your
14
+ machine. Nothing is uploaded, and no model is downloaded during translation.
15
+ - **LLM post-editing is optional.** An OpenAI-compatible model can polish the
16
+ offline drafts when you ask for it with `--llm`; otherwise no LLM credential
17
+ is read and no network request is made.
18
+ - **Nothing is overwritten blind.** Outputs go to a separate directory, and every
19
+ region that fails to translate or fit is reported instead of clipped.
20
+
21
+ Two commands ship in the package: `manga-translate` (detect → OCR → translate →
22
+ reletter) and `manga-bubbles` (analyze and reletter by hand, no translation).
23
+
24
+ ---
25
+
26
+ ## Requirements
27
+
28
+ | Requirement | Why | Notes |
29
+ | --- | --- | --- |
30
+ | Python 3.10+ | runtime | |
31
+ | Tesseract OCR | reads the original text | plus a language pack **per source language** |
32
+ | Argos package for your language pair | offline translation | installed once by `manga-translate models` |
33
+ | A font with your **target** script | reletters the page | CJK needs a CJK font; Latin/Cyrillic/Greek work with DejaVu |
34
+
35
+ OCR language packs, Argos translation packages and target fonts are three
36
+ independent things. Changing the target language usually means installing a
37
+ font; changing the source language usually means installing an OCR pack.
38
+
39
+ ## Installation
40
+
41
+ ### 1. System packages (Debian/Ubuntu)
42
+
43
+ ```sh
44
+ sudo apt-get install tesseract-ocr tesseract-ocr-eng # English source
45
+ # Japanese source also needs:
46
+ # tesseract-ocr-jpn tesseract-ocr-jpn-vert
47
+ # A CJK font is only needed when the TARGET is Japanese/Chinese/Korean:
48
+ # fonts-noto-cjk
49
+ ```
50
+
51
+ ### 2. The CLI
52
+
53
+ With `pip`:
54
+
55
+ ```sh
56
+ python3 -m venv .venv
57
+ .venv/bin/python -m pip install -e .
58
+ ```
59
+
60
+ With [uv](https://docs.astral.sh/uv/) (faster, and what CI uses):
61
+
62
+ ```sh
63
+ uv venv .venv
64
+ uv pip install --python .venv/bin/python -e .
65
+ ```
66
+
67
+ Or straight from PyPI, once published:
68
+
69
+ ```sh
70
+ pip install manga-translator
71
+ # or
72
+ uv tool install manga-translator
73
+ ```
74
+
75
+ > `argostranslate` depends on PyTorch. On a GPU-less machine, install the CPU
76
+ > build first to avoid pulling several gigabytes of CUDA wheels:
77
+ > ```sh
78
+ > uv pip install --python .venv/bin/python torch --index-url https://download.pytorch.org/whl/cpu
79
+ > uv pip install --python .venv/bin/python -e .
80
+ > ```
81
+
82
+ Verify:
83
+
84
+ ```sh
85
+ .venv/bin/manga-translate --help
86
+ ```
87
+
88
+ ## Usage
89
+
90
+ ### Step 1 — install the translation route (once, needs internet)
91
+
92
+ Translation needs an Argos package for the pair you want. `models` resolves the
93
+ shortest route, direct or through a pivot such as Japanese → English →
94
+ Indonesian, and downloads only the missing packages:
95
+
96
+ ```sh
97
+ .venv/bin/manga-translate models --source ja --target id
98
+ ```
99
+
100
+ ```text
101
+ Installed offline route: ja->en, en->id
102
+ ```
103
+
104
+ This is the **only** command that touches the network. Run it again for each new
105
+ pair; it is a no-op when the route is already installed.
106
+
107
+ ### Step 2 — translate
108
+
109
+ ```sh
110
+ # A whole folder, subfolders included
111
+ .venv/bin/manga-translate translate ./pages --source ja --target id --output ./translated-id
112
+
113
+ # Just a few pages
114
+ .venv/bin/manga-translate translate cover.jpeg page-01.jpeg \
115
+ --source ja --target id --output ./translated-id
116
+ ```
117
+
118
+ Outputs are PNGs under `--output` (default `translated-TARGET`), keeping the
119
+ input folder's relative paths. Existing outputs are left alone unless you pass
120
+ `--overwrite`.
121
+
122
+ ### Language codes
123
+
124
+ `--source` / `--target` are **Argos** codes; `--ocr-lang` is a **Tesseract**
125
+ code. The Tesseract code is inferred from `--source` for the common languages,
126
+ and can always be overridden:
127
+
128
+ | | | |
129
+ | --- | --- | --- |
130
+ | `ja` → `jpn` | `en` → `eng` | `id` → `ind` |
131
+ | `zh` → `chi_sim` | `ko` → `kor` | `es` → `spa` |
132
+
133
+ ```sh
134
+ # Vertical Japanese lettering
135
+ .venv/bin/manga-translate translate ./pages --source ja --target id \
136
+ --ocr-lang jpn_vert --psm 5
137
+ ```
138
+
139
+ Use `--font /path/to/font.ttf` whenever the default target font cannot render
140
+ your script; Japanese/Chinese/Korean default to
141
+ `/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc` and fail with an
142
+ actionable message if it is absent.
143
+
144
+ ### Reading the report
145
+
146
+ Every run writes `translation-report.json` next to the images:
147
+
148
+ ```json
149
+ {
150
+ "source": "en", "target": "id", "llm": false,
151
+ "pages": [{
152
+ "input": "pages/panel-1.jpeg",
153
+ "output": "translated-id/panel-1.png",
154
+ "status": "ok",
155
+ "regions": [{
156
+ "id": 1,
157
+ "bbox": [576, 4, 856, 436],
158
+ "source": "AT THE MOMENT OF MY DEATH, ...",
159
+ "translation": "Pada saat kematianku, ...",
160
+ "status": "translated"
161
+ }]
162
+ }]
163
+ }
164
+ ```
165
+
166
+ | Status | Meaning |
167
+ | --- | --- |
168
+ | `translated` | OCR'd, translated and reletted |
169
+ | `offline_fallback` | LLM text did not fit; the offline draft was used instead |
170
+ | `untranslated` | translator returned nothing new; the balloon was left untouched |
171
+ | `unfittable` | translation cannot fit at the minimum font size; nothing erased |
172
+ | `no_text` | detector found no text in this region |
173
+ | `failed` | OCR or render error; artwork untouched |
174
+
175
+ Page statuses are `ok`, `partial` (any region failed) or `no_text`. The command
176
+ exits `0` when every page is `ok`, `1` when any page is `partial`/`failed`, and
177
+ `2` on a configuration error. **Always eyeball the output**: OCR, detection and
178
+ inpainting are all approximate.
179
+
180
+ ### Missed a balloon? Annotate it
181
+
182
+ Open the image, read the coordinates, and add a region the detectors did not
183
+ find. Give points *inside* the balloon outline:
184
+
185
+ ```json
186
+ {"panel-1.jpeg": [{"polygon": [[903, 1276], [948, 1281], [998, 1298], [999, 1362], [986, 1465]]}]}
187
+ ```
188
+
189
+ ```sh
190
+ .venv/bin/manga-translate translate panel-1.jpeg --source en --target id \
191
+ --regions regions.json --output ./translated-id
192
+ ```
193
+
194
+ Keys are the input filename, or the relative path inside a folder input. A
195
+ `bbox: [left, top, right, bottom]` works instead of a polygon. Keep the shape
196
+ inset: it masks both the erase step and the fitted text, so a polygon that
197
+ spans artwork can erase art.
198
+
199
+ ### Optional: LLM post-editing
200
+
201
+ The LLM receives the recognized source text and its offline draft — **not the
202
+ image** — and rewrites the draft into more natural target language. It never
203
+ replaces a translation that does not fit; the offline draft is used then.
204
+
205
+ ```sh
206
+ .venv/bin/manga-translate translate ./pages --source ja --target id \
207
+ --llm --provider netra --model deepseek/deepseek-v4-flash-0731 \
208
+ --output ./translated-id
209
+ ```
210
+
211
+ Credentials, in order of priority:
212
+
213
+ 1. **A provider file.** Defaults to `~/.omp/agent/models.yml`:
214
+
215
+ ```yaml
216
+ providers:
217
+ netra:
218
+ baseUrl: https://your-endpoint/v1
219
+ apiKey: sk-...
220
+ models:
221
+ - id: deepseek/deepseek-v4-flash-0731
222
+ ```
223
+
224
+ Point elsewhere with `--models-file ./models.yml`; pick an entry with
225
+ `--provider` and `--model`.
226
+
227
+ 2. **Environment variables**, which skip the file entirely and are convenient
228
+ for CI:
229
+
230
+ | Variable | Purpose |
231
+ | --- | --- |
232
+ | `MANGA_LLM_BASE_URL` | OpenAI-compatible base URL |
233
+ | `MANGA_LLM_API_KEY` | key for any provider |
234
+ | `MANGA_LLM_API_KEY_<PROVIDER>` | key for one provider, e.g. `MANGA_LLM_API_KEY_NETRA` |
235
+ | `MANGA_LLM_MODEL` | model id |
236
+ | `OPENAI_API_KEY` | last-resort fallback |
237
+
238
+ Remote endpoints must be `https`; plain `http` is accepted only for loopback.
239
+ `--llm` makes network requests and may cost money. Without it, no key is read,
240
+ no config file is opened, and no request is made.
241
+
242
+ ### Manual relettering without translation
243
+
244
+ `manga-bubbles` finds balloons and gives each an ID you can translate by hand.
245
+
246
+ ```sh
247
+ .venv/bin/manga-bubbles analyze panel-1.jpeg > detections.json
248
+ ```
249
+
250
+ ```json
251
+ [{"id": 1, "bbox": [576, 4, 856, 436], "text": "I WANT TO DO IT!", "words": [ ... ]}]
252
+ ```
253
+
254
+ Write a JSON map of only the IDs you want changed, then apply it:
255
+
256
+ ```json
257
+ {"1": "I WILL DO IT TODAY, EVEN IF IT TAKES ALL DAY!"}
258
+ ```
259
+
260
+ ```sh
261
+ .venv/bin/manga-bubbles replace panel-1.jpeg translations.json out.png \
262
+ --font /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
263
+ ```
264
+
265
+ Use the same `--lang` and `--psm` for `analyze` and `replace` so the IDs line
266
+ up. Balloons you do not list, and everything outside the balloon interiors,
267
+ stay pixel-identical. Text too long to fit raises an error rather than clipping.
268
+
269
+ ## How detection works
270
+
271
+ 1. **Bright-region geometry** finds enclosed balloons and light rectangular
272
+ narration boxes by thresholding, filling, and filtering on shape.
273
+ 2. **A bundled ONNX balloon segmenter** adds balloons the geometric pass cannot
274
+ see — notably open balloons whose white interior flows into the page
275
+ background. Its mask is inset by a few pixels before OCR, so lettering
276
+ sitting outside the outline is never read as dialogue.
277
+ 3. Overlapping proposals are deduplicated, so a balloon is never OCR'd twice.
278
+
279
+ The segmenter is `huyvux3005/manga109-segmentation-bubble` (Apache-2.0),
280
+ exported to a fixed 1600×1600 ONNX graph and run with `onnxruntime` only — no
281
+ Ultralytics install, no download at run time. See `manga_models/NOTICE` and
282
+ `manga_models/LICENSE`.
283
+
284
+ ## Scope and limits
285
+
286
+ - Targets speech balloons and light narration boxes. Dark balloons, heavy
287
+ screentone, and unusual shapes may be missed — annotate them with `--regions`.
288
+ - Erasure covers OCR word boxes, so stylized lettering or punctuation the OCR
289
+ missed can need manual retouching.
290
+ - Right-to-left and vertical reading order is not reconstructed for you; the
291
+ report lists regions top-to-bottom, left-to-right.
292
+ - Detection is not a general artwork segmenter, and no OCR result is guaranteed.
293
+ - Inspect before publishing anything.
294
+
295
+ ## Development
296
+
297
+ ```sh
298
+ uv venv .venv
299
+ uv pip install --python .venv/bin/python -e .
300
+ uv pip install --python .venv/bin/python "numpy>=1.26" "opencv-python-headless>=4.11" "Pillow>=10" "PyYAML>=6" "onnxruntime>=1.17,<2"
301
+
302
+ .venv/bin/python -m unittest discover -s tests -v
303
+ ```
304
+
305
+ Tests need Tesseract with the `eng` pack, DejaVu fonts, and the bundled
306
+ `manga_models/bubble.onnx` (it ships in the wheel, so an editable install has
307
+ it). The suite is deterministic, runs offline, and draws its own pages with
308
+ Pillow — including the open balloon whose interior joins the page background,
309
+ which geometry alone cannot separate. No third-party manga page is redistributed
310
+ with this project.
311
+
312
+ `examples/` is gitignored: it is the place for your own scans.
313
+
314
+ ## License
315
+
316
+ MIT — see [LICENSE](LICENSE). The bundled balloon model weights are Apache-2.0;
317
+ see [manga_models/NOTICE](manga_models/NOTICE).
318
+
319
+ ### Research basis
320
+
321
+ - [OpenCV contour extraction](https://docs.opencv.org/4.13.0/df/d0d/tutorial_find_contours.html): contour-derived interior masks.
322
+ - [Pillow text measurement and drawing](https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html): measured line layout and mask rendering.
323
+ - [huyvux3005/manga109-segmentation-bubble](https://huggingface.co/huyvux3005/manga109-segmentation-bubble) (Apache-2.0): bundled YOLO11n balloon segmenter, exported to ONNX.
324
+ - [Dubray and Laubrock, speech balloon segmentation](https://arxiv.org/abs/1902.08137): open balloons need learned segmentation rather than thresholding alone.
325
+ - [comic-text-detector](https://github.com/dmMaze/comic-text-detector): learned text masks as an alternative to OCR rectangles; its GPL-3.0 code is not included.