"""Registry audit for one chapter, after vision + identity + reconcile and before anything downstream. Runs inside manga-orchestrator (reads /data/manga.db). `audit_speakers.py` answers the attribution questions and needs the dialogue stage; this one answers the questions that decide whether dialogue is worth running at all: 1. did the bbox fix land — are stored boxes pixels, or still gemma's 0-1000 grid, 2. how many characters did the rebaseline mint, and did one of them absorb the chapter again, 3. what happened on panel 7, the worked example. Usage: docker exec manga-orchestrator python3 /app/audit_registry.py [chapter_id] [panel_index] """ import collections import json import sqlite3 import sys CHAPTER = sys.argv[1] if len(sys.argv) > 1 else "7c944dd4-e972-42c7-ba60-9f6939548e80" WORKED_EXAMPLE = int(sys.argv[2]) if len(sys.argv) > 2 else 7 c = sqlite3.connect("/data/manga.db") c.row_factory = sqlite3.Row manga_id = c.execute("SELECT manga_id FROM chapters WHERE chapter_id=?", (CHAPTER,)).fetchone()[0] reg = {r["character_id"]: dict(r) for r in c.execute( "SELECT character_id, name, aliases, gender, ref_image_uris, embedding_uri " "FROM characters WHERE manga_id=?", (manga_id,))} panels = c.execute( 'SELECT panel_id, panel_index, page_index, bbox FROM panels WHERE chapter_id=? ORDER BY panel_order', (CHAPTER,)).fetchall() # 1. coordinate space. A 0-1000 grid box on a panel wider or taller than 1000px cannot exceed 1000, # and clamps AT 1000. Real pixel boxes track the panel and scatter past it. The tell is the ratio of # the largest coordinate to the panel dimension, plus how many boxes sit exactly on 1000. detections = 0 past_1000 = at_1000 = 0 max_ratio = 0.0 assigned_total = 0 per_char = collections.Counter() worked = None for p in panels: row = c.execute("SELECT result_json FROM vision_results WHERE panel_id=?", (p["panel_id"],)).fetchone() if not row: continue v = json.loads(row["result_json"]) # the vision blob carries no panel size. panels.bbox is the panel's box on its page and is # [x, y, w, h], not corners — panel 3 of this chapter is [0, 615, 900, 106]. pb = json.loads(p["bbox"] or "null") pw, ph = (pb[2], pb[3]) if pb and len(pb) == 4 else (None, None) assigns = {a["local_id"]: (a["character_id"], a["confidence"]) for a in c.execute( "SELECT local_id, character_id, confidence FROM identity_assignments WHERE panel_id=?", (p["panel_id"],))} assigned_total += len(assigns) for cid, _ in assigns.values(): per_char[reg.get(cid, {}).get("name") or cid[:16]] += 1 people = [ch for ch in (v.get("characters") or []) if ch.get("bbox")] detections += len(people) for ch in people: x1, y1, x2, y2 = ch["bbox"] past_1000 += 1 if max(x2, y2) > 1000 else 0 at_1000 += 1 if 1000 in (x2, y2) else 0 if pw and ph: max_ratio = max(max_ratio, x2 / pw, y2 / ph) if p["panel_index"] == WORKED_EXAMPLE: worked = (p, v, people, assigns, pw, ph) named = [r for r in reg.values() if (r["name"] or "").strip()] print(f"registry: {len(reg)} characters, {len(named)} named -> {sorted((r['name'] or '') for r in named)}") print(f"detections: {detections} assignments: {assigned_total} " f"= {100*assigned_total/max(detections,1):.0f}% coverage") if per_char: top, n = per_char.most_common(1)[0] print(f"assignment spread: {dict(per_char.most_common(8))}") print(f" top character holds {n}/{assigned_total} = {100*n/max(assigned_total,1):.0f}% " f"({'ABSORBING, same signature as before' if n > 0.5 * assigned_total else 'ok'})") print(f"bbox space: {past_1000}/{detections} boxes exceed 1000, {at_1000} sit exactly on 1000, " f"largest coord/panel-dimension = {max_ratio:.2f}") print(f" verdict: {'PIXELS' if past_1000 or max_ratio > 0.02 and at_1000 == 0 else 'STILL 0-1000 GRID'}") missing_refs = [k for k, r in reg.items() if not r["ref_image_uris"] or not r["embedding_uri"]] print(f"characters missing a ref crop or embedding: {len(missing_refs)}") if worked: p, v, people, assigns, pw, ph = worked print(f"\npanel_index {WORKED_EXAMPLE} ({p['panel_id']}), {pw}x{ph}:") for ch in people: cid, conf = assigns.get(ch["local_id"], (None, None)) name = reg.get(cid, {}).get("name") or (cid[:16] if cid else "-- none --") print(f" {ch['local_id']:10} {str(ch['bbox']):28} {name:22} " f"{'' if conf is None else f'{conf:.2f}'}") else: print(f"\npanel_index {WORKED_EXAMPLE} not found in this chapter")