Files
manga-recap-pipeline/attic/worker_parse.py
T
kami ff6a512630 Reconstruct repo from Claude Code + codex transcripts
Working tree (including .git) was lost to an rm. Rebuilt by replaying Write/Edit/
Read/attachment events from 25 Claude sessions and 22 successful codex apply_patch
blocks into one timestamp-ordered timeline.

Verified against ground truth recorded in the transcripts: wc -l on 10 files and
ls -l on 5 files at 2026-07-18T13:13:44Z both match exactly; 18 files are
byte-identical to their newest ~/.claude/file-history blob.

See HANDOFF.md for sources, gaps, and how to rebuild .venv.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 02:42:41 +04:00

126 lines
5.2 KiB
Python

# worker_parse.py — manga parse (paged manga only). FastAPI :8009. GPU, session-guarded ("magi").
# Magi v2 chapter-wide pass: panel detection + reading order + OCR in one shot. Replaces the
# crop+ocr stages for paged manga; downstream ocr stage no-ops because rows are pre-populated.
# webtoons do NOT come here — they use worker_crop /crop/webtoon. See manga-two-repo-split memory.
import os, uuid
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import cv2
import numpy as np
import transport
app = FastAPI()
SHM = "/dev/shm"
MAGI_MODEL = "ragavsachdeva/magiv2"
_model = None
def _load_magi():
global _model
if _model is None:
import torch
from transformers import AutoModel
_model = AutoModel.from_pretrained(MAGI_MODEL, trust_remote_code=True).cuda().eval()
_model._torch = torch
return _model
class ParseInput(BaseModel):
page_uris: list # all pages of the chapter, in order
manga_id: str
chapter_id: str
session_id: str = "" # magi GPU lease (opened by orchestrator)
job_id: str = ""
def _center_in(box, panel) -> bool:
x1, y1, x2, y2 = box
px1, py1, px2, py2 = panel
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
return px1 <= cx <= px2 and py1 <= cy <= py2
def assemble_panels(pages, results, manga_id, chapter_id, put):
"""Flatten Magi's per-page output into chapter-order panels with their OCR.
`pages`: RGB np arrays. `results`: per-page dicts (Magi keys). `put(np_crop, uri)` uploads.
Text is assigned to the panel whose box contains the text-box center; SFX (non-essential)
is dropped so narration isn't polluted. bbox converted [x1,y1,x2,y2] -> [x,y,w,h]."""
out, gidx = [], 0
for img, res in zip(pages, results):
panels = res.get("panels", [])
texts = res.get("texts", [])
ocr = res.get("ocr", [])
essential = res.get("is_essential_text", [True] * len(texts))
for p in panels:
x1, y1, x2, y2 = (int(v) for v in p)
uri = f"s3://manga/{manga_id}/{chapter_id}/panels/p{gidx:03d}.png"
if not transport.exists(uri): # deterministic per gidx -> resumable
put(img[y1:y2, x1:x2], uri)
ocr_texts = []
for ti, tb in enumerate(texts):
if ti < len(ocr) and essential[ti] and _center_in(tb, p):
tx1, ty1, tx2, ty2 = (int(v) for v in tb)
ocr_texts.append({"text_id": f"t{ti}", "content": ocr[ti],
"bbox": [tx1, ty1, tx2 - tx1, ty2 - ty1], "confidence": 1.0})
out.append({"panel_index": gidx, "uri": uri,
"bbox": [x1, y1, x2 - x1, y2 - y1], "ocr": ocr_texts})
gidx += 1
return out
def _put_crop(np_rgb, uri):
tmp = f"{SHM}/parse_{uuid.uuid4().hex[:8]}.png"
cv2.imwrite(tmp, cv2.cvtColor(np_rgb, cv2.COLOR_RGB2BGR))
transport.put(tmp, uri)
os.remove(tmp)
@app.post("/parse")
async def parse(data: ParseInput):
tag = uuid.uuid4().hex[:8]
pages = []
for i, u in enumerate(data.page_uris):
local = transport.get(u, f"{SHM}/pp_{tag}_{i:03d}.png")
img = cv2.imread(local)
if img is None:
raise HTTPException(400, f"page not readable: {u}")
pages.append(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
os.remove(local)
model = _load_magi()
# ponytail: empty character bank in v1 — identity stays with the downstream siglip stage;
# feed a real bank (known-char ref crops + names) here to get Magi speaker association.
bank = {"images": [], "names": []}
with model._torch.no_grad():
results = model.do_chapter_wide_prediction(pages, bank, use_tqdm=False, do_ocr=True)
panels = assemble_panels(pages, results, data.manga_id, data.chapter_id, _put_crop)
return {"panels": panels}
@app.get("/health")
async def health():
return {"status": "ok"}
if __name__ == "__main__":
# self-check: model-free. Fake a 2-page Magi result and assert panel flattening,
# chapter-order indexing, text->panel containment, SFX drop, and bbox conversion.
transport.exists = lambda *a, **k: False # no minio in self-check
stored = {}
pages = [np.zeros((100, 100, 3), np.uint8), np.zeros((100, 100, 3), np.uint8)]
results = [
{"panels": [[0, 0, 50, 100], [50, 0, 100, 100]], # page 0: two panels
"texts": [[10, 10, 20, 20], [60, 10, 70, 20]], # one text in each
"ocr": ["HELLO", "BOOM"], "is_essential_text": [True, False]}, # BOOM = SFX, dropped
{"panels": [[0, 0, 100, 100]], # page 1: one panel
"texts": [[5, 5, 15, 15]], "ocr": ["WORLD"], "is_essential_text": [True]},
]
panels = assemble_panels(pages, results, "m", "c", lambda img, uri: stored.__setitem__(uri, img.shape))
assert [p["panel_index"] for p in panels] == [0, 1, 2], "chapter-order index"
assert panels[0]["ocr"][0]["content"] == "HELLO"
assert panels[1]["ocr"] == [], "SFX text dropped from panel 1"
assert panels[2]["ocr"][0]["content"] == "WORLD"
assert panels[0]["bbox"] == [0, 0, 50, 100], "xyxy->xywh"
assert panels[0]["ocr"][0]["bbox"] == [10, 10, 10, 10]
assert stored, "crops uploaded via put"
print("worker_parse self-check ok")