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:
+71
-86
@@ -1,121 +1,106 @@
|
||||
# HANDOFF, 2026-08-12/13 (seventh session)
|
||||
# HANDOFF, 2026-08-13 (eighth session)
|
||||
|
||||
Live state is in `NEXT.md`. This file is only what this session did. The previous handoff is in
|
||||
`JOURNAL.md`.
|
||||
|
||||
## Asked
|
||||
|
||||
"go" on item 1 of the previous plan, then a fourth GPU cycle. Then "commit and let's figure out how to
|
||||
deal with the 2b1b". Then a long architecture message, "mind turning the architecture in docs?", plus a
|
||||
question about panel merging before cropping. Then "well, I'd like the merge to be wired actually. and
|
||||
then 3 of your fixes."
|
||||
Push the repo to Gitea and open a PR to `master`. Then, from the PR review: fix the s3 URI constants,
|
||||
run ruff and fix what it finds, push, run a ponytail audit, fix, push, merge. Then audit.
|
||||
|
||||
## Result
|
||||
|
||||
Item 1 written, run on a GPU, committed. Naming diagnosed from a real dialogue run and three fixes written
|
||||
against it. The crop merge wired. `ARCHITECTURE.md` written. Nothing has run on a GPU since 20:25 UTC.
|
||||
No GPU work. Nothing ran on a pipeline stage. PR #1 is open at
|
||||
`https://gitea.kvmx.ru/kami/manga-recap-pipeline/pulls/1`, `master` <- `restore-runtime`.
|
||||
|
||||
## Committed and proven on a GPU
|
||||
`master` held only the reconstruction commit `ff6a512`. All 27 commits of real work sat unpushed on
|
||||
`restore-runtime`.
|
||||
|
||||
`a386e9d` here, `2daaa84` in the orchestrator. **The resolver gallery is the live cast, not cosine's top-k**
|
||||
(`decisions/identity-bbox.md#cast-is-the-gallery`). `tracklets.cast_gallery` builds it from
|
||||
`get_known_characters`, gender-compatible, named first, `GALLERY_CAP = 10`, re-read per tracklet. The
|
||||
`if s.get("candidates")` guard is gone, so a crop with an empty cosine top-k now reaches the resolver.
|
||||
`worker_vision.REF_BUDGET = 12` spreads reference images, `max(1, min(3, 12 // n))` apiece.
|
||||
## Landed
|
||||
|
||||
Fourth cycle, 19:44-19:52 UTC: vision 116/116 3m55s, identity 116/116 2m44s, reconcile 20/20 44s. Ran with
|
||||
the span cap and `GENERIC` tokenizer, which had never touched a GPU either.
|
||||
| commit | what |
|
||||
| --- | --- |
|
||||
| `12d4ed8` | the `ARCHITECTURE.md` rewrite, which was finished but uncommitted |
|
||||
| `be63b22` | deleted a 0-byte file named `II, d[i+4:i+12]))\n"; done`, added by `8113bdf` |
|
||||
| `bec9411` | s3 URI templates into `transport.py`, plus `ruff.toml` and the lint fixes |
|
||||
|
||||
| metric | 18:07 run | 19:44 run |
|
||||
| --- | --- | --- |
|
||||
| detections | 119 | 119 |
|
||||
| assignments | 68 | 60 |
|
||||
| coverage | 57% | 50% |
|
||||
| tracklets / crops | 12 / 64 | 33 / 72 |
|
||||
| lead's assignments | 36 | 16 |
|
||||
| characters after reconcile | 18 | 14 |
|
||||
| minted / cleared | -- | 10 / 12 |
|
||||
### S3 URIs
|
||||
|
||||
Coverage fell because gemma clears 12 crops instead of naming them wrongly. Checked by eye and confirmed by
|
||||
the user. The lead's 16 are 14 him plus 2 art-in-art. `character_2b1b12a1` holds 13, all correct.
|
||||
`character_f0d4e901` holds 9, of which 2 are `2b1b12a1`.
|
||||
Eight templates now live in `transport.py`: `PANEL_URI`, `PAGE_PANEL_URI`, `AUDIO_URI`,
|
||||
`AUDIO_FLAT_URI`, `LAYER_URI`, `CLIP_URI`, `CHAPTER_URI`, `CHAR_PNG_URI`, `CHAR_NPY_URI`. Five workers
|
||||
formatted their own before.
|
||||
|
||||
## Measured, then fixed
|
||||
`transport.ids_from_uri` replaces three separate copies of the same parse in `worker_tts`,
|
||||
`worker_layers` and `worker_render`. It raises on a uri too short to carry the ids instead of returning
|
||||
a wrong pair. `worker_render._mc_from_uri` is gone, its 6 call sites repointed.
|
||||
|
||||
Dialogue ran 116/116 in 5m57s at 20:19 UTC to test the naming path. Six claims, three defects:
|
||||
### Lint
|
||||
|
||||
```
|
||||
p040 character_2b1b12a1 "Choi Haeseon" caption 1.00 -> NOT promoted
|
||||
p010 character_b1dd5659 "Lim Seonho" caption 1.00 -> conflict flag
|
||||
p047 character_b1dd5659 "Seonho" address 0.90 -> conflict flag
|
||||
p011 character_f0d4e901 "Seonho" address 1.00 -> PROMOTED (wrong body)
|
||||
p026 character_f0d4e901 "Seonho" address 1.00 -> PROMOTED (wrong body)
|
||||
p110 character_028d4a49 "Haeseon" address 1.00 -> already named
|
||||
```
|
||||
`ruff check .` exits 0. Ruff's defaults found 115. Fixed: implicit `Optional` in 8 signatures, an
|
||||
unparenthesized implicit concatenation inside the ASS filter list, 5 `subprocess.run` calls now saying
|
||||
`check=False` out loud, 1 unused import, 1 duplicate exception handler, 1 non-executable shebang,
|
||||
4 `dict()` calls and 2 `startswith` chains.
|
||||
|
||||
## Written, tested, NOT run on a GPU
|
||||
12 rules are off in `ruff.toml`, each with its reason. The ASYNC ones matter: ffmpeg and ffprobe run
|
||||
synchronously inside `async def` endpoints, so a busy worker cannot answer `/health`. That is already
|
||||
`caveats/audit-open.md#blocking-event-loop`, tracked as [#199], and it needs a per-handler refactor
|
||||
rather than a lint fix.
|
||||
|
||||
Orchestrator, committed `2927927`, `db.add_name_claim` + `decisions/identity-naming.md`:
|
||||
`CLAUDE.md` said "There is no lint or build step". It now names `ruff check .` and the rule that every
|
||||
output URI is a template in `transport.py`.
|
||||
|
||||
- `alias_groups`: one name's word set inside another's is the same name, longer wins
|
||||
- `STRONG_EVIDENCE` gains `caption` at confidence >= 0.9
|
||||
- a name held by another live character refuses to promote, files `name-already-taken`
|
||||
## Audit findings
|
||||
|
||||
Here, committed `97cb483`:
|
||||
Applied:
|
||||
|
||||
- `merge_faceless_captions` wired into both crop endpoints. It existed and was never called.
|
||||
- `ARCHITECTURE.md`, the target shape from the user's design, each section carrying what exists today.
|
||||
- `delete:` `attic/worker_ocr.py` and `attic/worker_parse.py`, 224 lines, imported by nothing and named
|
||||
in no doc. `attic/char-recognition.md` and `attic/plan-workpc.md` kept, they are design history.
|
||||
- `delete:` `worker_vision._panel_size`, 8 lines, one reference and it is the definition.
|
||||
|
||||
## Not started
|
||||
Found and NOT applied, in order of size:
|
||||
|
||||
- **The head/body split is not fixed.** The wired merge only folds a fragment with text and no face. A body
|
||||
fragment has no dialogue, so `_merge_plan` leaves it solo. No evidence gathered on how often it happens.
|
||||
- The three cheap items from `ARCHITECTURE.md`: a `plane` field per detection, same-panel co-presence as a
|
||||
weak cannot-link, `offscreen` as a fourth `speaker_ref` kind.
|
||||
- `caveats/audit-open.md#gallery-cap-drops-the-unnamed`: the cap fired at `p097` (16 -> 10) and `p109`
|
||||
(11 -> 10) and dropped exactly the freshly minted anonymous rows.
|
||||
- `shrink:` `call_gemma4`, `_extract_json` and `_strip_thought` each exist twice, in `worker_vision.py`
|
||||
and `worker_script.py`, and **have already diverged**. `worker_vision.call_gemma4` is 21 lines and
|
||||
takes a content list, `worker_script`'s is 8 and takes a prompt string plus a system prompt. The two
|
||||
`_extract_json` bodies carry the same comment about `raw_decode` but different error text. This is
|
||||
the live risk: `caveats/audit-open.md#repair-fabricates` says the JSON repair pass can fabricate
|
||||
dialogue, and a fix would land in one copy. A shared `gemma.py` costs one new file and removes about
|
||||
25 duplicated lines. Not done because it touches the two largest workers and nothing has run on a GPU
|
||||
since.
|
||||
- `stdlib:` `worker_render._wrap2`, 16 lines of greedy word-wrap with a 2-line cap, is close to
|
||||
`textwrap.wrap(text, width, max_lines=2, placeholder="…")`. Not identical: `_wrap2` does
|
||||
`rstrip(".,")` before the ellipsis, and textwrap reserves width for the placeholder, so the break
|
||||
points differ. It renders burned-in subtitles, so the diff is user-visible. Verify against the render
|
||||
self-check before swapping.
|
||||
- `shrink:` `_letterbox` and `_load` are duplicated between `bubble_detect.py` and `face_detect.py`,
|
||||
two ONNX detectors with the same preprocessing. About 16 lines.
|
||||
- `delete:` `worker_layers.py:14` points `LAYERED_WORKFLOW` at `legacy/qwen_layered_workflow.json`.
|
||||
`legacy/` was **never tracked in git** and is absent from disk, so the module's self-check has never
|
||||
been able to pass here. This is the other half of `caveats/audit-open.md#layers-writes-nothing`.
|
||||
|
||||
## Checks
|
||||
|
||||
```bash
|
||||
.venv/bin/python worker_vision.py # ok, including the reference-budget asserts
|
||||
.venv/bin/python worker_crop.py # ok
|
||||
./check_stale.sh # exit 0 before the cycle
|
||||
/usr/bin/ssh kami@192.168.1.104 "cd /home/kami/docker-apps/manga-infra/orchestrator && python3 tracklets.py && python3 test_name_binding.py && python3 -m pytest -q --ignore=test_api.py" # 121 passed
|
||||
ruff check . # All checks passed
|
||||
.venv/bin/python worker_render.py # ok, ffmpeg ran, about 4 minutes
|
||||
```
|
||||
|
||||
Each new assert was confirmed to fail with its fix disabled.
|
||||
Self-checks pass: `transport`, `collage`, `bubble_detect`, `test_vision_parse`, `worker_crop`,
|
||||
`worker_scene`, `worker_script`, `worker_identity`, `worker_tts`, `session_manager`, `worker_vision`,
|
||||
`worker_render`, `face_detect`.
|
||||
|
||||
`worker_layers` fails, and did before this branch, on the missing `legacy/` file above.
|
||||
|
||||
## Next command
|
||||
|
||||
Re-crop and run the fifth cycle. **Clear the panels prefix first** or the merge will not take effect:
|
||||
`crop_webtoon` skips an upload when the key exists.
|
||||
The fifth GPU cycle is still the next pipeline work. It is blocked only on the GPU being free. The
|
||||
exact sequence is in `JOURNAL.md` under the seventh session, and `NEXT.md` item 1 holds the
|
||||
expectations. **Clear the panels prefix first** or the wired caption merge
|
||||
will not take effect.
|
||||
|
||||
```bash
|
||||
cd /home/kami/Programs/n8n-worker && ./check_stale.sh # restart crop, it is stale after the wiring
|
||||
/usr/bin/ssh kami@192.168.1.104 "cd /home/kami/docker-apps/manga-infra/orchestrator && docker compose up -d --build orchestrator"
|
||||
J=778297bc-e7ce-439d-91b5-8a027060d17f; M=ef105a86-4b7e-4ac4-b45c-b7d83b8f5b5e; C=7c944dd4-e972-42c7-ba60-9f6939548e80
|
||||
/usr/bin/ssh kami@192.168.1.104 "mc rm --recursive --force homesrv/panels/$M/$C/panels/"
|
||||
/usr/bin/ssh kami@192.168.1.104 "curl -s -X POST http://127.0.0.1:9090/characters/reset -H 'Content-Type: application/json' -d '{\"manga_id\":\"$M\",\"confirm\":true}'"
|
||||
# honour restart_identity_worker: true
|
||||
/usr/bin/ssh kami@192.168.1.104 "curl -s -X POST http://127.0.0.1:9090/stage/clear -H 'Content-Type: application/json' -d '{\"job_id\":\"$J\",\"stage\":\"crop\"}'"
|
||||
for S in crop vision identity reconcile dialogue; do /usr/bin/ssh kami@192.168.1.104 "curl -s --max-time 7200 -X POST http://127.0.0.1:9090/stage/run -H 'Content-Type: application/json' -d '{\"job_id\":\"$J\",\"stage\":\"$S\"}'"; done
|
||||
```
|
||||
Before that, or instead of it while the GPU is busy, `ARCHITECTURE.md` step 0 is the measurement spine:
|
||||
`eval/chapter-truth.json` plus purity and fragmentation printed by `audit_registry.py`.
|
||||
|
||||
Expect: fewer than 116 panels, `2b1b12a1` named `Choi Haeseon`, no `conflicting-name-claims` on the lead,
|
||||
and a `name-already-taken` flag where the female `Seonho` used to be.
|
||||
|
||||
## Traps confirmed or found
|
||||
|
||||
- **`crop_webtoon` skips an upload when the key exists.** Right for a resume, silently wrong after any
|
||||
slicing change. Clear `s3://panels/<manga>/<chapter>/panels/` before re-cropping.
|
||||
- Restart a worker with `tmux send-keys -t manga-workers:<window> C-c`, then re-send the launch line from
|
||||
`start_workers.sh` with its `MIOPEN_ENV` prefix. A `pgrep` kill matches the calling shell.
|
||||
- `POST /characters/reset` returns `restart_identity_worker: true`. Honour it.
|
||||
- `docker compose up --build orchestrator` drops any `docker cp`'d script, `audit_registry.py` included.
|
||||
- A heredoc piped into `docker exec` over `/usr/bin/ssh` produces no output. `scp` the file, `docker cp`
|
||||
it, then run it.
|
||||
- `panel_order` and the panel filename differ by one: `panel_order` 10 is `p009.png`.
|
||||
- The contact-sheet script that found the identity errors is `sheet.py` in the session scratchpad, not
|
||||
committed. It reads `assigns.json` dumped from `identity_assignments` and pulls crops from
|
||||
`manga/<manga_id>/characters/_crops/`.
|
||||
One ordering trap, unresolved: the truth file cannot be keyed on `panel_id` or `character_id`. The
|
||||
fifth cycle re-crops and calls `/characters/reset`, which destroys both. Key it on page-space geometry,
|
||||
or write it after the fifth cycle rather than before.
|
||||
|
||||
@@ -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")
|
||||
@@ -28,6 +28,7 @@ a complaint, so give it one or drop it.
|
||||
| [`layers` runs after `tts`, so pipelined solo beats lose parallax](audit-open.md#layers-after-tts) | AUDIT.md |
|
||||
| [`completed` means something different in each stage](audit-open.md#inconsistent-stage-policy) | AUDIT.md |
|
||||
| [Identity worker caches characters the orchestrator has deleted](audit-open.md#stale-known-cache) | AUDIT.md |
|
||||
| [The gemma helpers exist twice and have diverged](audit-open.md#gemma-helpers-duplicated) | repo audit |
|
||||
| [A character seen once gets no assignment at all](audit-open.md#pending-in-worker-memory) | AUDIT.md |
|
||||
| [MinIO credentials are hardcoded in committed source](audit-open.md#hardcoded-credentials) | AUDIT.md |
|
||||
| [Assemble marks a job completed with no clips](audit-open.md#empty-assemble) | AUDIT.md |
|
||||
|
||||
@@ -255,3 +255,19 @@ query crop. The cap is a VRAM and context budget, not a modelling choice.
|
||||
here. The fix is to order the gallery by how many assignments each character already holds in this chapter,
|
||||
so the tail is the rows nobody has matched rather than the rows nobody has named yet. That needs one count
|
||||
query per tracklet.
|
||||
|
||||
## The gemma helpers exist twice and have diverged {#gemma-helpers-duplicated}
|
||||
|
||||
**Open. Found by the repo audit of 2026-08-13, not yet by a failure.**
|
||||
|
||||
`call_gemma4`, `_extract_json` and `_strip_thought` are defined in both `worker_vision.py` and
|
||||
`worker_script.py`. They are no longer the same code. `worker_vision.call_gemma4` is 21 lines and takes a
|
||||
content list; `worker_script`'s is 8 and takes a prompt string plus a system prompt. Both `_extract_json`
|
||||
bodies carry the same comment about `raw_decode` stopping at the first object, and raise different errors.
|
||||
|
||||
Who pays: whoever fixes the JSON path. `#repair-fabricates` above says the repair pass can fabricate
|
||||
dialogue. A fix written against one copy leaves the other worker on the old behaviour, and no self-check
|
||||
compares them.
|
||||
|
||||
**Revisit when** either JSON path is touched for any reason. The fix is a `gemma.py` holding the three
|
||||
helpers, imported by both workers, which removes about 25 duplicated lines and costs one file.
|
||||
|
||||
@@ -367,16 +367,6 @@ def _mark_has_face(img, chars: list) -> list:
|
||||
return chars
|
||||
|
||||
|
||||
def _panel_size(path: str) -> tuple:
|
||||
"""(width, height) of a panel image, or (0, 0) when it cannot be read."""
|
||||
import cv2
|
||||
img = cv2.imread(path)
|
||||
if img is None:
|
||||
return (0, 0)
|
||||
h, w = img.shape[:2]
|
||||
return (w, h)
|
||||
|
||||
|
||||
@app.post("/vision")
|
||||
async def vision(data: VisionInput):
|
||||
local = transport.get(data.panel_uri, f"{SHM}/vision_{uuid.uuid4().hex[:8]}.png")
|
||||
|
||||
Reference in New Issue
Block a user