Audit Phase 1: correctness and scheduling safety
Implements every P0 from AUDIT.md plus four P1s, across both halves of the pipeline. Verified by CPU-only self-checks and the orchestrator test suite. No GPU work ran and no pipeline ran. workpc: - worker_scene: read speaker_ref, not the rewritten speaker field. Every line narrated as "Someone" before this. Emit `actions` for the verifier. - worker_script: declare beat + verifier_feedback (pydantic dropped both, so the retry was blind) and render them as a repair prompt. - worker_vision: gate face->identity pairing on containment, assign globally shortest-first, map an out-of-range resolver index to `unresolved` instead of minting a character, parse JSON with raw_decode. - session_manager: tear down a server whose lease vanished mid-load, and spawn the supervisor respawn unlocked. orchestrator (edited in place, NOT committed there): - tracklets: canonicalize gender, add co-presence cannot-links, block transitive bridges across a hard constraint. - correctness: stop failing valid narration on sentence-initial capitals and short quotes; read action evidence from the singular key. - db: stop orphan flags leaking into every chapter; resolve by flag id. - service: TTS returns instead of raising under GATES, auto-resolves under autonomous mode; job admission control; registry names on dialogue resume. - session_proxy: queue on 409 instead of stealing the lease; run heartbeats. Docs restructured per the repo-structure layout: CLAUDE.md is a pointer table, NEXT.md replaces HANDOFF.md, plus ROADMAP.md, JOURNAL.md, decisions/ and caveats/. AUDIT.md now points at those instead of restating them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XD7cAy81MZrc7gCr6aZGWr
This commit is contained in:
+48
-6
@@ -21,6 +21,10 @@ class ScriptInput(BaseModel):
|
||||
recent: list = [] # #1: last few panels' narration text (local flow, no repeats)
|
||||
introduced: list = [] # #2: names already narrated -> refer by name/pronoun, don't re-describe
|
||||
panel_count: int = 1 # scene-level narration: #panels in this beat -> scales the length budget
|
||||
# the orchestrator has been sending both of these on a failed-script retry since the verifier
|
||||
# landed; pydantic dropped them silently, so the retry was another blind roll of the dice.
|
||||
beat: dict = {} # {beat_id, panels:[{panel_id, dialogue:[{text}], actions}]} — the evidence
|
||||
verifier_feedback: list = [] # [{kind, values|quote}] from verify_script on the previous attempt
|
||||
|
||||
|
||||
class SummaryInput(BaseModel):
|
||||
@@ -80,9 +84,33 @@ def _chars_line(name_by_id, genders_by_id):
|
||||
return ", ".join(out) or "none"
|
||||
|
||||
|
||||
def _feedback_block(feedback, beat) -> str:
|
||||
"""Render the previous attempt's verifier failures as concrete corrections. Without this the
|
||||
retry is just another sample at the same temperature — the model never learns what was wrong."""
|
||||
if not feedback:
|
||||
return ""
|
||||
lines = []
|
||||
for f in feedback:
|
||||
kind = f.get("kind")
|
||||
if kind == "unsupported-proper-noun":
|
||||
lines.append("- You used name(s) that are not in this beat: "
|
||||
+ ", ".join(str(v) for v in f.get("values", []))
|
||||
+ ". Use only the names listed under 'Characters present', or a pronoun.")
|
||||
elif kind == "misquote":
|
||||
lines.append(f'- Your quote "{f.get("quote","")}" is not what the panel says. '
|
||||
"Quote the exact words below, or drop the quotation marks.")
|
||||
else:
|
||||
lines.append(f"- {kind}: {json.dumps(f, ensure_ascii=False)[:200]}")
|
||||
quotes = [d.get("text", "") for p in (beat or {}).get("panels", [])
|
||||
for d in p.get("dialogue", []) if d.get("text")]
|
||||
exact = ("Exact lines you may quote from:\n" + "\n".join(f'- "{q}"' for q in quotes) + "\n") if quotes else ""
|
||||
return ("Your previous attempt was REJECTED. Fix exactly these problems and rewrite it:\n"
|
||||
+ "\n".join(lines) + "\n" + exact + "\n")
|
||||
|
||||
|
||||
def build_prompt(sg: dict, chapter_context: str, names_by_id=None,
|
||||
brief: str = "", recent=None, introduced=None, genders_by_id=None,
|
||||
panel_count: int = 1) -> str:
|
||||
panel_count: int = 1, beat=None, verifier_feedback=None) -> str:
|
||||
name_by_id = _name_map(sg.get("characters", []), names_by_id)
|
||||
chars = _chars_line(name_by_id, genders_by_id)
|
||||
dialogue = "\n".join(_render_line(d, name_by_id) for d in sg.get("dialogue", [])) or "none"
|
||||
@@ -120,7 +148,7 @@ def build_prompt(sg: dict, chapter_context: str, names_by_id=None,
|
||||
else:
|
||||
unit, budget, thin = "this manga panel", "1-2 short sentences, max ~25 words", "panel"
|
||||
return (
|
||||
ov + rec + ctx + intro +
|
||||
_feedback_block(verifier_feedback, beat) + ov + rec + ctx + intro +
|
||||
f"Write TIGHT recap narration for {unit}: {budget}, "
|
||||
"present tense. Tell it like you're recapping to a friend — natural, with momentum. Output "
|
||||
"only the narration — no preamble, no notes, no alternatives, never ask for more info. If the "
|
||||
@@ -182,7 +210,7 @@ async def summary(data: SummaryInput):
|
||||
async def script(data: ScriptInput):
|
||||
text = call_gemma4(build_prompt(data.scene_graph, data.chapter_context, data.names_by_id,
|
||||
data.brief, data.recent, data.introduced, data.genders_by_id,
|
||||
data.panel_count))
|
||||
data.panel_count, data.beat, data.verifier_feedback))
|
||||
return {"panel_id": data.panel_id, "text": text}
|
||||
|
||||
|
||||
@@ -199,10 +227,16 @@ class NormalizeInput(BaseModel):
|
||||
|
||||
def _extract_json(raw: str) -> dict:
|
||||
text = _strip_thought(raw)
|
||||
m = re.search(r"\{.*\}", text, re.DOTALL)
|
||||
if not m:
|
||||
i = text.find("{")
|
||||
if i < 0:
|
||||
raise ValueError(f"no json: {text[:200]}")
|
||||
return json.loads(m.group(0))
|
||||
# raw_decode stops at the end of the FIRST object. The old greedy `\{.*\}` swallowed any trailing
|
||||
# brace in prose after it and turned a parseable reply into a wasted repair call.
|
||||
try:
|
||||
obj, _ = json.JSONDecoder().raw_decode(text[i:])
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"bad json: {text[:200]}") from e
|
||||
return obj
|
||||
|
||||
|
||||
def build_normalize_prompt(names, entities) -> str:
|
||||
@@ -297,4 +331,12 @@ if __name__ == "__main__":
|
||||
nr = _extract_json('{"entities":[{"name":"Everyday","kind":"shop"}],'
|
||||
'"name_fixes":{"CHOI HAESEON":"Choi Haeseon"}}')
|
||||
assert nr["entities"][0]["name"] == "Everyday" and nr["name_fixes"]["CHOI HAESEON"] == "Choi Haeseon"
|
||||
# trailing braced prose after the object must not break the parse (was a wasted repair call)
|
||||
assert _extract_json('{"a":1}\nnote: {not json}')["a"] == 1
|
||||
# verifier feedback reaches the retry prompt with the exact allowed quotes; absent by default
|
||||
assert "REJECTED" not in p
|
||||
beat = {"beat_id": "b1", "panels": [{"panel_id": "p1", "dialogue": [{"text": "stand proud"}]}]}
|
||||
pf = build_prompt(sg, "s", beat=beat, verifier_feedback=[
|
||||
{"kind": "unsupported-proper-noun", "values": ["Kyoto"]}, {"kind": "misquote", "quote": "be proud"}])
|
||||
assert "REJECTED" in pf and "Kyoto" in pf and "be proud" in pf and '- "stand proud"' in pf
|
||||
print("worker_script self-check ok")
|
||||
|
||||
Reference in New Issue
Block a user