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
+1
View File
@@ -44,3 +44,4 @@ still live belongs in `caveats/`.
| [A stage result proves nothing until the worker is newer than the edit](identity-bbox.md#stale-worker-invalidates) | closed |
| [A detection with no detected face never enrolls or binds](identity-bbox.md#face-gates-enrollment) | closed |
| [A resolver NONE mints an anonymous character, it does not clear the crop](identity-bbox.md#none-mints-an-anonymous-character) | closed |
| [The extras gate runs at enrollment and at narration, not at the speaker prompt](identity-bbox.md#extras-gate-consumers) | closed |
+30
View File
@@ -153,3 +153,33 @@ changed in the same session.
Forbids: treating an absent `character_id` as one outcome. A resolver that answered and a resolver that
failed are different facts.
## The extras gate runs at enrollment and at narration, not at the speaker prompt {#extras-gate-consumers}
**Closed, 2026-08-12. Written and self-checked, not yet proven on a GPU run.**
`#face-gates-enrollment` stops a faceless detection taking an identity. It does not stop the detection
being narrated, because three places read the raw vision character list and only one of them consults an
assignment. Panel 7 is the worked example: six detections, four of them extras or scenery.
`worker_scene.build_scene` already drops an unassigned detection from `characters` and `present`
(`worker_scene.py:63`), 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. Both now skip `has_face is False`.
`service._beat` picks the cinematographer's "who" from the first three detections, falling back to a
detection's action when it has no name. An extra could take a slot and steer the camera. Also gated.
`service._present_characters` is deliberately **not** gated. It builds the dialogue stage's candidate
speaker list and the set-of-mark boxes. Two reasons. The failure is already contained: an extra chosen as
the speaker has no identity assignment, so `normalize_dialogue` resolves it to unknown rather than to a
wrong name. And the gate's own cost lands hardest here, because a character drawn from behind has no face
box, so gating would delete a real speaker from the only list that can attribute their line.
All three gates test `is False`, never falsiness. A vision blob written before the gate existed carries no
`has_face` key, and a panel whose detector failed is marked `True` by the fail-open path. Both keep their
previous behaviour.
Forbids: adding a fourth consumer of `vision["characters"]` without deciding which side of this line it is
on. The blob keeps every detection on purpose, so the audit can still see what was gated.