bec9411af3
Five workers built output URIs with inline f-strings, so the bucket-per-artifact layout was spread across worker_tts, worker_identity, worker_crop, worker_layers and worker_render. Moving a class between buckets meant a grep. They are now templates in transport.py, formatted at each call site. Three of those workers also each reimplemented the same parse to recover manga_id and chapter_id from an input uri, because the orchestrator does not send them. That is transport.ids_from_uri now, and it raises on a uri too short to carry the ids rather than returning a wrong pair. ruff.toml makes `ruff check .` exit 0, so CI can gate on it and a new finding means a new defect. Fixed: an implicit Optional in 8 signatures, an unparenthesized implicit concatenation in the ASS filter list, 5 subprocess.run calls now saying check=False out loud, an unused import, a duplicate exception handler and a non-executable shebang. Every rule left off carries its reason in ruff.toml. The ASYNC rules are off because ffmpeg on the event loop is real and already recorded at caveats/audit-open.md#blocking-event-loop. It needs a refactor per handler, not a lint fix. Checked: transport, collage, bubble_detect, test_vision_parse, worker_crop, worker_scene, worker_script, worker_identity, worker_tts, session_manager, worker_vision and worker_render self-checks all pass. worker_layers still fails on a missing legacy/qwen_layered_workflow.json, which predates this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
94 lines
4.5 KiB
Python
94 lines
4.5 KiB
Python
"""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} {ch['bbox']!s:28} {name:22} "
|
|
f"{'' if conf is None else f'{conf:.2f}'}")
|
|
else:
|
|
print(f"\npanel_index {WORKED_EXAMPLE} not found in this chapter")
|