comic-asset-ref-generator — the Reference Producer (Phase 1 · S4)

SkillAI & models

Phase-1 (S4) of a comic movie — PRODUCE the canonical reusable references the whole spiral conditions on. Per asset it bakes ONE canonical 1:1 white-bg identity ref via the agent mcp__codex__codex sidecar bake (Codex native image_gen — conditioned, never hand-pasted) OR, for a deterministic motif (clock/chart/stamp/star-map), emits a single-source parametric SVG from a python generator (asset_lib.py). Hashes + base64-encodes the bake into the asset node's output_ref (all 6 fields or it's a schema violation), versions every ref _v{NNN} with a supersedes self-edge, and runs a single-source collision gate. GENERATES ONLY — it NEVER self-locks/approves (that is the cross-model asset-review-loop's job, a different model family). Identity is BRING-YOUR-OWN; the ARIS chibi duo is only the worked example.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the comic-asset-ref-generator — the Reference Producer (Phase 1 · S4) skill

What this skill tells your AI

The instructions your AI receives, as published by wanshuiyin/aris-movie-director in skills/comic-asset-ref-generator/SKILL.md and read by ahel’s review.

The missing Layer-2 of comic-author: take the storyboard's consolidated ASSET_REQUESTS and produce, for every declared asset, the one canonical artifact that every downstream panel bake will condition on — so the film never grows two visual dialects and every panel of the same character/prop/motif reads as the same thing. This is precisely the producer of comic.json's identity_refs (e.g. duo_canonical_ref_v001.png + per-character locks like executor.hoodie #1D4684 / reviewer.beard true) that the proven comic created by hand. It is the upstream sibling of comic-blueprint-author (which authors per-panel content-SVGs) and feeds comic-asset-review-loop and the comic-director spiral.

Two asset classes, two production routes — this is the load-bearing fork:

storyboard.consolidated_asset_requests
   ├── identity / scene / prop  ──▶  RASTER ref:  agent mcp__codex__codex sidecar bake, CONDITIONED on a labeled white-bg
   │      (a face, a hoodie,           condition + real identity refs → 1:1 (or 16:9 scene) PNG
   │       an empty room)              → output_ref{file_path,data_url,sha256,width,height,mime}  (ALL 6)
   │
   └── deterministic motif      ──▶  SVG SOURCE:  ONE parametric builder in asset_lib.py
          (clock / chart / stamp        (ddl_chip / stamp / mug / curve_panel / tokyo_chip / starmap …)
           / mug / star-map)            the "ref" IS the single-source SVG — NOT an image bake
                                        → records generator_script, owner_script, file_sha256

   both routes → review_status:"pending"  (NEVER "locked" here)  → collision gate → asset-review-loop

The battle lesson, landed as the fork above: a clock, a chart, a verdict stamp, a star-map is deterministic content — you do NOT bake it as a fuzzy image and hope the digits land; you build it once from a python generator (asset_lib.py) so all 18 instances of the DDL timeline, all 7 verdict stamps, both the labeled and the wordless star-map render from the same coordinates and never diverge. An identity (a face, a costume) is not deterministic — that you bake once via the sidecar bake (Codex's native image tool), conditioned on a labeled white-bg reference and the real identity refs, never a free prompt, never a hand-paste.

Constants

  • GENERATOR (raster route) = the agent's mcp__codex__codex sidecar bake: model: "gpt-5.5", config{ model_reasoning_effort: "xhigh", include_image_gen_tool: true }, sandbox: "workspace-write", plus approval-policy: "never" — an mcp-call argument, NOT a .bakereq.json field (the sidecar payload carries no such key). This exact shape is the empirical v3.5 lesson: without workspace-write + approval=never + include_image_gen_tool=true, Codex falls back to writing descriptive text (or an SVG renderer) instead of firing its native image tool. image_gen is incompatible with minimal effort; the shipped bake runs xhigh — the same gpt-5.5 + xhigh single compat default pinned in run_comic.get_bake_plan() (contract bakereq/v1, the digest the p0_proof cert binds to; a config-driven model/effort override is planned, not yet implemented). Honest scope: this Phase-1 raster ASSET bake is paid and runs PRE-P0 — the p0_proof cert gates the Phase-2/3 PANEL bakes only (no cert can exist before comic.json compiles, and assets must lock first); the gate on THIS spend is the cross-model comic-asset-review-loop + the single-source collision gate (P5), not P0. NB the Codex CLI reviewers elsewhere in the pipeline pin no model (they follow the local codex config — currently gpt-5.6-sol) at effort xhigh; only the bake payload pins gpt-5.5. The authoritative bake reply is the .bakestatus.json sidecar ({status, failure_kind, mcp_output, request_id} — P2r step 3) + the native PNG on disk; codex is never asked to emit a JSON-line self-report (that was a retired codex-exec-era contract).
  • SVG SOURCE (deterministic route) = a pure-python parametric builder living in gen/asset_lib.py, emitted by a thin gen/gen_core_assets.py writer. The palette is pinned to comic.json ui_tokens — never re-typed per asset. This route calls no image model and spends zero credits.
  • MAX_GEN_RETRIES = 2 (3 attempts total) — on ok:false OR a missing / zero-byte / invalid PNG (file <path> check). Append "[retry {n}: previous failure was: {reason}]" to the prompt each retry.
  • VERSIONING = monotonic 3-digit _v{NNN} (duo_canonical_ref_v001.png, ddl_widget_template_v1.svg). Version only grows; the prior artifact stays on disk for the audit cascade; a supersedes self-edge links the new version to the old (record prev_sha256:<hex> + prev_version:N in tags).
  • <refs> RESOLUTION + SUBDIR<refs> = <project>/ + movie.project.json dirs.assets (default assets/); the canonical identity target resolves via movie.project.json identity_ref with fallback assets/duo_canonical_ref_v001.png (the same resolution run_comic.py derive_paths() uses). The characters | scenes | props | text_panels subdir split is the RECOMMENDED layout for a new multi-asset project; the worked example predates it and keeps a FLAT layout — duo ref at assets/duo_canonical_ref_v001.png, extra cast under assets/identity/ (researcher_chibi_canonical_ref_v001.png, trio_identity_sheet_v001.png). SVG sources under gen/ + assets/. Placeholder key: <project> = the project dir (e.g. examples/comic_m3_audit); <refs> as above; the pickup verifier lives at the literal repo-relative path skills/method-figure/scripts/pickup_image.py.
  • NEVER LOCKreview_status is left "pending" (node statusunder_review). The executor that produces an asset is forbidden from approving it; locking belongs to the cross-model comic-asset-review-loop (a different model family). Setting review_status:"locked" here is a contract violation.
  • ONE RUNNER PER PROJECT — each raster bake writes its own explicit out_path via the .bakereq.json/.bakestatus.json sidecar seam, so there is no global-dir cross-pollination between concurrent bakes. There is no /tmp/aris_imagegen.lock in agent mode — the agent wrapper itself serializes the mcp__codex__codex calls. The real race surface is two runners on the same project colliding on the per-asset .bakereq.json/.bakestatus.json sidecars + the shared out_path, so keep one runner per project. (The global generated-images dir + newest-after-marker pickup that could cross-pollinate is a hazard of the legacy exec path ONLY, retired for real bakes.)

Input contract (3 modes)

This skill never invents an asset id. If the outline/storyboard did not declare it, the operator adds it at the outline layer (comic-outline-creator) — not here.

Pipeline position (the Phase-1 asset DAG — the documented contract): OUTLINE_DRAFT_VALID (the outline gate validates narrative + continuity + safety and that every referenced asset_id is DECLARED with a complete, generatable request — it does NOT require locked assets) → human outline approval → provisional storyboard (structural pass; may reference draft assets) → consolidated_asset_requeststhis skill (S4) + comic-asset-review-loop (S5) generate and LOCK the assets → OUTLINE_FINAL_LOCK (cheap re-check: the locked assets still match the approved outline) → storyboard FINAL asset-resolution validation → blueprints. The hard locked-asset barrier sits before blueprint authoring — not at the outline gate (the old single-stage contract deadlocked: an outline demanding locked assets that depend on a storyboard that depends on a locked outline).

  1. single asset_id matching ^asset:[a-z0-9_-]+$ — produce one asset, await its verdict.
  2. --batch-from-outline <outline_id> — read the human-approved outline's character_asset_ids[] + scene_asset_ids[] + prop_asset_ids[] ∪ the provisional storyboard's consolidated_asset_requests (structural pass — the storyboard is not asset-resolved yet at this point in the DAG), keep only review_status=="pending", process serially (raster bakes never overlap), fire the review loop once at the end.
  3. --regenerate <asset_id> --reason "<text>" — force a re-render even if approved/rejected; prefix the generation prompt with "[regen reason: ...]", bump _v{NNN}, write a supersedes self-edge.

What it reads / what it writes (wiki nodes & edges)

Per schemas/node_schema.json (node/comic/3.0):

  • READS storyboard_spec (payload.consolidated_asset_requests, payload.global_policies) and outline_spec (payload.character_asset_ids / scene_asset_ids / prop_asset_ids, payload.global_style_bible).
  • WRITES / MUTATES the asset node (node_id: ^asset:[a-z0-9_-]+$, node_type:"asset"). Required payload fields (schema oneOf → asset): asset_kind, name, visual_description, identity_lock, ref_requirements, review_status, version. This skill additionally writes the produced ref onto the node (see the two contracts below) and leaves review_status:"pending", node status:"under_review".
  • EDGES (schemas/edge_schema.json, src/dst/type): on a regenerate, emit a supersedes true self-edge with the bare node_id on BOTH endpoints (the node_id pattern ^(...|asset|...):[a-z0-9_-]+$ forbids @/{/}, and there is ONE asset:<slug> node per asset — no per-version node files — so any @v{N} endpoint would dangle and fail cli/validate_wiki.py lines 165-167 (endpoint resolution; the node_id-pattern check is lines 113-114)): {src: asset:<id>, dst: asset:<id>, type:"supersedes", evidence:"regen v{N}->v{N+1}: <reason>; prev_sha256:<hex>; prev_version:N"}. The cross-version lineage lives in the artifact filename _v{NNN} + prev_sha256/prev_version in tags (per output-versioning) — NOT in the edge endpoints. (Only if you truly need a versioned snapshot node, encode the version IN the slug — asset:<slug>_v002, which matches the pattern — AND actually write that second node file so both endpoints resolve.)
  • FAILURE — on retry exhaustion write a failure_mode node so the spiral routes around it, and exit non-zero. No asset_ref-layer example node ships in the repo (every examples/comic_m3_audit/wiki/nodes/fail_*.json is a downstream SHOT-level node — e.g. fail_s09_a01.json carries layer:"panel_visual" + affected_shot_ids:["S09"] — do NOT mirror those for this upstream gate). Write this literal skeleton instead:
    { "node_id": "fail:<asset-slug>_image_gen_unavailable", "node_type": "failure_mode", "status": "active",
      "title": "asset_ref bake failed — <asset-slug>", "created_at": "<real UTC now>",
      "tags": ["image_gen_unavailable"],
      "payload": { "layer": "asset_ref", "affected_shot_ids": [], "active": true,
                   "repair_pattern": "<tag-derived hint>" } }
    
    (root status:"active" is the runtime canon validate_wiki.py enforces for failure_mode; payload has all 3 PAYLOAD_REQUIRED fields — layer, affected_shot_ids (empty: this is the UPSTREAM asset gate, no shots yet), active.) Do not trigger the review loop on an empty file.

The two fail-closed engine contracts (honor these or the director refuses to run)

These are the same fail-closed invariants the spiral enforces — produce assets so they cannot violate them:

  1. Every panel needs a content_svg. Every deterministic motif a panel will use MUST be produced as a single-source SVG in asset_lib.py, so the panel's blueprint can name a real content_svg. If a recurring motif has no parametric source here, the downstream blueprint has nothing to bake-condition on and the director's render step fails closed. One parametric builder per recurring motif — no exceptions.
  2. A baked figure-panel needs expected_literals. Any motif carrying gated text (a chart with 0.71→0.66, a stamp reading REJECT, a DDL chip reading T-16:05) must render those literals deterministically so the panel can declare ascii-tokenizable expected_literals for the blind token-diff gate. Conversely, a zero-text asset (the wordless constellation) must contain zero glyphs — assert it at build time (assert "<text" not in svg). Producing a fuzzy raster of a chart breaks this contract; that is exactly why charts/clocks/stamps take the SVG route.

Procedure (numbered — an agent runs this top to bottom)

P0 · Resolve the target

Read the asset node. Assert node_type=="asset". Abort if review_status=="locked" ("immutable per asset gate") or review_status=="approved" unless --regenerate. Classify the asset by asset_kind into its production route: character | scene | prop (with a non-deterministic appearance) → raster route (P1r); a deterministic motif (clock/chart/stamp/mug/symbol/star-map, anything whose pixels are computable) → SVG-source route (P1s). When in doubt: if the asset has gated literals or must be byte-identical across instances, it is deterministic → SVG route.

P1r · RASTER route — build the bake prompt

From payload: asset_kind, name, visual_description, identity_lock.must_preserve[] (traits that MUST appear — e.g. "blue hoodie #1D4684", "beard", "silver hair"), identity_lock.must_avoid[] (→ a image-prompt "do not add" block — e.g. "celebrity likeness", "corporate branding", "voxel 3-D blocks"), ref_requirements.{aspect_ratio,background,pose,isolated_reference,data_url_required}.

  1. Run the STYLE-BIBLE / banned-vocab filter (see below) over visual_description; rewrite inline and record each substitution into the composed prompt.
  2. Compose the prompt (template):
    Render a single reference image for asset {id} ({kind}: {name}).
    Subject: {filtered visual_description}
    Identity lock — preserve exactly: {must_preserve[]}
    Identity lock — do not add: {must_avoid[]}
    Reference requirements: {aspect_ratio} canvas; pure {background} fully isolated, no shadow/halo;
      pose = {pose}; isolated reference (no scene around the subject).
    No labels / watermarks / text / captions / contact-sheet grids.
    Output a SINGLE coherent reference image — not a collage, not a variation grid.
    Treat composability (this becomes a reusable identity ref) as the primary goal.
    
    Pose defaults by kind: character = "three-quarter front view, neutral expression, upper-body or full-body framing"; scene = "establishing wide of the empty location, no characters, neutral daytime lighting"; prop = "isolated centred view, slight three-quarter angle"; logo_free_symbol = "centred symbol on white, no scene". Provide the project's real identity refs as read-only condition images when the asset must match an existing cast member (never invent a face).

P2r · RASTER route — bake via the .bakereq/.bakestatus sidecar (CONDITION it, never hand-paste)

This is the same fail-closed bake seam run_comic.py / run_spiral.py use (no marker, no /tmp lock, no newest-pickup — each bake writes its OWN explicit OUTPUT_PATH). OUTPUT_PATH is a deterministic project-relative path (<refs>/{subdir}/{name}_v{NNN}.png — in the worked example's flat layout that is assets/{name}_v{NNN}.png / assets/identity/{name}_v{NNN}.png); canvas guidance in the prompt = 1024x1024 (1:1 default) or 1280x720 (scene 16:9), background opaque/fully white, ONE image.

ONE agent, BOTH sidecar roles. Unlike run_comic.py (a Python orchestrator serviced by an external agent wrapper), this skill ships no runner — the SAME agent running this SKILL plays the orchestrator (emit + verify) and the wrapper (call mcp__codex__codex, write the status). The one-liners below are transitional until the shared cli/service_bake_requests.py broker lands (planned — it does not exist yet); they import the canonical primitives from skills/method-figure/scripts/pickup_image.py (the single source of truth) — never hand-assemble a .bakereq.json any other way. <ABS_OUT> = the absolute OUTPUT_PATH; <ABS_PROJECT> = the absolute project dir; run from the repo root.

  1. Emit the request (orchestrator role). First write the composed P1r prompt body to <ABS_OUT>.promptbody.txt, then emit. emit_bake_request mints the per-bake uuid4-hex request_id itself (stamps it into the payload and RETURNS it), pre-deletes any stale out_path + status so a prior bake can't be silently reused, and writes the sidecar atomically (.tmp → os.replace). Capture the printed id:
    python3 -c 'import sys, time; sys.path.insert(0, "skills/method-figure/scripts"); \
    from pickup_image import build_bake_prompt, emit_bake_request; \
    body = open("<ABS_OUT>.promptbody.txt").read(); \
    pt = build_bake_prompt(body, "<ABS_CONDITION_PNG>", "<ABS_IDENTITY_REF or empty>", "<ABS_OUT>"); \
    print(emit_bake_request("<ABS_OUT>", {"prompt_text": pt, "out_path": "<ABS_OUT>",
      "content_png": "<ABS_CONDITION_PNG>", "identity_ref": "<ABS_IDENTITY_REF or empty>",
      "model": "gpt-5.5",
      "config": {"model_reasoning_effort": "xhigh", "include_image_gen_tool": True},
      "sandbox": "workspace-write", "cwd": "<ABS_PROJECT>", "created_at": time.time(),
      "min_bytes": 500000, "aspect": 1.0}))'
    
    aspect = W/H as a float (1.0 for 1:1, 1.7778 for a 16:9 scene). build_bake_prompt prepends the canonical "use your native image generation tool" header and embeds the reference + output paths LITERALLY in prompt_text (ref #1 = the labeled white-bg condition, ref #2 = the identity ref — this skill's own doctrine: never a free prompt, so ref #1 always exists). Also note the epoch you passed as created_at.
  2. Bake (wrapper role). Call mcp__codex__codex with prompt_text verbatim as the prompt and model / config / sandbox / cwd exactly as in the .bakereq.json, plus approval-policy: "never" (an mcp-call argument — NOT a bakereq field); codex writes the native PNG to OUTPUT_PATH (the mcp__codex__codex schema has no -i image param — the paths inside prompt_text are the only transport). The config MUST carry both model_reasoning_effort:"xhigh" and include_image_gen_tool:true, else codex is never handed the native image tool and no bake fires.
  3. Write the status (wrapper role). Use the Write tool to create <ABS_OUT>.bakestatus.json = {status:"ok"|"fail", failure_kind:null|"throttle"|"other", mcp_output:"<the FULL raw mcp__codex__codex reply text, pasted verbatim>", request_id:"<the id step 1 printed, VERBATIM>"}. mcp_output is MANDATORY on BOTH ok and fail and must be the COMPLETE reply — never a summary or a truncation (the HARD-VETO scans it for import struct/zlib.compress/<svg/matplotlib/… traces of a hand-drawn fallback; an empty mcp_output leaves the veto inert and fail-closes). A missing/mismatched request_id is a stale/foreign bake and fail-closes.
  4. Verify the EXPLICIT OUTPUT_PATH (orchestrator role). Optionally re-read the status through the canonical poller (returns immediately in this single-agent flow; kept for parity with the split-role seam):
    python3 -c 'import sys, json; sys.path.insert(0, "skills/method-figure/scripts"); \
    from pickup_image import await_bake_status; print(json.dumps(await_bake_status("<ABS_OUT>", 600)))'
    
    Then — only on status:"ok" — run the pickup verifier (literal repo-relative path):
    python3 skills/method-figure/scripts/pickup_image.py --out-existing --out <ABS_OUT> \
      --min-bytes 500000 --aspect <W/H> --created-at <epoch from step 1> \
      --request-id <the id step 1 printed> --transcript <ABS_OUT>.bakestatus.json
    
    which checks PNG sig + IHDR dims + size strictly > min_bytes + mtime >= created_at + a non-empty mcp_output, fail-closes on a request_id mismatch, and HARD-VETOES struct/zlib/PIL/<svg/ matplotlib markers in the transcript (a clean PNG sig NEVER overrides a fallback marker). There is no newest-pickup — pickup verifies the one explicit OUTPUT_PATH this bake wrote.

RETRY per MAX_GEN_RETRIES. After exhaustion: keep the best valid PNG if any, else write the failure_mode node + exit non-zero (do not call the review loop on an empty file). Never patch a failed bake by hand-pasting the missing trait — re-condition and re-bake.

Grid guard. A 2×2 / contact-sheet / variation-grid output is a failed attempt → retry (the "single coherent image, not a grid" line is the primary mitigation; a PIL aspect-vs-canvas check + an optional codex "is this a grid?" sanity reply is the backstop).

P3r · RASTER route — hash, encode, update (atomic), honor the 6-field contract

  • SHA256 = shasum -a 256 <png>; (W,H) via PIL; DATA_URL = "data:image/png;base64,$(base64 -b 0 <png> || base64 -w 0 <png>)" (macOS -b / GNU -w, cross-platform). data_url is a v4 invariant — Layer-3 multi-ref composition reads it directly; a missing data_url silently breaks downstream.
  • Mutate the node output_ref with ALL SIX fields {file_path, data_url, sha256, width, height, mime}. A partial write is a schema violation — reject it. Write .tmp then mv (atomic; the wiki must validate even if interrupted). Leave review_status:"pending", node status:"under_review". Append a log.md line.

P1s–P3s · SVG-SOURCE route — one parametric builder per motif

  1. Add / reuse one builder in gen/asset_lib.py for the motif (ddl_chip, stamp, mug, curve_panel, tokyo_chip, mini_stamp_glyph, verdict_card, the star-map coord tables). Palette pinned to comic.json ui_tokens at the top of the file. Encode every cross-panel reuse contract into the docstring (e.g. S11 REJECT ↔ S16 ACCEPT instantiate the SAME verdict_card — mirror pair; the star-map JSON is the single coordinate truth for S16b labeled + S22 wordless).
  2. Emit the canonical sheet via gen/gen_core_assets.py — a thin w(name, content) writer that imports only from asset_lib. Its docstring maps each output filename → the storyboard ASSET_REQUEST #. Enforce single-source inline: if a specialized generator (e.g. gen_b06) takes ownership of a richer variant, delete the generic duplicate here (don't keep two).
  3. Coordinate-truth motifs (the star-map): write the JSON (wiki_starmap_nodes_v1.json) as the only truth source; both the labeled SVG (S16b) and the wordless SVG (S22) derive from it programmatically禁目测 / never re-layout by eye. The zero-text twin asserts assert "<text" not in svg at build time.
  4. Record on the node: generator_script, owner_script, file_sha256; paths project-relative. Leave review_status:"pending".

P4 · Hand off to the review loop (NEVER self-acquit)

Single mode: trigger comic-asset-review-loop, await its verdict (approve → loop locks it / regenerate → recursive --regenerate callback, bounded by MAX_REVIEW_ROUNDS=3). Batch mode: fire the review loop once at the end. This skill never writes review_status:"locked" — locking is the cross-model loop's exclusive right.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
61
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
comic-asset-ref-generator
Source
github.com/wanshuiyin/aris-movie-director