{"slug":"comic-panel-prompt-builder","title":"comic-panel-prompt-builder","summary":"Phase-1 step of comic-author — the DETERMINISTIC compiler (搬运工原則) that turns ONE gate-approved panel_spec + its status:locked blueprint into the EXACT fixed-section bake string for the spiral engine via the shipped scripts/build_prompt.py (+ the canonical scripts/_validate.py vet","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-06T17:21:26.526244Z","repo":{"url":"https://github.com/wanshuiyin/ARIS-Movie-Director","stars":62,"forks":4,"license":"MIT","updatedAt":"2026-09-09T05:40:53Z"},"bodyHtml":"<hr>\n<h2>name: comic-panel-prompt-builder\ndescription: \"Phase-1 step of comic-author — the DETERMINISTIC compiler (搬运工原則) that turns ONE gate-approved panel_spec + its status:locked blueprint into the EXACT fixed-section bake string for the spiral engine via the shipped scripts/build_prompt.py (+ the canonical scripts/_validate.py vetoes). No Codex call in the happy path; it only emits, the engine bakes. Composes style_prefix + condition_string, injects the active failure_mode repair note on retry, and hard-REJECTS banned vocab (camera/lens/lighting/quality padding). Routes html_bubbles[] to the JSON viewer overlay, NEVER into the bake string (no baked bubbles unless the panel is text_mode:baked). Refuses unless the upstream blueprint is status==locked (the cross-layer gate's hand-off token) and every ref is a REAL locked asset path. Use when a panel_spec is locked and you need its prompt_bundle, just before comic-director's panel_gate.\"\nargument-hint: [panel_id | panel_spec.json] [--retry --reason \"</h2>\n<h1>comic-panel-prompt-builder — the Deterministic Bake Compiler (搬运工原則)</h1>\n<p>The <strong>last authoring step before a pixel ever gets baked</strong>. Given ONE upstream-approved <code>panel_spec</code> and its\n<code>status: locked</code> <code>blueprint</code>, this skill COMPILES — it does not brainstorm — the <em>exact</em> message\n<a href=\"../comic-director/SKILL.md\"><code>comic-director</code></a> (Phase 2/3) will hand to the agent <code>mcp__codex__codex</code> sidecar\nbake (Codex's native image tool), plus the locked identity-ref list. It is the comic twin of the framework's\n<code>shot-prompt-builder</code>: a pure Layer-3 transform\nwith <strong>no Codex call in the happy path</strong> — it only emits a <code>prompt_bundle</code>; the spiral engine submits it.\nEverything the generation backend ever sees passes through here, so this is where the <strong>搬运工原則</strong> is\nenforced at emit time: the bake backend (Codex's native image-generation tool; the underlying image model is\nnot pinned or attested by this repo) owns optics; <code>ART_BIBLE.md</code> owns style; <strong>no camera /\nlens / lighting / \"8K hyperrealistic cinematic\" padding leaks into the bake</strong>, and <strong>no bubble text gets\nbaked</strong> unless the panel is explicitly <code>text_mode: \"baked\"</code>. The author (storyboard/blueprint layers) decides\n<em>what</em>; this skill decides <em>nothing</em> — it transcribes a locked spec into a backend-legal string and FAILS\n(it never papers over) when the spec is malformed.</p>\n<pre><code>  panel_spec (status:locked) ─▶ ① RESOLVE  — load panel_spec + its blueprint + ART_BIBLE style_prefix\n        + blueprint (status:locked)    enforce 4 prerequisites (node_type, blueprint status:locked, REAL refs, style_prefix resolves)\n                              ▼\n                        ② COMPOSE  — fixed-section condition_string (order is load-bearing, do NOT refactor)\n                              ▼   style_prefix + [BAKED DIALOGUE only if text_mode==baked] + SCENE + FIXED ELEMENTS\n                              ▼   + ALLOWED CHANGE vs prior panel + FORBIDDEN + SCREEN-TEXT WHITELIST(expected_literals)\n                              ▼\n                        ②·5 INJECT (retry only) — active failure_mode positive-invariants to the FRONT\n                              ▼\n                        ③ VALIDATE  — banned-vocab + length + no-baked-bubbles + real-refs on the COMPOSED string\n                              ▼          (the hard gate — ANY hit = fail-closed, push the fix UPSTREAM)\n                              ▼\n                        ④ EMIT  — write the prompt_bundle node; route html_bubbles[] to the JSON viewer ONLY\n                              ▼\n                   pass? ─ no ─▶ exit 4 (banned/length) | exit 7 (failure_mode trigger still fires) | exit 3 (prereq)\n                              │ yes\n                              ▼\n                        ⑤ stdout one-line JSON {prompt_bundle_id, panel_id, source_blueprint_id}\n</code></pre>\n<h2>Role</h2>\n<p>You are a <strong>fail-closed transcriber</strong>, not an author. You take a <code>panel_spec</code> that an upstream layer already\ngate-approved and its <code>blueprint</code> that the cross-layer gate already flipped to <code>status: locked</code>, and you\nproduce the single exact <code>composed_prompt</code> (style_prefix + a fixed-section condition string) + the locked\nidentity-ref paths that the spiral engine will bake — together with the deterministic proof (a clean\n<code>banned_vocab_scan</code>) that the message is backend-legal. You <strong>never</strong> invent a scene element, a number, a cast\nmember, or a style adjective; you <strong>never</strong> silently fix a malformed spec (you FAIL and point upstream); and\nyou <strong>never</strong> call Codex on the happy path (a Codex call only ever appears as a reported upstream bug). The\ncross-model <em>judgement</em> of the baked pixels is not yours — that lives downstream in <code>comic-director</code>'s\n<code>panel_gate</code>; keeping you deterministic is what stops the executor from self-acquitting.</p>\n<h2>Node — what this skill reads / writes (<code>schemas/node_schema.json</code>)</h2>\n<ul>\n<li><strong>Reads <code>panel_spec</code></strong> (node prefix <code>panel:</code>) — payload fields used: <code>source_storyboard_id</code>, <code>page_id</code>,\n<code>panel_id</code>, <code>sequence_index</code>, <code>page_type</code>, <code>world</code>, <code>asset_ids[]</code>, <code>text_mode</code>, <code>expected_literals[]</code>,\n<code>content_blueprint</code>, <code>bubbles[]</code>, <code>side_narration</code>, <code>motifs</code>. The <code>panel_spec</code> must be its layer's\ngate-approved output (<code>status: locked</code>), not a draft.</li>\n<li><strong>Reads the upstream <code>blueprint</code></strong> (node prefix <code>blueprint:</code>, found via the panel's <code>content_blueprint</code>) —\npayload fields used (the schema's 9 <code>blueprint</code> required fields, verbatim from <code>validate_wiki.py</code>\nPAYLOAD_REQUIRED): <code>source_panel_id</code>, <code>content_svg</code>, <code>expected_literals[]</code>, <code>safe_zones[]</code>,\n<strong><code>html_bubbles[]</code></strong>, <code>crop</code>, <code>negative_space_policy</code>, <code>generator_script</code>, <code>file_sha256</code>. <strong>Refuse unless\nthe blueprint is <code>status: locked</code></strong> — that is the schema signal that the cross-layer gate signed off (the\ngate's ⑥ FLIP writes <code>status → locked</code> on advance; <code>comic-blueprint-author</code> emits blueprints at\n<code>status: locked</code>). The <code>blueprint</code> node has <strong>NO <code>review_status</code> field</strong> (only the <code>asset</code> node does, and\n<code>\"approved\"</code> is not even a legal <code>status</code> enum value) — never gate on <code>review_status</code> here. <code>html_bubbles[]</code>\nis the JSON viewer-overlay payload — it is <strong>routed to the viewer, NEVER concatenated into the bake string</strong>.</li>\n<li><strong>Writes one <code>prompt_bundle</code></strong> (node prefix <code>prompt:</code>, node_id a lowercase <code>[a-z0-9_-]+</code> slug of the\n<code>panel_id</code>) — payload required by the schema (the 6 <code>prompt_bundle</code> PAYLOAD_REQUIRED fields, verbatim):\n<code>source_panel_id</code>, <code>source_blueprint_id</code>, <code>style_prefix</code>, <code>composed_prompt</code>, <code>banned_vocab_scan</code>,\n<code>identity_ref_paths</code>. Status canon: author <code>draft</code>/<code>pending</code> → <code>under_review</code> → <strong><code>locked</code></strong> on a clean\nvalidate (or <code>rejected</code>). Write <strong>LEGAL</strong> edges (every <code>type</code> ∈ <code>validate_wiki.py</code> EDGE_TYPES):\n<code>derived_from</code> (prompt_bundle → panel_spec) and <code>uses_blueprint</code> (prompt_bundle → blueprint). On retry, the\nfailure_mode trace uses a <strong>legal <code>derived_from</code></strong> edge (prompt_bundle → failure_mode, with\n<code>evidence.consulted_failure_mode</code> + the <code>--reason</code> text — no match score; scoring is planned, not shipped);\nif a trigger still fires after repair, a legal <strong><code>failure_of</code></strong>\nedge (failure_mode → prompt_bundle — <code>failure_of</code> points failure_mode → target, matching the gate + the script,\ntrigger in <code>evidence</code>). There is <strong>NO <code>generated_from</code> /\n<code>consulted_failure_mode</code> / <code>violates_failure_mode</code> edge type</strong> in EDGE_TYPES — those would fail the release\ngate. Emit <strong>node IDs only</strong> to stdout — the orchestrator re-reads the node file from disk (no filename guessing).</li>\n</ul>\n<h2>Two engine contracts to compile to (fail-closed)</h2>\n<p>These are the same two contracts <code>comic-author</code> authors to — this skill is where they are <em>enforced at emit\ntime</em>, on the resolved spec, before any credit is spent:</p>\n<ol>\n<li><strong>Every panel needs a content_svg.</strong> On the wiki node this is <strong><code>blueprint.payload.content_svg</code></strong> (top-level\non the blueprint payload — NOT nested under any <code>condition.</code> key; the <code>condition.content_svg</code> prefix exists\nONLY in the runtime <code>comic.json</code> the engine reads). The blueprint's <code>content_svg</code> is the content authority +\nthe bake's layout reference. If it is <code>null</code> / missing / not on disk (or its <code>file_sha256</code> mismatches) →\n<strong>refuse</strong> (exit 3). A scene-only panel still has a <em>layout</em> blueprint SVG; there is no such thing as a\npanel with no <code>content_svg</code>.</li>\n<li><strong>A <code>baked</code> figure-panel must declare non-empty, ascii-tokenizable <code>expected_literals</code>.</strong> If\n<code>text_mode == \"baked\"</code> and the blueprint carries a figure but <code>expected_literals</code> is empty → <strong>refuse</strong>\n(exit 3). Those literals become the <strong>SCREEN-TEXT WHITELIST</strong> block (below) and are exactly what\n<code>panel_gate</code>'s blind token-diff later verifies. A scene panel with no audited numbers must be\n<code>text_mode: \"html\"</code> (dialogue is an HTML overlay, no baked-literal contract).</li>\n</ol>\n<h2>Procedure (numbered — an agent can execute this step by step)</h2>\n<p><strong>The shipped entrypoint (do NOT hand-roll the validator):</strong></p>\n<pre><code>python3 skills/comic-panel-prompt-builder/scripts/build_prompt.py &lt;project_dir&gt; &lt;panel_id|panel:slug&gt; \\\n    [--retry --reason \"&lt;text&gt;\"]\n</code></pre>\n<p><code>scripts/build_prompt.py</code> IS this procedure made executable — it reads the <code>panel_spec</code> + <code>blueprint</code> nodes\nfrom <code>&lt;project_dir&gt;/wiki/nodes/</code>, resolves the identity lock + refs, composes the fixed-section message, and\ncalls the <strong>single canonical</strong> <code>scripts/_validate.py</code> (which owns <code>MAX_MESSAGE_CHARS = 4000</code>, the ~80-pattern\n<code>BANNED_VOCAB</code>, <code>no_baked_bubbles</code>, <code>real_refs</code> — imported, never duplicated). It writes the trace files\n<code>wiki/prompt_build/&lt;panel_slug&gt;/{_resolved.json,_meta.json,_validation.json}</code> and, on a clean validate, the\n<code>prompt_bundle</code> node + its legal edges. <code>--retry</code> <strong>REQUIRES</strong> <code>--reason</code> (exit 2 otherwise) — a retry must\nname the active failure it is repairing. Run <code>python3 scripts/_validate.py --composed &lt;f&gt; --text-mode &lt;m&gt; --bubbles ... --refs ...</code> standalone for a quick re-check of an already-composed message.</p>\n<ol start=\"0\">\n<li><strong>Phase −1 · parse + scaffold.</strong> Parse <code>$2</code> (the <code>panel_id</code> or <code>panel:slug</code> node_id); make\n<code>wiki/prompt_build/&lt;panel_slug&gt;/</code>. <code>--retry</code> without <code>--reason</code> → <strong>exit 2</strong>. The emitted node_id is\n<code>prompt:&lt;lowercase-slug-of-panel_id&gt;</code> (the schema <code>node_id</code> pattern is <code>[a-z0-9_-]+</code>; the display\n<code>panel_id</code> may carry case). Never guess output filenames.</li>\n<li><strong>Phase 0 · resolve + prerequisites (refuse to proceed unless ALL hold).</strong> Load into one\n<code>_resolved.json</code>: the <code>panel_spec</code> node; its <code>blueprint</code> node (via <code>panel_spec.payload.content_blueprint</code>);\nand the <code>style_prefix</code> from <code>ART_BIBLE.md</code> (the world-keyed <code>STYLE_PREFIX[&lt;world&gt;]:</code> line — §0 register +\n§0.5 two-world palette). Enforce:\n<ul>\n<li>(a) <code>node_type == \"panel_spec\"</code> and <code>status == \"locked\"</code> — else <strong>exit 2</strong> (input missing/malformed).</li>\n<li>(b) the upstream <strong><code>blueprint.status == \"locked\"</code></strong> — the schema signal that the cross-layer gate signed\noff (the gate's ⑥ FLIP writes <code>status → locked</code>). The <code>blueprint</code> node has <strong>no <code>review_status</code> field</strong>;\ngating on <code>review_status == \"approved\"</code> is unreachable (not even a legal <code>status</code> value) → never do it.\nOptionally also assert the legal <code>decision</code> node / <code>decides</code> edge for this blueprint exists as gate proof.\nElse <strong>exit 3</strong>.</li>\n<li>(c) <strong>every ref is a REAL locked asset path</strong>, not a placeholder: <code>blueprint.payload.content_svg</code> exists on\ndisk (and <code>file_sha256</code> matches), and each identity ref — resolved by following\n<code>panel_spec.payload.asset_ids[]</code> to its identity-sheet <code>asset</code> node (the asset must be <code>status: locked</code>,\nand publishes a real <code>.png</code> via its <code>ref_requirements</code>) — is a real <code>.png</code> (NOT <code>pending:*</code>, NOT <code>null</code>\nwhere one is required). A <code>null</code> ref is allowed ONLY as the documented \"use project canonical\" fallback;\nan unresolved <code>pending:*</code> → <strong>exit 3</strong>.</li>\n<li>(d) the <code>style_prefix</code> resolves and is sane: ≤ 200 chars (hard), ≤ 32 words (warn). Else <strong>exit 3</strong>.\nAlso resolve asset <strong>names</strong> (scene/prop/identity <code>asset_ids[]</code> → display names) for the whitelist/reviewer\ncontext only — names go in the whitelist, raw IDs never go in the prompt.</li>\n</ul>\n</li>\n<li><strong>Phase 1 · compose the condition string DETERMINISTICALLY in this EXACT load-bearing layout</strong> (order is\nload-bearing — it empirically drives baked-text fidelity; do NOT refactor headings or order without an A/B\ntest). Front = strongest conditioning weight.\n<ul>\n<li><strong>STYLE PREFIX</strong> — from <code>ART_BIBLE.md</code>: tone + two-world palette + texture, world-keyed by\n<code>panel_spec.world</code> (e.g. <code>warm</code> / <code>seam</code> / <code>dark-cyber</code> / <code>starfield</code>). <strong>NO camera/lens/lighting/quality\nvocab.</strong></li>\n<li><strong>[BAKED DIALOGUE]</strong> — <strong>only if <code>text_mode == \"baked\"</code></strong> lift the panel's <code>bubbles[]</code> text into the\nprompt (so the image model draws legible balloons). <strong>If <code>text_mode == \"html\"</code> (or <code>code</code>): SKIP this\nblock entirely</strong> — the bubbles are the viewer's job (see Phase 4). This is the single switch that decides\nbaked-vs-overlay text.</li>\n<li><strong>SCENE COMPOSITION NARRATIVE</strong> — the scene staging authority is the <strong><code>blueprint.payload.content_svg</code></strong>\nlayout (the content blueprint already lays out the scene) summarized with the panel's\n<code>panel_spec.payload.side_narration</code>. <strong>There is NO <code>condition.scene</code> / <code>condition.characters</code> /\n<code>chibi_action</code> on the panel_spec node</strong> — those keys live ONLY in the runtime <code>comic.json</code> (do not read\nthem off the node). The world's warm/cold contrast is <em>by design</em>, not drift.</li>\n<li><strong>\"FIXED ELEMENTS (must NOT change):\"</strong> — the identity lock, obtained by following\n<code>panel_spec.payload.asset_ids[]</code> to each identity-sheet <code>asset</code> node and reading its\n<strong><code>asset.payload.identity_lock</code></strong> (the canonical-cast hex/beard/silhouette lock — there is NO\n<code>identity_desc</code> field on the panel_spec node; the node-model identity authority is the asset's\n<code>identity_lock</code>) + the locked props the motif ledger pins for this panel.</li>\n<li><strong>\"ALLOWED CHANGE (vs the prior panel):\"</strong> — from the panel's <code>motifs</code> delta (the comic reinterpretation\nof \"ALLOWED MOTION\": what may legitimately differ from the previous panel).</li>\n<li><strong>\"FORBIDDEN (must NOT happen):\"</strong> — the FIXED baseline <code>[\"no new character entering frame\", \"no off-model drift\", \"no scene/world recolor\"]</code> + any panel-specific <code>must_not_add</code>.</li>\n<li><strong>\"SCREEN-TEXT WHITELIST (exact characters to preserve):\"</strong> — the <code>expected_literals[]</code> from the\nblueprint (e.g. <code>[\"REJECT\",\"37\",\"T-16:05\"]</code>) + any baked-in signage/label names. Even when HTML owns the\nbubbles, baked-in figure text (chips, stamps, code) still needs char-exact preservation — this whitelist\nis the upstream of <code>panel_gate</code>'s literal diff.\nWrite <code>_meta.json</code> (character/element counts) for the trace.</li>\n</ul>\n</li>\n<li><strong>Phase 1·5 · failure_mode positive-invariant injection (retry only — the spiral active-memory hook).</strong>\nThis is what the SHIPPED <code>build_prompt.py</code> does — mirror it exactly, do not hand-roll a richer version. On\n<code>--retry</code>, collect <strong>ALL ACTIVE</strong> <code>failure_mode</code> nodes (<code>payload.active == true</code>) <strong>project-wide</strong> — there\nis NO layer or panel scoping in the shipped code. Sort them by <code>payload.severity</code> DESC (default 3 —\n<strong>severity ONLY</strong>, no recency term), take the <strong>top 10</strong>, and for each mode whose\n<code>encoding_style == \"positive_invariant\"</code> inject its <code>repair_pattern</code> <strong>UNCONDITIONALLY</strong> (no semantic\nmatching against the composed string) — non-<code>positive_invariant</code> modes are skipped here (negatives like \"no\nmissing ears\" make diffusion fixate on the negated concept). The injected patterns form one\n<strong>\"POSITIVE INVARIANTS (must be present in every panel, highest priority):\"</strong> block at the <strong>FRONT</strong> of the\nmessage. Record the consult as a <strong>legal <code>derived_from</code></strong> edge (prompt_bundle → the consulted failure_mode)\nwith <code>evidence.consulted_failure_mode: true</code> + the <code>--reason</code> text (there is no <code>consulted_failure_mode</code>\nedge type). This is exactly the <code>--reason</code> a retry must name.\n<em>(ASPIRATIONAL — planned, NOT yet implemented; do not describe as current behavior: scoping by the schema's\n<strong><code>layer</code></strong> field (∈ {prompt_pattern, visual_transition, global} — the field is <code>layer</code>, there is no\n<code>target_layer</code> in the schema), <code>recency*severity</code> ranking (recency <code>= 1.0/age_days</code>), a\n<code>semantic_signature</code> weighted-Jaccard match (<code>0.35*assets + 0.25*cterms + 0.20*kw + 0.20*frags</code>, threshold\n0.55), and a match score on the edge. The shipped injector is the severity-top-10 unconditional inject\nabove.)</em></li>\n<li><strong>Phase 1·6 · trigger re-scan (retry only).</strong> Re-scan the FULL composed message against the\n<code>trigger_patterns</code> of EVERY active failure_mode (not just injected ones). In the shipped script, trigger\nmatching gates <strong>ONLY the exit-7 path</strong>: if a trigger of a <strong>non-<code>positive_invariant</code></strong> mode still matches,\nit persists the bundle as <code>status: \"rejected\"</code> FIRST (so the edge endpoint resolves), writes a <strong>legal\n<code>failure_of</code></strong> edge (failure_mode → prompt_bundle — <code>failure_of</code> points failure_mode → target, the trigger\nin <code>evidence</code>; there is no <code>violates_failure_mode</code> edge type) and <strong>exits 7</strong> (a regenerate signal to the\norchestrator). A matched <code>positive_invariant</code> mode never exit-7s and there is <strong>NO append-in-place repair\nbranch</strong> — its <code>repair_pattern</code> was already injected in Phase 1·5 iff it made the severity top-10.</li>\n<li><strong>Phase 2 · VALIDATE = the hard gate (runs on the FULL composed message; this is the rubric).</strong> Delegate to\n<code>scripts/_validate.py</code> (the single source): <code>passes_all = length_ok AND no_banned_vocab AND no_baked_bubbles AND real_refs_ok</code>. ANY failure → preserve <code>_validation.json</code>, set the panel's\n<code>review_gates.storyboard_json_gate = \"fail\"</code>, and <strong>exit 4</strong> — see the EXACT gate below. <strong>Do NOT silently\nadd quotes / fix the spec</strong> — a malformed spec FAILS validation so the fix is pushed back to the authoring\nlayer.</li>\n<li><strong>Phase 3 · emit.</strong> Write the <code>prompt_bundle</code> node (payload = the schema's 6 required fields exactly:\n<code>{source_panel_id, source_blueprint_id, style_prefix, composed_prompt, banned_vocab_scan, identity_ref_paths}</code>); set its status <code>locked</code>. Write the <strong>LEGAL</strong> edges (every <code>type</code> ∈ EDGE_TYPES):\n<code>derived_from</code> (prompt_bundle → panel_spec) and <code>uses_blueprint</code> (prompt_bundle → blueprint) — <strong>NOT\n<code>generated_from</code></strong> (illegal, fails the release gate). <strong>Route <code>html_bubbles[]</code> to the JSON viewer overlay\nonly</strong> — they are NOT in <code>composed_prompt</code>. Append a timeline entry to <code>wiki/log.md</code>.</li>\n<li><strong>Phase 4 · stdout.</strong> Emit one-line JSON <code>{\"prompt_bundle_id\":\"prompt:...\",\"panel_id\":\"...\", \"source_blueprint_id\":\"blueprint:...\"}</code> — the canonical orchestrator input. Set\n<code>panel_spec.review_gates.storyboard_json_gate = \"pass\"</code>.</li>\n</ol>\n<h2>The EXACT gate (deterministic fail-closed — dimensions + thresholds + veto)</h2>\n<p>This is a <strong>transform</strong>, so there is <strong>NO cross-model / scored gate here</strong> (that lives downstream in\n<code>comic-director</code>'s <code>panel_gate</code>). The gate is a deterministic contract/banned-vocab validator on the\n<strong>COMPOSED message</strong> — ported verbatim from <code>shot-prompt-builder</code> / <code>segment-intent-builder</code>. Each dimension\nis binary; ANY hit is a hard fail (no single-vote averaging — this is detect-only).</p>\n<ul>\n<li><strong><code>length_ok</code></strong> — <code>len(composed_prompt) ≤ MAX_MESSAGE_CHARS = 4000</code> (~600 words, the empirical backend\ntolerance). Over cap → fail.</li>\n<li><strong><code>no_banned_vocab</code></strong> — the <strong>搬运工 v2</strong> validator: ~80 case-insensitive, word-boundary patterns the backend's\nown agents own. <strong>Veto = any single hit fails the whole message.</strong> The list below is the human-readable map;\nthe <strong>single source of truth is <code>scripts/_validate.py</code>'s <code>BANNED_VOCAB</code></strong> (imported by <code>build_prompt.py</code>, so\nthe list never forks). Categories:\n<ul>\n<li><em>Quality padding:</em> <code>\\b8K\\b</code>, <code>\\b4K\\b</code>, <code>hyperrealistic</code>, <code>photorealistic</code>, <code>ultra-realistic</code>, <code>cinematic</code>,\n<code>professional</code>, <code>award-winning</code>, <code>masterpiece</code>, <code>high/best quality</code>, <code>highly detailed</code>, <code>intricate details</code>,\n<code>trending on artstation</code>.</li>\n<li><em>Camera:</em> <code>the camera</code>, camera <code>moves/pans/tilts/zooms/tracks/pushes/pulls/cranes</code>,\n<code>wide/close-up/medium/long/establishing/POV shot</code>, <code>over-the-shoulder</code>, <code>dolly</code>, <code>tilt</code>, <code>pan</code>, <code>crane</code>,\n<code>drone</code>, <code>aerial</code>, <code>orbital</code>, <code>whip-pan</code>, <code>tracking shot</code>, <code>push-in</code>, <code>pull out</code>, <code>zoom</code>, <code>rack focus</code>,\n<code>focus pull</code>, <code>jib</code>, <code>steadicam</code>, <code>handheld</code>.</li>\n<li><em>Lens:</em> <code>\\d{2,3}mm</code>, <code>f/\\d</code>, <code>bokeh</code>, <code>depth of field</code>, <code>shallow DOF</code>, <code>DOF</code>, <code>lens</code>, <code>wide-angle</code>,\n<code>telephoto</code>, <code>anamorphic</code>, <code>lens flare</code>.</li>\n<li><em>Lighting:</em> bare <code>lighting</code>, <code>rim/key/fill light</code>, <code>three-point lighting</code>, <code>golden hour</code>, <code>blue hour</code>,\n<code>studio lighting</code>, <code>soft/hard light</code>, <code>low-key</code>, <code>volumetric</code>, <code>god rays</code>, <code>chiaroscuro</code>.</li>\n<li><em>Engine/brand:</em> <code>Unreal Engine</code>, <code>Octane</code>, <code>V-Ray</code>, <code>Blender</code>, <code>Midjourney</code>, <code>Stable Diffusion</code>, <code>Sora</code>,\n<code>Runway</code>, <code>Pika</code>.</li>\n<li><em>Clichés:</em> <code>epic</code>, <code>breathtaking</code>, <code>stunning</code>, <code>gorgeous</code>, <code>mesmerizing</code>, <code>otherworldly</code>,\n<code>surreal masterpiece</code>.</li>\n</ul>\n</li>\n<li><strong><code>no_baked_bubbles</code> (comic-specific veto)</strong> — if <code>text_mode != \"baked\"</code> and any of the panel's\n<code>html_bubbles[]</code> / <code>bubbles[]</code> quoted dialogue text appears in <code>composed_prompt</code> → <strong>fail</strong>. HTML owns the\nbubbles; baking them is the exact drift this gate exists to catch. (When <code>text_mode == \"baked\"</code> the dialogue\nis allowed in the BAKED DIALOGUE block by design — this veto fires only for html/code panels.)</li>\n<li><strong><code>real_refs_ok</code> (single-source ref veto)</strong> — <code>identity_ref_paths[]</code> must be REAL locked <code>.png</code> paths, and\nthe <strong>ref set is exact</strong>: no silent concatenation of extra identity IDs, no <code>pending:*</code>, no raw <code>asset_id</code>\nstrings smuggled into the prose. (Single-source discipline — the ref-count linchpin; concatenating refs\nsilently is forbidden.)</li>\n</ul>\n<p><strong>Exit-code contract (= the verdict surface; <code>scripts/build_prompt.py</code> returns these EXACT numeric codes):</strong>\n<code>0</code> validated + emitted + gate pass · <code>2</code> input missing / malformed CLI (<code>--retry</code> w/o <code>--reason</code>; panel_spec\nnot <code>node_type panel_spec</code> or not <code>status: locked</code>) · <code>3</code> upstream prereq unmet (<strong>blueprint not\n<code>status: locked</code></strong> / ref not real / <code>content_svg</code> missing / <code>expected_literals</code> missing on a baked\nfigure-panel / style_prefix bad / failure_mode query failed) · <code>4</code> banned-vocab | length | baked-bubble | ref\nfail (gate → fail, <code>_validation.json</code> preserved) · <code>7</code> Phase 1·6 regenerate (a <strong>non-<code>positive_invariant</code></strong>\nmode's <code>trigger_pattern</code> still matches after repair; the bundle is persisted <code>status: \"rejected\"</code> and a legal\n<strong><code>failure_of</code></strong> edge written).</p>\n<h2>Worked example</h2>\n<p>The reference movie's per-panel <code>condition</code> blocks —\n<strong><a href=\"../../examples/comic_m3_audit/comic.json\"><code>../../examples/comic_m3_audit/comic.json</code></a></strong> — are the\n<strong>compiled-artifact (comic.json runtime) OUTPUT shape this skill produces</strong> (the spiral engine inlines exactly\nthese <code>condition.*</code> keys). They are NOT the node fields this skill <em>reads</em> — those are the flat <code>panel_spec</code> /\n<code>blueprint</code> / <code>asset</code> node payloads (the <code>condition.</code> prefix exists only in comic.json). Copy these output\npatterns, but source each from its node field as noted:</p>\n<ul>\n<li><strong>A baked figure-panel — <code>S11</code> (the REJECT verdict).</strong> Compiled output (comic.json):\n<code>condition.content_svg: \"assets/reject_verdict_round1_v1.svg\"</code>, <code>text_mode: \"baked\"</code>,\n<code>condition.expected_literals: [\"REJECT\",\"37\",\"T-16:05\"]</code>, <code>world: \"dark-cyber\"</code>. <strong>Node sources:</strong>\n<code>content_svg</code> ← <code>blueprint.payload.content_svg</code>; <code>expected_literals</code> ← <code>blueprint.payload.expected_literals</code>;\nthe identity lock ← the identity-sheet <code>asset.payload.identity_lock</code> reached via <code>panel_spec.payload. asset_ids[]</code>. The compiler emits: STYLE PREFIX (dark-cyber palette, no camera vocab) → <strong>BAKED DIALOGUE</strong>\n(because <code>text_mode==baked</code>: from <code>panel_spec.payload.bubbles[]</code>, reviewer \"Rejected. Fix it, then come\nback.\" / executor \"...Copy that.\") → SCENE COMPOSITION NARRATIVE (from the <code>blueprint.content_svg</code> layout +\n<code>panel_spec.side_narration</code>: the verdict chamber + the REVIEW·ROUND 1 card) → FIXED ELEMENTS (from\n<code>asset.identity_lock</code>: \"blue executor brown hair NO beard / green reviewer dark hair beard\") → FORBIDDEN (no\nnew character, no off-model drift) → <strong>SCREEN-TEXT WHITELIST <code>REJECT</code> · <code>37</code> · <code>T-16:05</code></strong>. Those three\nliterals are exactly what <code>panel_gate</code> blind-diffs — they MUST be present and char-exact, and this is why a\nbaked figure-panel that omits <code>expected_literals</code> is refused at Phase 1.</li>\n<li><strong>The 搬运工 line in the data.</strong> Note what is <em>absent</em> from every compiled <code>condition.scene</code>/<code>characters</code>\nstring (and from the node fields they were compiled from): no \"the camera pushes in\", no \"85mm\", no\n\"cinematic lighting\", no \"8K\". The author already speaks backend-legal; the validator is the second line of\ndefense that keeps it that way when a style adjective sneaks in.</li>\n<li><strong>An HTML-bubble panel — <code>S22</code> (the constellation endcard).</strong> <code>text_mode: \"html\"</code>, <code>bubbles: []</code>,\n<code>expected_literals: []</code>, plus <code>safe_zones</code> for the HTML tagline. Here the compiler <strong>SKIPS the BAKED\nDIALOGUE block</strong> and routes nothing-text into the prompt; the scene string even hard-asserts \"NO text, NO\nglyphs anywhere in the image\". For a panel like <code>S02</code> whose bubbles ARE drawn (<code>text_mode: \"baked\"</code>,\nbubbles researcher/executor/reviewer), those same bubbles would instead go in the BAKED DIALOGUE block — the\nidentical <code>bubbles[]</code> field is routed to the <em>prompt</em> or to the <em>viewer</em> solely by <code>text_mode</code>. That switch,\napplied per panel, is the whole \"no baked bubbles unless baked\" guard made concrete.</li>\n<li><strong>The single-ref discipline — <code>S01</code> / <code>S20</code> / <code>S21</code>.</strong> Each pins exactly its locked identity sheet\n(<code>trio_identity_sheet_v001.png</code> or <code>researcher_chibi_canonical_ref_v001.png</code>) as <code>identity_ref</code> — one exact\nref set per panel, never a silent concatenation. <code>identity_ref_paths[]</code> in the emitted <code>prompt_bundle</code>\nmirrors precisely that set.</li>\n</ul>\n<p>(The provenance of these panels — the debate→synthesize outline, the MOTIF STATE TABLE that pins each\nliteral, the user-approval gate — lives upstream in <code>comic-outline-creator</code> / <code>comic-storyboard-creator</code>;\nby the time a <code>panel_spec</code> reaches <em>this</em> skill it is locked, and this skill only transcribes it.)</p>\n<h2>Hard do / don't (earned lessons)</h2>\n<ul>\n<li><strong>DO</strong> compose the EXACT fixed-section string <code>style_prefix + condition_string</code> deterministically — and on\nretry inject the active <code>failure_mode</code> <code>repair_pattern</code> (positive_invariant ONLY) at the FRONT.</li>\n<li><strong>DO</strong> run the banned-vocab + length + no-baked-bubble + real-ref validator on the <strong>COMPOSED</strong> message\n(catches <code>ART_BIBLE</code>/style contamination, not just per-field text) — and FAIL closed, never paper over.</li>\n<li><strong>DO</strong> route <code>html_bubbles[]</code> to the JSON viewer overlay; the bake string carries dialogue <strong>only</strong> when\n<code>text_mode == \"baked\"</code>.</li>\n<li><strong>DON'T</strong> call Codex in the happy path — this is a pure transform; a Codex call is only ever a reported\nupstream bug, never a way to \"fix\" a malformed spec here.</li>\n<li><strong>DON'T</strong> invent or \"improve\" a scene element, number, cast member, or style adjective — if the spec is\nmalformed or a ref is <code>pending:*</code>, exit non-zero pointing upstream (creator bug vs blueprint-not-<code>locked</code>\nvs ART_BIBLE contamination), do NOT patch locally.</li>\n<li><strong>DON'T</strong> let camera / lens / lighting / \"8K hyperrealistic cinematic\" padding into the prompt, and <strong>DON'T</strong>\nconcatenate extra identity refs — the backend owns optics, <code>ART_BIBLE</code> owns style, and the ref set is exact.</li>\n<li><strong>DON'T</strong> emit on a blueprint that is not <code>status: locked</code> or a panel_spec that is not <code>status: locked</code> —\nrefuse (exit 3 / exit 2). And <strong>DON'T</strong> write an edge <code>type</code> outside <code>validate_wiki.py</code> EDGE_TYPES (no\n<code>generated_from</code> / <code>consulted_failure_mode</code> / <code>violates_failure_mode</code> — they fail the release gate).</li>\n</ul>\n<h2>Protocols (governance contracts this skill honors)</h2>\n<ul>\n<li><a href=\"../../protocols/artifact-integrity.md\"><code>artifact-integrity</code></a> — this compiler does NOT judge the panel it\nbuilds; it transcribes a locked spec and emits a deterministic banned-vocab proof. Numbers\n(<code>expected_literals</code>) are <em>carried through verbatim</em>, never originated; the pixels are judged downstream.</li>\n<li><a href=\"../../protocols/reviewer-independence.md\"><code>reviewer-independence</code></a> — there is no reviewer in this step (it's\na transform). The downstream judge (<code>comic-director</code>'s <code>panel_gate</code>) gets the baked image + the blueprint's\n<code>expected_literals</code>, never this compiler's interpretation.</li>\n<li><a href=\"../../protocols/acceptance-gate.md\"><code>acceptance-gate</code></a> — a clean validate here is a <em>transform pass</em>, not an\nacquittal: a deterministic same-model check is allowed to confirm \"the message is backend-legal\", but the\nquality/correctness verdict on the result is the cross-model <code>panel_gate</code>, never this skill.</li>\n<li><a href=\"../../protocols/reviewer-routing.md\"><code>reviewer-routing</code></a> — N/A on the happy path (no model call); if a\nblocked retry ever escalates to an external consult, Codex at <code>xhigh</code> (no model pin — the CLI follows the\nlocal codex config) / Gemini <code>auto-gemini-3</code>, never downgraded.</li>\n<li><a href=\"../../protocols/review-tracing.md\"><code>review-tracing</code></a> — every emit logs to <code>wiki/log.md</code>; every retry writes\na legal <strong><code>derived_from</code></strong> edge to the consulted <code>failure_mode</code> (consult flag + retry reason in <code>evidence</code>;\nno match score — Jaccard scoring is planned, not shipped), and a legal\n<strong><code>failure_of</code></strong> edge if a trigger still fires, so the spiral memory is auditable (both edge types are in\n<code>validate_wiki.py</code> EDGE_TYPES).</li>\n<li><a href=\"../../protocols/output-versioning.md\"><code>output-versioning</code></a> — the <code>prompt_bundle</code> records <code>file_sha256</code> of\nthe blueprint it compiled, so a re-bake traces to the exact <code>content_svg</code> version.</li>\n</ul>\n","files":[{"path":"scripts/build_prompt.py","sizeBytes":20697,"isText":true},{"path":"scripts/_validate.py","sizeBytes":6490,"isText":true},{"path":"SKILL.md","sizeBytes":29838,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-06T17:21:58.545453Z","sha256":"DD978D6604A29BFD7AAC84A948B0154023F9C2DDB9BC6AE087FDCBEBDF588351","sizeBytes":21810},"review":null,"source":{"repositoryUrl":"https://github.com/wanshuiyin/ARIS-Movie-Director","path":"skills/comic-panel-prompt-builder","license":"MIT","commit":"a1e7319f8a0af18511c300462e24b06091b4c927","subtreeSha":"E3AE8DAA87CB54BCD3B4EF7C10A5F9C5074B1D9FB865133A824597B374C652FC","lastSyncedAt":"2026-09-23T13:50:40.850809Z"},"reviewedAt":"2026-09-06T17:22:42.976509Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/wanshuiyin/ARIS-Movie-Director/tree/main/skills/comic-panel-prompt-builder"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wanshuiyin-aris-movie-director@llmmart"},{"target":"git","command":"git clone https://github.com/wanshuiyin/ARIS-Movie-Director.git"}]}