Metadata-Version: 2.4
Name: lemon-manga-translator
Version: 0.0.21
Summary: Koharu-inspired manga translation pipeline (JA->KO) in Python
Keywords: manga,translation,japanese,korean,ocr,inpainting
Author: LemonDouble
Author-email: LemonDouble <contact@lemondouble.com>
License-Expression: GPL-3.0-only
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Dist: numpy
Requires-Dist: pillow
Requires-Dist: opencv-python-headless
Requires-Dist: httpx
Requires-Dist: onnxruntime>=1.17
Requires-Dist: huggingface-hub>=0.20
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/LemonDouble/lemon-manga-translator
Project-URL: Repository, https://github.com/LemonDouble/lemon-manga-translator
Project-URL: Issues, https://github.com/LemonDouble/lemon-manga-translator/issues
Description-Content-Type: text/markdown

# lemon-manga-translator

일본어 → 한국어 만화(망가) 자동 번역 파이프라인.

학습된 검출 모델로 글자와 말풍선을 찾고, Vision LLM으로 읽기와 번역을 함께 처리합니다. 번역문을 배치할 수 있는 영역의 원문을 지우고 한국어를 그립니다.

## Quick Start

```bash
pip install lemon-manga-translator
```

```python
from lemon_manga_translator import MangaTranslator
from PIL import Image

mt = MangaTranslator(
    openrouter_api_key="sk-or-...",
    openrouter_model="google/gemini-3.1-flash-lite",
)
mt.prepare()  # HuggingFace에서 ONNX 모델 자동 다운로드

image = Image.open("page.jpg")
result = mt.translate(image)
result.save("output.png")
```

`prepare()`는 [Koharu Layout ONNX](https://huggingface.co/ShiniShiho/koharu-layout-rfdetr-seg-2xl-1152-onnx)의 글자·말풍선 검출 모델과 [AOT](https://huggingface.co/lemondouble/lemon-manga-translator)를 내려받아 캐시합니다. 검출 모델 버전은 고정되어 있습니다. 모델의 사용 조건은 각각의 모델 카드를 확인해야 합니다.

- 기본 번역 범위는 대사·내레이션·제목·이름표·표지판입니다. 색상이 있거나 손글씨·장식체여도 이 범위에 해당하면 번역합니다.
- 효과음·저작권 표시·읽을 수 없는 글자와 상품 포장에 인쇄된 상품명·작은 장식 글자는 보존합니다. 포장의 상품명·작은 장식 글자는 읽혀도 `no_translate=true`, `ko_translation=null`로 처리합니다.
- 작은 글자와 검출이 누락된 글자의 번역은 보장하지 않습니다. 원문 색상은 자동 복원하지 않습니다.
- 기존 CTD 검출은 `detector="ctd"`로 선택할 수 있습니다.

### 로컬 모델 사용

```python
mt = MangaTranslator(
    openrouter_api_key="sk-or-...",
    openrouter_model="google/gemini-3.1-flash-lite",
    model_source="/path/to/local/models",  # HF 대신 로컬 경로
)
```

로컬 모델은 `onnx/layout/rfdetr-seg-2xlarge.onnx`, `onnx/aot-inpainting/aot_folded.onnx`, `pytorch/aot-inpainting/config.json`에 둡니다. CTD를 선택하면 검출 모델 경로는 `onnx/comic-text-detector/ctd.onnx`입니다.

## 파이프라인

```
원본 → 글자·말풍선 검출 → Vision 읽기·번역 → 응답·배치 검증 → 원문 삭제 → 한국어 합성
```

| 단계 | 설명 |
|------|------|
| 검출 | RF-DETR의 글자 mask와 말풍선 mask를 연결합니다. 같은 말풍선의 대사는 함께 읽으며 효과음은 별도 후보로 유지합니다. |
| 읽기·번역 | 번호가 있는 페이지를 한 번 보내고 JSON 응답을 검증합니다. 기본 모델 최대 2회, fallback 최대 1회까지 호출하며 전체 사용량을 합산합니다. |
| 배치 검증 | ID와 타입을 확인하고 실제 폰트·폴백·stroke의 픽셀을 검사합니다. 원문을 보존하는 영역에는 번역 글자를 놓지 않습니다. |
| 삭제 | 유효한 번역과 배치가 있는 블록만 지웁니다. 학습된 말풍선의 균일한 배경은 같은 색으로 채우고 나머지는 AOT로 처리합니다. API 실패·번역 누락·배치 실패 때 원문을 남깁니다. |
| 합성 | 검증한 글자 레이어를 그대로 그립니다. 말풍선 글자 크기는 페이지의 중앙값으로 상한을 제한합니다. 같은 크기에서는 단어를 덜 나누는 줄바꿈을 먼저 고르고, 중앙에 가까운 배치를 선택합니다. Jua와 PretendardJP를 사용합니다. |

### GPU 사용

`device="cuda"`를 지정하면 ONNX 추론을 GPU에서 실행합니다. CUDA provider 초기화에 실패하면 오류를 반환합니다.

```python
mt = MangaTranslator(
    openrouter_api_key="sk-or-...",
    openrouter_model="google/gemini-3.1-flash-lite",
    device="cuda",
)
```

GPU 사용 시 `onnxruntime` 대신 환경의 CUDA 버전에 맞는 `onnxruntime-gpu`를 직접 설치해야 한다. 자세한 호환성은 [onnxruntime 공식 문서](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html) 참고.

### 튜닝

각 단계의 하이퍼파라미터는 dataclass로 노출되어 있다. `MangaTranslator` 생성자에 주입하면 단계 동작을 바꿀 수 있고, 미지정 시 라이브러리 기본값을 사용한다.

```python
from lemon_manga_translator import (
    MangaTranslator,
    LayoutConfig,
    CtdConfig,
    BubbleConfig,
    PipelineConfig,
    RenderConfig,
)

mt = MangaTranslator(
    openrouter_api_key="sk-or-...",
    openrouter_model="google/gemini-3.1-flash-lite",
    layout_config=LayoutConfig(text_confidence_threshold=0.25),
    pipeline_config=PipelineConfig(expand_bubbles=True),
    render_config=RenderConfig(min_font_size=12, max_font_size=64),
    vision_system_prompt="path/to/custom_prompt.txt",  # 파일 경로 또는 문자열
)
```

| Config | 다루는 것 |
|--------|----------|
| `LayoutConfig` | 학습 검출 confidence와 글자·말풍선 mask 연결 기준 |
| `CtdConfig`, `BubbleConfig` | CTD를 선택할 때 사용하는 기존 검출·flood 설정 |
| `PipelineConfig` | `drop_bbox_pad`, `skip_llm`, `expand_bubbles` |
| `RenderConfig` | 텍스트 색/스트로크, 패딩, mask 여백, 최소·최대 글자 크기. 최소 크기 기본값은 페이지 크기에 따라 정합니다. |

기본 Vision 시스템 프롬프트는 `lemon_manga_translator/prompts/vision_system.txt`에 동봉되어 있다.

- 기본 `openrouter_response_mode="standard"`는 각 블록의 `id`, `text_kind`, `no_translate`, `ko_translation`을 받습니다. `openrouter_response_mode="compact_reading"`는 같은 호출에서 일본어를 먼저 읽고 `[id, text_kind, ja_source, ko_translation]` 배열을 JSON schema로 받습니다. 번역할 때 일본어 원문도 비어 있지 않아야 하며, 보존할 때는 번역을 `null`로 반환합니다. 공개 결과 형식은 같습니다.
- 사용자 정의 프롬프트는 선택한 모드의 응답 형식을 따라야 합니다. `text_kind`는 [기본 프롬프트](lemon_manga_translator/prompts/vision_system.txt)의 분류 값 중 하나이며, 포장 상품명·장식 글자·효과음·기타·불확실한 글자는 코드에서 원문을 보존합니다. 수동 번역의 `VisionBlockResult` 형식은 세 필드를 유지합니다.

### 배경 마스킹

Vision LLM에 보내는 이미지에서 텍스트 bbox 바깥 영역을 마스킹할 수 있다. 배경 정보를 줄여 LLM이 텍스트에 집중하도록 유도하는 옵션.

```python
from lemon_manga_translator import MangaTranslator

mt = MangaTranslator(
    openrouter_api_key="sk-or-...",
    openrouter_model="google/gemini-3.1-flash-lite",
    bg_mask_mode="blur",  # "none" | "solid" | "blur" | "overlay"
)
```

| 모드 | 설명 |
|------|------|
| `none` | 기본값. 원본 이미지 그대로 전송. |
| `solid` | bbox 밖을 흰색으로 채움. |
| `blur` | bbox 밖을 강한 가우시안 블러 처리. |
| `overlay` | bbox 밖을 반투명 회색 오버레이. |

테스트 스크립트에서도 `--bg-mask` 플래그로 사용 가능:

```bash
uv run python scripts/test_pipeline.py image.jpg --bg-mask blur
```

### 배치 번역

여러 페이지를 병렬로 번역할 수 있다. 내부적으로 `ThreadPoolExecutor`를 사용하며, 입력 순서가 보장된다.

```python
images = [Image.open(p) for p in page_paths]
results = mt.translate_batch(images, max_workers=4)

for i, r in enumerate(results):
    r.save(f"output_{i:03d}.png")
```

내부 컴포넌트가 stateless라 단일 인스턴스를 스레드 간 공유해도 안전하다.

### 진행 상황 콜백

`on_progress`를 넘기면 각 단계를 시작할 때 `PipelineStage`로 호출된다. 텍스트가 없는 페이지는 `DETECT` 이후 바로 끝난다.

```python
from lemon_manga_translator import PipelineStage

def on_progress(stage: PipelineStage) -> None:
    print(stage)  # detect → translate → inpaint → render

result = mt.translate(image, on_progress=on_progress)
```

| 단계 | 파이프라인 |
|------|------|
| `DETECT` | 1~2 |
| `TRANSLATE` | 3~4 |
| `INPAINT` | 5 |
| `RENDER` | 6~7 |

`on_stage_done`을 넘기면 각 단계가 끝날 때 `StageResult`로 중간 결과를 받는다.

| 단계 | 채워지는 필드 |
|------|------|
| `DETECT` | `text_blocks` (bbox) |
| `TRANSLATE` | `vision_results` (블록별 번역, `no_translate`) |
| `INPAINT` | `image` (원문을 지운 이미지) |
| `RENDER` | `image` (최종 이미지) |

### 번역 수정 후 다시 렌더

`translations`에 `VisionBlockResult` 목록을 넘기면 Vision LLM을 호출하지 않고 그 번역으로 인페인트·렌더만 다시 한다. 같은 이미지는 검출 결과가 같으므로 `block_index`가 그대로 유지된다. `no_translate=True`인 블록은 지우지 않고 원문을 남긴다.

`text_boxes`로 추가하거나 바꾼 박스는 해당 사각형을 삭제·배치 영역으로 사용합니다. 좌표를 바꾸지 않은 박스는 학습 검출의 mask를 유지합니다. `block_index`는 박스 순서입니다. 수동 번역에서 `no_translate=False`와 빈 번역문을 함께 보내면 해당 원문만 지웁니다.

```python
from dataclasses import replace

first = []
mt.translate(image, on_stage_done=lambda result: first.extend(result.vision_results or []))

edited = [replace(first[0], ko_translation="수정한 번역")] + first[1:]
result = mt.translate(image, translations=edited)
```

### 로깅

라이브러리는 표준 `logging` 모듈로 단계별 진행 상황을 INFO 레벨로 기록한다. 호출자가 핸들러를 설정하면 실시간 로그가 보인다.

```python
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    datefmt="%H:%M:%S",
)

mt = MangaTranslator(...)
mt.prepare()
mt.translate(image)
```

샘플 출력:

```
11:53:10 INFO lemon_manga_translator.pipeline: 페이지 처리 시작 (크기=1200x1800)
11:53:10 INFO lemon_manga_translator.pipeline: [1/7] CTD 검출 완료: 텍스트 12블록
11:53:11 INFO lemon_manga_translator.pipeline: [3/7] Vision LLM 완료 (시도=1회, 대사=11, SFX/제외=3)
11:53:13 INFO lemon_manga_translator.pipeline: [7/7] 텍스트 렌더 완료 (11블록)
```

## Docker

`pre_download()`로 빌드 시 모델을 미리 캐싱하면 컨테이너 시작 시 다운로드 없이 바로 사용할 수 있다.

```dockerfile
FROM python:3.11-slim

RUN pip install lemon-manga-translator

# 빌드 시 ONNX 모델을 캐시에 다운로드 (세션 로드 없음)
RUN python -c "from lemon_manga_translator import MangaTranslator; \
    MangaTranslator.pre_download()"

COPY server.py .
CMD ["python", "server.py"]
```

```python
# server.py — prepare()는 캐시에서 즉시 로드
mt = MangaTranslator(
    openrouter_api_key=os.environ["OPENROUTER_API_KEY"],
    openrouter_model="google/gemini-3.1-flash-lite",
)
mt.prepare()
```

## Debug

```python
from lemon_manga_translator.api.debug_translator import DebugTranslator

dt = DebugTranslator(
    openrouter_api_key="sk-or-...",
    openrouter_model="google/gemini-3.1-flash-lite",
)
dt.prepare()

image = Image.open("page.jpg")
result = dt.translate(image, debug_dir="./debug/")

result.detection       # CTD 결과 (mask + text_blocks)
result.inpainted       # AOT 인페인트 결과
result.vision_results  # LLM 응답 (block_index, is_sfx, ko_translation)
result.save_all("./debug/")
```

`debug_dir`를 지정하면 각 단계의 중간 산출물이 파일명 prefix로 정렬되어 저장된다.

<details>
<summary>디버그 산출물 목록</summary>

| 파일 | 의미 |
|------|------|
| `00_input.png` | 원본 입력 이미지 |
| `00_params.json` | 실행 하이퍼파라미터 스냅샷 |
| `01_mask_raw.png` | UNet 원본 float 마스크 |
| `01_mask_refined.png` | 임계화 + 모폴로지 후처리 후 마스크 |
| `01b_mask_bbox_restricted.png` | 최종 텍스트 bbox 밖 마스크 제거 후 |
| `02_bboxes.png` | 원본 위에 최종 텍스트 박스 + 박스별 YOLO/UNet 신뢰도 라벨 |
| `02_text_blocks.json` | 검출된 텍스트 블록 메타(bbox, yolo_confidence 등) JSON |
| `02a_balloon_flood_scope.png` | YOLO seed 기반 CTD flood fill 영역 |
| `02a_balloon_flood_overlay.png` | CTD flood fill 영역을 원본 위에 하늘색 반투명 오버레이 |
| `03_mask_overlay.png` | 마스크를 원본 위에 핑크색 반투명 오버레이 |
| `03b_mask_filtered.png` | SFX 드롭 후 마스크 (해당 시만) |
| `03c_mask_filtered_overlay.png` | SFX 드롭 마스크 오버레이 (해당 시만) |
| `04_inpainted.png` | AOT 인페인트 결과 |
| `05a_annotated.png` | LLM에 전송된 번호 박스 이미지 |
| `05_vision.json` | LLM 응답 JSON |
| `05_cost.json` | 토큰/비용 정보 |
| `05b_render_bboxes.png` | 원본 bbox(빨강) + 확장된 render_bbox(초록) + 말풍선 확장 status/expansion ratio 라벨 |
| `06_final.png` | 최종 결과 |

</details>

## Dev Playground

파라미터를 바꿔가며 단계별로 결과를 확인하는 로컬 디버깅 GUI(`dev-tools/playground`)가 별도로 들어 있다 — 라이브러리 PyPI 패키지에는 포함되지 않는다. 사용법은 [`dev-tools/README.md`](dev-tools/README.md) 참고.

## 모델·코드·폰트 출처

모델 가중치는 [lemondouble/lemon-manga-translator](https://huggingface.co/lemondouble/lemon-manga-translator)에서 PyTorch(safetensors) + ONNX 형태로 제공된다.

| 구성 요소 | 출처 | 라이선스 |
|----------|------|---------|
| Comic Text Detector | 원 저자 [dmMaze/comic-text-detector](https://github.com/dmMaze/comic-text-detector) · 배포 [manga-image-translator beta-0.3](https://github.com/zyddnys/manga-image-translator/releases/tag/beta-0.3) | [GPL-3.0](https://github.com/dmMaze/comic-text-detector/blob/master/LICENSE) |
| AOT Inpainting | 아키텍처 [AOT-GAN](https://arxiv.org/abs/2104.01431) (Zeng et al., 2021) · 파인튜닝 [manga-image-translator beta-0.3](https://github.com/zyddnys/manga-image-translator/releases/tag/beta-0.3) · 리팩 [mayocream/aot-inpainting](https://huggingface.co/mayocream/aot-inpainting) | [GPL-3.0](https://github.com/zyddnys/manga-image-translator/blob/main/LICENSE) |
| `detection/ctd_utils/` | [zyddnys/manga-image-translator](https://github.com/zyddnys/manga-image-translator) `manga_translator/detection/ctd_utils` (벤더) | [GPL-3.0](https://github.com/zyddnys/manga-image-translator/blob/main/LICENSE) |
| `assets/fonts/Jua-Regular.ttf` | The Jua Project Authors / Woowahan Brothers · [Google Fonts: Jua](https://fonts.google.com/specimen/Jua) | [SIL OFL 1.1](https://openfontlicense.org/) |
| `assets/fonts/PretendardJP-Regular.otf` | [orioncactus/pretendard](https://github.com/orioncactus/pretendard) (Kil Hyung-jin) | [SIL OFL 1.1](https://github.com/orioncactus/pretendard/blob/main/LICENSE) |
| Vision LLM (런타임 API) | [OpenRouter](https://openrouter.ai/) 경유 호출 | 번들되지 않음. 사용 모델 제공자 ToS |

## License

[GPL-3.0](LICENSE)
