Stop a background extra's action reaching narration

build_scene already dropped an unassigned detection from `characters` and
`present`, so an extra never reached the cast list. Its ACTION did. `actions` was
built from every detection, and that list is what the script prompt renders and
what the verifier uses as evidence, so "standing at the window" arrived as a fact
about the panel with no character attached and the verifier confirmed it, because
the action really was in the blob.

Skip has_face is False, the same gate and the same fail-open semantics as
enrollment. Self-check covers all three cases: a real cast member's action
survives, a faceless one's does not, and a detection from a panel where the
detector never ran keeps its action.

decisions/identity-bbox.md#extras-gate-consumers

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 19:16:30 +04:00
parent 491dad1c67
commit ca4661763c
3 changed files with 52 additions and 1 deletions
+21 -1
View File
@@ -91,7 +91,13 @@ def build_scene(data: SceneInput):
vchars = data.vision_result.get("characters", [])
# `actions` is the list the orchestrator's beat builder reads for verifier evidence; `action` is the
# joined string the script prompt renders. Emitting only the string left verification blind.
actions = [c["action"].strip() for c in vchars if str(c.get("action") or "").strip()]
# `characters` above already drops an unassigned detection, but its ACTION used to survive, so a
# background extra standing at a window became a sentence the narrator read out. A detection with no
# detected face is an extra, a figure on a poster, or scenery gemma called a person
# (`decisions/identity-bbox.md#face-gates-enrollment`). Gate on `is False`, like the enrollment gate,
# so a blob written before the gate existed and a panel whose detector failed both behave as before.
actions = [c["action"].strip() for c in vchars
if str(c.get("action") or "").strip() and c.get("has_face") is not False]
action = "; ".join(actions)
return {"panel_id": data.panel_id, "characters": characters, "dialogue": dialogue,
"action": action, "actions": actions,
@@ -132,6 +138,20 @@ if __name__ == "__main__":
"description": '{"hair":"black","features":["glasses"]}'}],
))
assert outu["characters"][0]["name"] == "" and outu["characters"][0]["label"] == "the one with black hair and glasses"
# a faceless detection's ACTION never reaches the script prompt or the verifier's evidence, but a
# real cast member's does, and so does one on a panel where the detector never ran (no key).
p7 = build_scene(SceneInput(
panel_id="p007",
vision_result={"characters": [
{"local_id": "person_1", "action": "reading a report", "has_face": True},
{"local_id": "person_2", "action": "standing at the window", "has_face": False},
{"local_id": "person_3", "action": "pointing"},
]},
identity_assignments=[{"local_id": "person_1", "character_id": "c1"}],
characters_registry=[{"character_id": "c1", "name": "Seonho"}],
))
assert p7["actions"] == ["reading a report", "pointing"], p7["actions"]
assert "window" not in p7["action"], p7["action"]
assert _describe({}) == "" # bare appearance -> empty, script falls back to Person X
# animals are named as their species (color + species), never by clothing/"the one with..."
assert _describe({"species": "cat", "hair": "black"}) == "the black cat"