Cut the dead attic workers, and file what the audit left standing
attic/worker_ocr.py and attic/worker_parse.py are 224 lines imported by nothing and named in no doc. The OCR stage was removed when narration moved to the director beat. The two design notes in attic/ stay, they are history. worker_vision._panel_size had one reference and it was the definition. The audit's larger finding is filed rather than fixed: call_gemma4, _extract_json and _strip_thought exist in both worker_vision and worker_script and have already diverged. That matters because the JSON repair pass can fabricate dialogue, so a fix would land in one copy and not the other. It is caveats/audit-open.md#gemma-helpers-duplicated with its revisit trigger. HANDOFF.md carries the rest: _wrap2 against textwrap, the duplicated ONNX preprocessing, and worker_layers pointing at a legacy/ directory that was never tracked in git. Checked: ruff clean, worker_vision and worker_render self-checks pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,99 +0,0 @@
|
||||
# worker_ocr.py — stage 3 text extraction. FastAPI :8001. cpu (easyocr), no session.
|
||||
# panel in -> text blocks with bboxes + confidence out. orchestrator persists to sqlite.
|
||||
# easyocr (not tesseract): it reads stylized manga lettering far better -- recovers whole
|
||||
# lines tesseract garbles or drops. runs on GPU (~0.4s/page warm) by default; the OCR stage
|
||||
# runs before any LLM session opens so it doesn't contend with the resident model. set
|
||||
# OCR_GPU=0 to force CPU (~3s/page). GPU needs MIOPEN_FIND_MODE=FAST in the env or the first
|
||||
# ROCm run spends ~60s in MIOpen's exhaustive kernel search -- the launcher sets it.
|
||||
import os, uuid
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
import transport
|
||||
|
||||
app = FastAPI()
|
||||
SHM = "/dev/shm"
|
||||
MIN_CONF = 0.3 # easyocr line confidence floor
|
||||
OCR_GPU = os.environ.get("OCR_GPU", "1") == "1"
|
||||
|
||||
_reader = None # easyocr.Reader, lazy-loaded on first request
|
||||
|
||||
|
||||
def _get_reader():
|
||||
global _reader
|
||||
if _reader is None:
|
||||
import easyocr
|
||||
_reader = easyocr.Reader(["en"], gpu=OCR_GPU, verbose=False)
|
||||
return _reader
|
||||
|
||||
|
||||
def _detections_to_texts(detections):
|
||||
"""easyocr readtext output [(box_pts, text, conf)] -> our text blocks with xywh bboxes.
|
||||
box_pts is 4 corner [x,y] points. drops low-confidence and art-noise (<2 letters).
|
||||
casing is left as-is (mixed) -- the vision stage re-cases from the image anyway."""
|
||||
texts = []
|
||||
for i, (box, txt, conf) in enumerate(detections):
|
||||
txt = txt.strip()
|
||||
if conf < MIN_CONF or sum(c.isalpha() for c in txt) < 2:
|
||||
continue
|
||||
xs = [p[0] for p in box]; ys = [p[1] for p in box]
|
||||
x, y = int(min(xs)), int(min(ys))
|
||||
texts.append({
|
||||
"id": f"t{i+1:03d}",
|
||||
"content": txt,
|
||||
"bbox": [x, y, int(max(xs)) - x, int(max(ys)) - y],
|
||||
"confidence": round(float(conf), 3),
|
||||
})
|
||||
return texts
|
||||
|
||||
|
||||
def ocr_image(path: str):
|
||||
return _detections_to_texts(_get_reader().readtext(path, detail=1, paragraph=False))
|
||||
|
||||
|
||||
class OCRInput(BaseModel):
|
||||
panel_uri: str
|
||||
job_id: str = ""
|
||||
panel_id: str = ""
|
||||
|
||||
|
||||
@app.post("/ocr")
|
||||
async def ocr(data: OCRInput):
|
||||
local = transport.get(data.panel_uri, f"{SHM}/ocr_{uuid.uuid4().hex[:8]}.png")
|
||||
texts = ocr_image(local)
|
||||
os.remove(local)
|
||||
return {"panel_id": data.panel_id, "texts": texts}
|
||||
|
||||
|
||||
@app.post("/unload")
|
||||
async def unload():
|
||||
"""free the resident easyocr reader (~1-2GB) once the OCR stage is done, before gemma4 loads.
|
||||
ocr isn't session-managed, so the orchestrator calls this at stage end."""
|
||||
global _reader
|
||||
was = _reader is not None
|
||||
_reader = None
|
||||
import gc; gc.collect()
|
||||
try:
|
||||
import torch; torch.cuda.empty_cache()
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True, "unloaded": was}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# self-check: detection->text-block conversion (pure, no model needed).
|
||||
dets = [
|
||||
([[10, 10], [110, 10], [110, 40], [10, 40]], "HELLO", 0.9), # kept
|
||||
([[10, 200], [70, 200], [70, 230], [10, 230]], "WORLD", 0.8), # kept
|
||||
([[5, 5], [13, 5], [13, 13], [5, 13]], "=", 0.9), # art-noise: <2 letters
|
||||
([[0, 0], [50, 0], [50, 20], [0, 20]], "REAL", 0.1), # below MIN_CONF
|
||||
]
|
||||
texts = _detections_to_texts(dets)
|
||||
assert [t["content"] for t in texts] == ["HELLO", "WORLD"], texts
|
||||
assert texts[0]["bbox"] == [10, 10, 100, 30], texts[0]["bbox"]
|
||||
assert texts[1]["confidence"] == 0.8
|
||||
print("worker_ocr self-check ok")
|
||||
@@ -1,125 +0,0 @@
|
||||
# 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")
|
||||
Reference in New Issue
Block a user