What the conflict checker outputs — real example (Tadej alarm shot)¶
This is the real, unedited output of the pre-generation validation for one production shot, validated live on 2026-08-04. Full raw JSON: pregen-validation-output-example.json.
The shot: "Phone screen ignites with cycling anthem alarm blaring" — Tadej's smartphone alarm, extreme close-up, top-down. The shot has a sketch (never analyzed), one character (Tadej), one location (Tadej's Bedroom), one prop (Tadej's Smartphone Alarm).
1. The verdict (one per shot)¶
| Field | Value for this shot | Meaning |
|---|---|---|
status |
blocked |
Overall verdict: pass / pass_with_warnings / review_required / blocked / validation_failed |
selectedSketchMoment |
middle |
Which moment of the shot the sketch depicts (start/middle/end) — inferred by the vision model |
validatedAt |
2026-08-04 11:46 | When it ran |
inputHash |
717cb3f2f88d70cf |
Fingerprint of the inputs — same inputs later = cached verdict, no re-run. Full sha256 hex (64 chars) on current runs; this example was captured while it was still truncated |
2. The conflicts (the interesting part — 7 found on this shot)¶
Every conflict carries the same structure. Here are three representative ones, exactly as stored:
Example A — deterministic blocker (no AI needed)¶
code: SKETCH_ANALYSIS_MISSING
severity: blocker → red banner in design, gates generation
one-liner: "Sketch analysis not found"
found: "The selected sketch has not been analyzed. Run sketch analysis
first — it labels characters and produces the notes the checks
and the prompt rely on."
fields: [sketch] → highlights the Sketch card
dismissible: false → no "Not a conflict" button; must be resolved
Example B — camera blocker with a fix (vision model, Stage 3)¶
Note the severity: the catalog's default for this code is risky, and the model raised it to blocker for this instance. That is normal — see §4.
code: SKETCH_CAMERA_MISMATCH
severity: blocker (catalog default: risky — model decided)
one-liner: "Shot size in sketch doesn't match in Shot size property"
found: "The sketch is framed wider than the described extreme close-up.
The frame description says the phone screen should fill the
frame with only the nightstand edge and a blanket corner also
visible, but the sketch includes extra tabletop space plus a
digital clock and lamp base."
proposed fix: "Tighten the crop so the phone is the dominant subject and
remove the clock/lamp from frame."
fields: [FRAME DESCRIPTION, CAMERA PROPERTIES, STAGE 2 SKETCH OBSERVATION]
dismissible: false
"FRAME DESCRIPTION" here means the checker's derived frame text, not the creator's field — see the box in §3.2.
Example C — dismissible pose issue (the "Not a conflict" candidate)¶
code: SKETCH_POSE_ACTION_MISMATCH
severity: risky
one-liner: "Pose in sketch doesn't match frame description"
found: "The sketch mixes multiple action moments with a ghosted retreat
hand and motion arrow, instead of depicting the single instant
of impact described."
proposed fix: "Keep only the impact pose: one solid hand flattened on the
phone, no ghosted positions or directional arrow."
fields: [FRAME DESCRIPTION, STAGE 2 SKETCH OBSERVATION]
dismissible: true → user can mark "Not a conflict"
All 7 on this shot, at a glance¶
| # | Severity | Code | One-liner |
|---|---|---|---|
| 1 | blocker | SKETCH_ANALYSIS_MISSING | Sketch analysis not found |
| 2 | blocker | SKETCH_CAMERA_MISMATCH | Shot size in sketch doesn't match in Shot size property |
| 3 | blocker | SKETCH_ANGLE_MISMATCH | Camera angle in sketch doesn't match in Camera angle property |
| 4 | blocker | SKETCH_POSITION_MISMATCH | Character position in sketch doesn't match frame description |
| 5 | risky | SKETCH_COMPOSITE_MOMENTS | Sketch combines several moments in one drawing |
| 6 | risky | SKETCH_POSE_ACTION_MISMATCH | Pose in sketch doesn't match frame description |
| 7 | minor | dialogue_truncated (model-invented code — see §4) |
Shot properties contradict each other |
Reading it as a story: the sketch for this shot is drawn as an oblique medium view mixing several action moments, while the shot metadata asks for a single top-down extreme close-up of the phone at the instant the hand slams it. The checker caught the framing, the angle, the layout and the multi-moment drawing, and attached a concrete fix to 4 of the 7 (the deterministic missing-analysis blocker and the two multi-moment/text findings have no suggestion — suggestion is optional).
Row 7 also shows the Stage-1 problem: the code is dialogue_truncated, invented by the model, so the user just sees the generic "Shot properties contradict each other." The actual finding — a dialogue line cut off mid-sentence — is only in description.
3. Design UI fields — everything available to show on the review screen¶
For the head of design and the frontend engineer: this is the inventory of what can go on the "Review all inputs before generation" screen, and where each piece comes from.
Read the "Worth showing?" / "Notes" columns carefully — not everything here ships in the validate response. Three groups:
- In
validation— comes back from the validate call, ready to render. - On the shot / project / story — the frontend already fetches these separately; validation does not echo them back.
- Needs backend work — flagged inline where it applies (asset
variantId, the annotated sketch URL, Stage-1 conflict codes).
3.1 Visual references (image cards)¶
Source: validation.resolvedInputs — the EXACT images the checker validated, same resolution generation uses.
| Card | What's available per card | Notes |
|---|---|---|
| Sketch | resolvedInputs.sketchUrl |
Always first. Conflicts about the drawing attach here |
| Character (one card each) | one references[] entry with type: "character" — name, imageUrl, assetId |
One card per character assigned to the shot that has a reference image |
| Location | one references[] entry with type: "environment" |
The environment reference |
| Prop (one card each) | references[] entries with type: "prop" |
Includes container props (a bus/tent interior that acts as the setting) |
Each references[] entry is exactly { type, name, imageUrl, assetId } — four keys, nothing else. Two caveats:
variantIdis not in the payload. The collector resolves the variant to pick the right image, but only theassetIdsurvives intoresolvedInputs. If the UI needs to show which view ("Default / Back / Side"), that's a backend addition, not something to read off this object.- The sketch URL is the raw sketch, not the annotated one.
resolvedInputs.sketchUrlisinput.sketch.rawUrl. Stage 3 actually looks at the annotated version when one exists (selectedForGenerationUrl), and that URL is not exposed. Also a backend addition if design wants the C1/C2 labels visible.
Reference counts per shot in the measured scenes ranged from 1 to 6.
Per-card conflict decoration: each conflict carries fieldPaths + a code, so the UI knows which card gets the amber border and the inline conflict text (prop conflicts → prop card, location conflicts → location card, sketch conflicts → sketch card). Note that Stage-3 fieldPaths are section labels, not asset names — mapping a prop conflict to a specific prop card means matching on the prop name inside description/evidence, which is unreliable. Safest V0: decorate by card type, not by individual asset.
Not available (yet): reference shots (prior generated shots used for lighting/atmosphere) — not part of validation V0.
3.2 Textual references (field rows) — exhaustive list¶
Source: the shot document (what the user/system wrote) + validation.frameCandidates (what the checker derived). Suggested display label → actual field.
Only validation.* rows come back from the validate endpoint. Everything sourced from the shot document, the project or the story is a separate fetch the frontend already does — validation does not echo those fields back.
The one thing to get right:
frameCandidatesis the checker's own text¶Stage 1 rewrites the shot's text fields into three static frame descriptions and stores them in
frameCandidates. Stage 3 then compares the sketch againstframeCandidates[selectedSketchMoment]— not againstinitialFrameInstructions/middleFrameInstructions/finalFrameInstructions(selectedFrameAlignmentStage.ts:61).Conflict descriptions then say things like "the frame description says the phone screen should fill the frame" — quoting the derived text. If the UI only shows the creator's fields, the user reads that sentence, goes looking for it in their own text, and it isn't there. Our PM hit this on the first pass and reported it as a bug.
Concrete example from the measured scene (shot 1). Creator wrote, in
initialFrameInstructions:"Becca stands frame-left and Tim stands frame-right beside the Bubble Car at center-right, both waist-up with the driveway and street behind them."
frameCandidates.start, which is what the checker actually compared the sketch to:"Medium eye-level static 2-shot, framed head-to-waist. Becca stands frame-left at roughly 30% of the frame, body angled slightly three-quarter right, shoulders square and chin level, eyes fixed off-frame right. Tim stands frame-right at roughly 65% of the frame, body also angled slightly three-quarter…"
Same intent, far more specific — and the extra specifics are what conflicts get raised against. Show the derived text on the review screen, labelled as derived (e.g. "What the checker compared your sketch to"), otherwise the conflict text is unverifiable to the user.
| Display label | Field | What it contains | Worth showing? |
|---|---|---|---|
| Description | description |
One-line summary of the shot | yes — primary |
| Performance | actingInstructions |
The movement/acting across the shot (temporal) | yes — primary |
| Initial frame | initialFrameInstructions |
Static description of the start moment, as the creator wrote it | yes |
| Middle frame | middleFrameInstructions |
Static description of the middle moment | yes, but it is empty in practice: nothing in the product writes this field (it's not in ALLOWED_METADATA_FIELDS, so SketchAIDA can't set it) and it was empty on 16/16 shots of the scene we measured |
| Final frame | finalFrameInstructions |
Static description of the end moment | yes |
| What the checker actually compared against | validation.frameCandidates.{start,middle,end} |
See the box below — this is not the creator's text | yes, and label it clearly as derived |
| Sketch shows | validation.selectedSketchMoment |
start / middle / end — which moment the sketch depicts, inferred by Stage 2 | yes — one chip, very useful context. The shot field sketchConnectedTo exists in the schema and the checker reads it if set, but nothing writes it today, so in practice this always comes from validation |
| Shot size | shotSize |
Enum: WIDE, MEDIUM, CLOSE_UP… | yes |
| Shot size details | shotSizeDetails |
Free text explaining the framing | yes |
| Camera angle | cameraAngle |
Enum: EYE_LEVEL, LOW_ANGLE, TOP_DOWN… | yes |
| Camera angle details | cameraAngleDetails |
Free text | yes |
| Camera movement | cameraMovement |
Enum: STATIC, PAN, PUSH_IN… | yes |
| Camera movement details | cameraMovementDetails |
Free text | optional |
| Dialogue | dialogues[] (structured: speaker + line) with legacy dialogue fallback |
Who says what in this shot | yes |
| Timing | timing |
Shot duration in seconds | optional |
| Sound | sound |
Sound notes | optional |
| Effect | effect + effectDetails |
Visual effect + rationale | optional |
| Turn marker | turnMarker |
The meaningful change in this shot | probably not (internal) |
| Visual tactics | visualTactics |
Rationale object for visual choices | probably not (internal) |
| User feedback | feedback |
Regeneration feedback the user typed | only in regenerate flows |
| Art style | project artStyle |
Project-wide art style text | optional (right column context) |
| Creative intent | story creativeIntent.summary |
Tone/mood of the story | optional (right column context) |
Same per-row conflict decoration as the cards: conflicts carry fieldPaths (e.g. ["dialogue","actingInstructions"]) → highlight those rows.
3.3 Verdict area (from validation itself)¶
| Element | Source | Notes |
|---|---|---|
| Overall verdict | status |
pass / pass_with_warnings / review_required / blocked / validation_failed |
| Generate allowed? | canGenerate |
drives the button |
| Conflict list | conflicts[] |
the banner (see §4 for the anatomy of each) |
| "Sketch shows the END of this shot" | selectedSketchMoment + sketchAlignmentConfidence |
nice contextual chip |
| "What the checker compared your sketch to" | frameCandidates[selectedSketchMoment] |
the derived frame text conflicts are raised against — see the box in §3.2. Label it as derived, not as the creator's own words. Optional field: absent on results validated before 2026-08-04 |
| Checked at / freshness | validatedAt |
e.g. "checked 2 min ago" |
4. The conflict object, key by key (for PM/design)¶
Every conflict is one JSON object. Here is each key, the values it can take, and where it appears in the UI:
code — what kind of problem it is¶
Not shown to the user directly: the code selects the one-liner text, and it's the stable ID we count in analytics ("which conflict type do users dismiss most?"). The full catalog, one row per code, with the exact one-liner the user sees.
The "Default severity" column is a fallback, not a guarantee. For every LLM-produced finding the model reports its own severity and that wins:
severity: finding.severity || template.defaultSeverity(preGenValidation.service.ts:321). The catalog value only applies when the model omits it, or for the deterministic non-LLM conflicts (SKETCH_ANALYSIS_MISSING,SKETCH_MATCHES_NO_FRAME,SKETCH_COMPOSITE_MOMENTS,VALIDATION_INFRASTRUCTURE_FAILED), which always use the catalog value.Measured on 31 real shots (2 scenes):
SKETCH_POSE_ACTION_MISMATCH(cataloguedrisky) came back blocker 6× · risky 10× · minor 3×.SKETCH_PROP_STATE_MISMATCH(cataloguedrisky) came back blocker 9× · risky 4×.ENVIRONMENT_VARIANT_MISMATCH(cataloguedblocker) came back risky 3× and blocker 0×.Do not build UI that maps a code to a severity. Read
severityoff each conflict object.
The sketch contradicts the shot's settings (found by comparing the drawing against the metadata)
All model-decided. "Observed" = what actually came back across 31 real shots.
| Code | One-liner shown to the user | Default severity | Observed |
|---|---|---|---|
SKETCH_CAMERA_MISMATCH |
Shot size in sketch doesn't match in Shot size property | risky | blocker 5 · risky 3 |
SKETCH_ANGLE_MISMATCH |
Camera angle in sketch doesn't match in Camera angle property | risky | blocker 1 · minor 1 |
SKETCH_ENTITY_COUNT_MISMATCH |
Characters in sketch don't match Characters in shot | risky | blocker 1 |
SKETCH_POSITION_MISMATCH |
Character position in sketch doesn't match frame description | risky | blocker 4 · risky 6 |
SKETCH_POSE_ACTION_MISMATCH |
Pose in sketch doesn't match frame description | risky | blocker 6 · risky 10 · minor 3 |
SKETCH_PROP_STATE_MISMATCH |
Prop in sketch doesn't match Props property | risky | blocker 9 · risky 4 |
SKETCH_ENVIRONMENT_MISMATCH |
Location in sketch doesn't match in Location variant | risky | blocker 3 · risky 2 · minor 1 |
The sketch itself has a problem (regardless of the metadata)
These four are deterministic — raised by code, not by the model, so the severity below is the real severity, always.
| Code | One-liner shown to the user | Severity | Raised when |
|---|---|---|---|
SKETCH_ANALYSIS_MISSING |
Sketch analysis not found | blocker | the selected sketch has no stored analysis (fires on every un-analyzed shot) |
SKETCH_MATCHES_NO_FRAME |
Sketch doesn't match any moment of this shot | blocker | best moment score < 0.4 |
SKETCH_COMPOSITE_MOMENTS |
Sketch combines several moments in one drawing | risky | Stage 2 sets appearsCompositeOrMultiMoment |
SKETCH_MOMENT_AMBIGUOUS |
Sketch could show more than one moment of the shot | risky | defined in the catalog but nothing raises it today — no code path emits this. Treat as reserved |
A reference image is missing or wrong (character/location/prop images)
All model-decided.
| Code | One-liner shown to the user | Default severity | Observed |
|---|---|---|---|
CHARACTER_REFERENCE_MISSING |
Character has no reference image | blocker | never fired in 31 shots |
ENVIRONMENT_VARIANT_MISMATCH |
Location in sketch doesn't match in Location variant | blocker | risky 3 (blocker 0) |
PROP_REFERENCE_MISSING |
Prop has no reference image | risky | risky 5 · blocker 1 |
REFERENCE_SKETCH_INCOMPATIBLE |
A reference image can't work with this sketch composition | risky | risky 3 · blocker 1 |
The text fields contradict each other (no images involved)
Read this before using these codes for anything. Stage 1's code field has no enum in the response schema (stageSchemas.ts:57 — code: { type: 'string' }), so the model writes whatever string it wants. Measured over 31 shots: 14 text conflicts, 14 distinct invented codes, and not one of them matched the catalog. Every one fell through resolveIssueTemplate() to the generic one-liner "Shot properties contradict each other."
| Code | One-liner shown to the user | Default severity | Observed |
|---|---|---|---|
TEMPORAL_PROGRESSION_CONTRADICTION |
Start, middle and end don't form one coherent shot | blocker | never fired |
TEXT_FIELD_CONTRADICTION |
Shot properties contradict each other | risky | never fired |
CAMERA_TAXONOMY_DETAILS_CONFLICT |
Camera property contradicts its own details | risky | never fired |
FRAME_NOT_STATIC |
Frame description contains motion, not a single moment | minor | never fired |
DIALOGUE_ATTRIBUTION_AMBIGUOUS |
Dialogue speaker is unclear for this shot | minor | never fired |
whatever the model invents — LOCATION_CONTRADICTION, dialogue_speaker_unclear, eye_contact_conflict, car_position_contradiction, extra_boarding_implication_in_camera_movement_details, … |
Shot properties contradict each other (generic fallback) | as reported by the model | 14 of 14 text conflicts |
Consequences for the frontend and for analytics:
- Every text conflict currently shows the same generic one-liner, whatever it's actually about. The specific problem is only in
description. If the UI shows one-liners collapsed by default, text conflicts read as identical rows. - Text-conflict codes are not countable. They're free prose, so per-code dismissal analytics won't work on them until Stage 1 gets a code enum. Backend fix, not a frontend one — tracked, not done.
- Stage 3's codes are enum-locked (
stageSchemas.ts:230-243), so everything in the tables above this one is reliable.
Informational — never shown, analytics only
| Code | Meaning | Severity | Observed |
|---|---|---|---|
DETAIL_DELEGATED_TO_REFERENCE |
A detail isn't in the sketch because the reference image carries it (that's fine) | info | Stage 3 can emit it; never observed in 31 shots |
SKETCH_CONNECTION_INFERRED |
The system inferred which moment the sketch shows | info | defined but nothing raises it — reserved |
System
| Code | One-liner shown to the user | Severity |
|---|---|---|
VALIDATION_INFRASTRUCTURE_FAILED |
Input check could not complete — try again | blocker (deterministic) |
severity — how bad it is¶
| Value | Meaning | Surfaced in UI as |
|---|---|---|
blocker |
The image will clearly be wrong; generation is gated | Conflict row in the banner; Generate button disabled; no "Not a conflict" button (in the design: the red banner tier) |
risky |
Likely wrong interpretation | Conflict row in the yellow banner, dismissible |
minor |
Small deviation, probably fine | Conflict row in the yellow banner, dismissible |
info |
FYI only (e.g. "detail delegated to reference") | Not shown at all — logged for analytics only |
shortMessage — the one-liner¶
| Values | One fixed sentence per catalogued code, e.g. SKETCH_CAMERA_MISMATCH → "Shot size in sketch doesn't match in Shot size property". Never model-written — the same code always reads identically. Uncatalogued codes (all Stage-1 text conflicts today) get the generic "Shot properties contradict each other" |
| Surfaced in UI as | The banner row text ("5 conflicts found" list) and the amber text on the affected input card |
description — what the system found¶
| Values | Free prose written by the model for THIS shot, e.g. "The sketch is framed wider than the described extreme close-up…" |
| Surfaced in UI as | "What the system found" in the expanded view (today: hover tooltip on the row; design: the expandable panel) |
suggestion — the proposed fix, in words¶
| Values | Free prose, e.g. "Tighten the crop so the phone is the dominant subject and remove the clock/lamp from frame." Absent when no safe fix exists |
| Surfaced in UI as | "Proposed fix" in the expanded view |
proposedFix — the fix as a machine-applicable patch¶
| Values | { path, oldValue, newValue, reason, confidence }. path is always one of the five text fields Stage 1 can rewrite — description, actingInstructions, initialFrameInstructions, middleFrameInstructions, finalFrameInstructions. It is never an enum field like shotSize, and only Stage-1 text conflicts ever carry one (findFixForFields, preGenValidation.service.ts:346). newValue is a full rewritten paragraph, not a token — 10 of 118 conflicts had one in the measured scenes |
| Surfaced in UI as | Nothing yet (V0). Stored so a future one-click "Fix with AIDA" button can apply it without re-running anything. Because newValue is a long paragraph, a one-click apply will need a diff view, not an inline chip |
dismissible — can the user wave it away?¶
| Value | Surfaced in UI as |
|---|---|
true (risky/minor) |
The "Not a conflict" button appears on the row |
false (blockers) |
No button — the only way forward is "Go back and resolve" |
fieldPaths — which inputs are involved¶
| Values | List of field/input names. Two different naming styles, depending on the stage: Stage 1 and the deterministic checks use real camelCase field names (["sketch"], ["dialogue", "actingInstructions"]); Stage 3 uses uppercase section labels from its own prompt, not field names — ["FRAME DESCRIPTION", "CAMERA PROPERTIES", "STAGE 2 SKETCH OBSERVATION"]. STAGE 2 SKETCH OBSERVATION in particular is an internal pipeline artefact and maps to no user-visible input at all |
| Surfaced in UI as | Amber border + inline conflict text on the matching input card (Sketch card, Location card, the Dialogue row…) — and where "Go back and resolve" should land the user. Match case-insensitively, map the section labels yourself, and treat anything unrecognised as "general" rather than dropping the conflict |
confidence — how sure the checker is¶
| Values | 0.0–1.0. Mostly hardcoded, not a real model confidence: 0.9 for Stage-1 conflicts, 0.85 for all Stage-3 conflicts, 1 for the deterministic ones (preGenValidation.service.ts). Only SKETCH_MATCHES_NO_FRAME and SKETCH_COMPOSITE_MOMENTS derive it from a score |
| Surfaced in UI as | Not shown in V0 — and it should stay that way until it means something |
5. Also in the output (beyond conflicts)¶
frameCandidates— the start/middle/end frame descriptions Stage 1 derived from the shot's text. Not the creator's own fields — see the box in §3.2 before you render this.sketchMomentScores— how well the sketch matched each moment, e.g. a real measured shot came back start 0.20 / middle 0.14 / end 0.05, meaning the sketch matched nothing well; anything under 0.4 also raisesSKETCH_MATCHES_NO_FRAME.sketchAlignmentConfidence— Stage 2's own confidence in the moment it picked (0.45 on that same shot).resolvedInputs— the exact sketch + reference images the checker looked at (what the Visual references cards render).timings— how long each stage took. Empty array on a cached verdict — the reliable way to tell a cache hit from a fresh run.traces— the full stage-by-stage LLM output. Stripped by the controller before the response is sent (workbench.controller.ts:306), so the frontend never sees it. It's in the local analysis JSON only.
6. Known behavior to be transparent about¶
Measured over 31 shots across 2 real scenes (118 conflicts). None of this is speculation.
Detection is stable, attribution is not. A broken shot gets flagged every run. The specific dimension blamed can vary between runs — one run says "camera mismatch", another says "pose mismatch" for the same underlying problem. Doesn't affect whether the user is warned; does mean per-code counts drift.
Severity for the same code is not stable across shots. SKETCH_POSE_ACTION_MISMATCH came back blocker 6× / risky 10× / minor 3×. The Stage-3 prompt gives the model a single line of guidance on how to pick, so this spread is expected until the rules are tightened. Practical consequence: whether a shot is blocked or merely review_required can hinge on one model judgement call.
Stage-1 conflict codes are unusable as identifiers. No enum in the schema, 14 invented codes in 14 findings, all rendered with the same generic one-liner (§4).
Every measured shot was blocked. 31/31 — but SKETCH_ANALYSIS_MISSING fires on all 31 because sketch analysis had never been run on either scene, and it alone forces blocked. Discounting it: 2.81 conflicts per shot, 1.06 blockers per shot, 14 of 31 shots blocked and 3 fully clean. Read any "100% blocked" number with that in mind.
Dialogue barely contributes. 5 of 118 conflicts involved dialogue — 2 risky, 3 minor, 0 blockers.
Latency: ~100–110 s average on a fresh run, 194 s worst case measured. Cached verdicts are instant.
Unmeasured: the false-positive rate. Every conflict read in detail was defensible, but "defensible to the person who built it" is not "users agree." Per-code dismissal analytics is the instrument, and it only produces data once real users click.