comic-json-compiler

SkillMedia

Phase-1 (S9, the FINAL comic-author step) — assemble the LOCKED storyboard + locked per-panel blueprints into ONE schema-valid `comic.json` (the comic-ir/1.0 contract boundary handed to comic-director / run_comic.py). Project the page_order + each panel's condition{} + render fields into `pages[]` + keyed `panels{}` per schemas/comic.schema.json; author ONLY authored fields and leave image_path/active_attempt_id/wiki_node_id EMPTY for the engine. THE step where page-count integrity is reconciled and the orphan-panel class of bug is caught (a panel defined in panels{} that no page references — ship that and the finale silently vanishes) — caught by an INLINE whole-comic reconcile this skill runs (page_refs vs panels{} keys vs the storyboard page_order), because the deterministic scripts are per-page and the schema leaves condition/content_svg OPTIONAL. Gated by comic-cross-layer-gate --gate compile (the DETERMINISTIC gate: both run_comic.py --dry-run AND cli/validate_wiki.py must exit 0). Use when the user says '编译 comic.json', 'compile the comic', 'assemble comic.json', '出 comic.json', or the storyboard + all blueprints are locked and you need the single IR to hand to Phase 2/3. Do NOT use to author the page order (that is comic-storyboard-creator) or to bake panels (that is comic-director).

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-json-compiler skill

What this skill tells your AI

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

The last node of the left third of Figure 1: take the locked storyboard_spec (the authoritative page order + the MOTIF STATE TABLE) and the locked per-panel blueprint nodes and compile them into the single comic.json (comic-ir/1.0) that is the contract boundary to comic-director (Phase 2/3). You don't hand-write this file panel-by-panel — that already happened upstream; this step is the deterministic projection + reconciliation that turns the authoring nodes into the IR and catches the integrity bugs that only show up once everything is assembled — chiefly the page-count / orphan-panel mismatch (the orphan-panel class). The two deterministic scripts the compile gate runs are per-page (run_comic.py needs --page/--panels) and the JSON schema leaves condition/content_svg OPTIONAL — so neither of them sees the whole pages[]/panels{} set and neither catches an orphan. That whole-comic reconciliation against the storyboard authority is THIS skill's own inline job (the ~7-line set diff below), run before the gate; the gate then certifies the per-page + wiki-conformance floor.

  locked storyboard_spec (page_order = AUTHORITY)            ┐
  locked motif_ledger    (per-panel ground-truth)           │
  locked panel_spec × N  (the 9-field specs)                 ├─▶ ① ASSEMBLE  pages[] + panels{}  per comic.schema.json
  locked blueprint × N   (content_svg + expected_literals)   │        (author ONLY authored fields; engine fields EMPTY)
  locked prompt_bundle × N (optional, identity refs)         ┘        ▼
                                                                ② RECONCILE page-count integrity  ── INLINE python (this skill; no phantom script)
                                                                   (orphans · dangling refs · page_count vs page_order · grid2x2==4 · recap legitimacy)
                                                                      ▼
                                                                ③ VALIDATE  jsonschema(comic.schema.json) — SHAPE only (it does NOT enforce content_svg/literals)
                                                                      ▼
                                                                ④ FAIL-CLOSED contracts  (every panel a content_svg · baked figure ⇒ expected_literals)  ← THIS skill asserts; schema won't
                                                                      ▼
                                                                ⑤ comic-cross-layer-gate decision:compile_<slug> --gate compile  (DETERMINISTIC: run_comic.py --dry-run per page AND cli/validate_wiki.py both exit 0)
                                                                      ▼
                                                            approve? ─ revise ─▶ fix the OFFENDING node upstream, recompile  (you don't invent content)
                                                                      │ approve
                                                                      ▼
                                                                ⑥ EMIT comic.json (output-versioning) + movie.project.json pointer-hub + asset-copy manifest

Constants

  • CONTRACT BOUNDARY = comic.json (comic-ir/1.0) + the assets it references. Everything downstream (comic-director, run_comic.py, the Phase-3 viewer build packages/viewer/build_comic.py) reads ONLY this file + its assets — so a bug that survives this step survives to production.
  • AUTHORITY = the storyboard's page_order. When the assembled comic.json page set disagrees with the locked storyboard_spec.page_order, the storyboard wins — the compiler conforms, it does NOT silently ship its own page count. (This is the converse of the storyboard gate's page-order veto.)
  • GATE = comic-cross-layer-gate decision:compile_<slug> --gate compile (the target is the real wiki decision node this skill writes — there is NO comic node type and comic.json is not a wiki node, so it can never be a gate target or an edge endpoint). Per comic-cross-layer-gate the compile gate is purely DETERMINISTIC: NO review:* fan-out, NO Codex adjudication (so unlike every other gate, the "create review nodes first" pre-step does NOT apply here). It PASSES (approve) iff BOTH on-disk scripts exit 0: python3 skills/comic-director/scripts/run_comic.py --project <dir> --page <P> --panels <ids> --dry-run (per page) AND python3 cli/validate_wiki.py <dir>; else revise with their stderr as the blocker list. (The whole-comic orphan/page-count catch is NOT in either script — it is this skill's inline step ② below, run before the gate.)
  • VERDICT enum (the compile gate's legal set, per the gate skill): approve / revise. On approve flip the compile decision to final and emit; on revise fix the offending upstream node and recompile (the compiler never patches content into comic.json by hand). A verdict is never a node status: the status the FLIP writes is locked on advance, under_review on needs-work, rejected on a terminal fail (schema-enum statuses only — revise/regenerate are verdicts, not statuses).
  • NO INVENTION — the compiler is a projection + reconciliation step. It must NOT invent a panel, a number, an expected_literal, a page, or a content_svg. A missing piece is a HALT with the offending node id, not a fabrication (artifact-integrity).
  • ENGINE FIELDS STAY EMPTYimage_path / active_attempt_id / wiki_node_id are written by the spiral engine on KEEP. Authoring them is a veto (you'd be claiming a bake that never ran).
  • ASCII node-id discipline — every wiki touch goes through the one node_id_to_filename helper (:_), per wiki-helper-resolution.

Input contract — what must be LOCKED before you compile

This is a compile step: it consumes locked upstream nodes, it does not author them.

  • One storyboard_spec (status: "locked") — its page_order (the authority), page_ids, panel_ids, global_policies (page-order authority, text-mode rules, mirror-lock policy), motif_ledger_id, consolidated_asset_requests.
  • The motif_ledger it points at — the per-panel ground-truth the gate uses to confirm condition prose agrees with each panel's continuity row.
  • One panel_spec per panel (status: "locked") — page_type, world, text_mode, expected_literals, content_blueprint, bubbles, side_narration, motifs, (cover/finale: safe_zones + endcard fields).
  • One blueprint per panel (status: "locked") — content_svg, expected_literals (verbatim), safe_zones, html_bubbles, crop, negative_space_policy, generator_script, file_sha256.
  • (optional) the prompt_bundle per panel — for identity_ref_paths (else fall back to the project canonical from identity_refs).
  • HALT if any of these is not locked (or its content_svg/output_ref file is missing) — name the offending node id. Convergence is the upstream gate's job, not yours.

Workflow (the inline reconcile + the two deterministic gate scripts — all runnable, no phantom tool)

① ASSEMBLE pages[] + panels{}

Project the locked nodes into the IR. Copy examples/comic_m3_audit/comic.json as the shape.

  1. Top levelschema_version: "comic-ir/1.0", comic_id, defaults (text_mode, default_locale, panels_per_page_cap, nav_default, pixel_rendering), identity_refs (the locked cast hex table from the style bible — pure pointer/hex hub, never duplicated art), ui_tokens (viewer theming, palette pinned).
  2. pages[] — reading order, ONE entry per storyboard_spec.page_order page, in that exact order — EXCEPT a declared endcard page (the ENDCARD FOLD, below: an endcard/finale storyboard page compiles into the PRIOR page's closing{}, never its own pages[] entry). Each entry: id (the stable FINAL page id, e.g. P00_cover/P_B03/P02_b08 — see the PAGE-ID rule below), type (cover|single|grid|grid2x2|feature|finale), panel_ids (the panel ids that page shows), plus optional beat/beat_title/narration/skill/title/links (cover/closing buttons — https:// only).
  3. panels{} — ONE keyed entry per panel id that appears in any page. Per panel project:
    • condition{} (what to GENERATE): content_svg (the locked blueprint's deterministic SVG = the content authority), expected_literals (verbatim from the blueprint), world, identity_ref (from the prompt_bundle or null → project canonical), identity_desc, characters, scene.
    • render fields (for the viewer): text_mode, crop (shape∈hero|wide|square, position [x,y] in 0..1, zoom ≥ 1), safe_zones (each with an id), bubbles (speaker∈researcher|executor|reviewer, style∈say|shout|terse|thought|whisper, text{zh,en}, optional anchor = one of this panel's safe-zone ids), overlays, caption{zh,en}.
    • leave EMPTY: image_path, active_attempt_id, wiki_node_id.
  4. Emit movie.project.json — the pure pointer-hub manifest (project_id, title{zh,en}, story, schema_version, comic_json, art_bible, identity_ref, dirs, text_mode_default, palette). It names the other artifacts; it NEVER duplicates their content.

Two storyboard→IR projections step ② MUST know (or the reference itself looks broken):

  • THE ENDCARD FOLD. A storyboard page of type endcard (the finale card) does NOT compile to its own pages[] entry: its single panel becomes the PRIOR page's closing.image (with the logo/tag/links in the same closing{} block). Reference: the storyboard declares 19 pages ending in a standalone P03_end[S22·endcard]; the compiled IR ships 18 pages[] with P_B12 (type finale, panels S20+S21) carrying closing.image: "S22". The 19-vs-18 difference IS the fold — by design, not a lost page. NOTE the schema does not model closing{} at all (it validates via additionalProperties), so asserting "the fold actually happened" is step ②'s inline job, nobody else's.
  • THE PAGE-ID RULE. The compiled IR's canonical page ids are beat-keyed (P_B<beat>: P_B03…P_B07, P_B08_0…P_B12), while the human STORYBOARD_DRAFT.md's first-third draft ids are ordinal-prefixed (P02_b03/P03_b04/…). The stable key is the beat token (b03B03, case-insensitive); the already-canonical anchors (P00_cover, P01_b02, P02_b08) keep their ids. The LOCKED storyboard_spec.page_order — the reconcile authority — must be authored in the FINAL IR vocabulary; the compiler never renames silently. Never build a page_order for the reconcile from the .md draft's ordinal ids — that draft is a human artifact, not the locked node, and an element-wise compare against it false-blocks the shipped reference.

② RECONCILE page-count integrity (the heart of this skill — INLINE python, NO external script)

There is no reconcile_pages.py — it never existed. This catch is run inline (the schema and the per-page run_comic.py are both blind to it), so paste-and-run this ~15-line whole-comic diff against the storyboard authority. This is where the orphan-panel class dies. Exit non-zero on ANY blocker; do NOT proceed to the gate until it is clean.

# inline reconcile — run against the assembled comic.json + the locked storyboard_spec node (NO phantom tool)
import json, sys, re
c  = json.load(open("comic.json", encoding="utf-8"))
sb = json.load(open("wiki/nodes/storyboard_<slug>.json", encoding="utf-8"))["payload"]   # the LOCKED authority
page_refs = [pid for p in c["pages"] for pid in p["panel_ids"]]      # every page→panel reference (with dups)
closing_refs = [(p.get("closing") or {}).get("image") for p in c["pages"] if (p.get("closing") or {}).get("image")]
ref_set   = set(page_refs) | set(closing_refs);  defined = set(c["panels"])  # a finale endcard (closing.image) IS a use
orphans   = sorted(defined  - ref_set)            # defined, not on a page AND not a closing endcard → BLOCKER
dangling  = sorted(ref_set  - defined)            # a page/closing points at a missing panel → BLOCKER
# PAGE-ORDER AUTHORITY — ELEMENT-WISE vs the storyboard (NOT a length check): catches reorder / id-swap / dup /
# over-count that a count comparison silently passes.
comic_order = [p["id"] for p in c["pages"]]
sb_order    = list(sb["page_order"])
# ENDCARD FOLD — a declared endcard page has NO pages[] entry (its panel compiles into the PRIOR page's
# closing.image: storyboard P03_end[S22·endcard] → P_B12.closing.image; 19 declared → 18 pages[]). EXCLUDE the
# declared endcard pages from the element-wise compare, then assert the fold HAPPENED (a declared endcard panel
# on no page's closing.image = the finale silently vanished → BLOCKER). Read the declaration off the storyboard
# (its global_policies endcard map {endcard_page_id: panel_id}) — declared, never inferred.
gp0 = sb.get("global_policies", {})
endcard_map = dict(gp0.get("endcard_folds") or {})     # e.g. {"P03_end": "S22"}; {} when the comic has no endcard
sb_order_cmp = [pid for pid in sb_order if pid not in endcard_map]
endcard_unfolded = sorted(pn for pn in endcard_map.values() if pn not in set(closing_refs))  # fold missing → BLOCKER
order_mismatch = comic_order != sb_order_cmp           # STRICT: any reorder / missing / extra / dup → BLOCKER
order_missing  = [pid for pid in sb_order_cmp if pid not in comic_order]  # storyboard page absent from comic.json
order_extra    = [pid for pid in comic_order if pid not in sb_order_cmp]  # comic.json page not in storyboard
page_id_dups   = sorted({pid for pid in comic_order if comic_order.count(pid) > 1})  # duplicate page id (over-count)
bad_grid  = [p["id"] for p in c["pages"] if p["type"] == "grid2x2" and len(p["panel_ids"]) != 4]  # arity → BLOCKER
empty_pg  = [p["id"] for p in c["pages"] if not p["panel_ids"]]    # empty page → BLOCKER
# FAIL-CLOSED engine contracts — jsonschema leaves these OPTIONAL, so this inline step is the REAL enforcer:
def cond(pn): return pn.get("condition") or {}
no_csvg = sorted(pid for pid, pn in c["panels"].items() if not cond(pn).get("content_svg"))   # null/missing content_svg
# cfg_usable EXACTLY (run_comic.py): the EFFECTIVE mode resolves from defaults (a panel inheriting a baked
# default is still baked); a baked panel WITH a content_svg MUST carry expected_literals as a non-empty LIST
# whose every entry is an ascii-tokenizable string. ONE predicate = never weaker than the engine floor.
def emode(pn): return pn.get("text_mode") or (c.get("defaults") or {}).get("text_mode") or "html"
def baked_lits_bad(pn):
    if emode(pn) != "baked" or not cond(pn).get("content_svg"): return False
    exp = cond(pn).get("expected_literals")
    return not (isinstance(exp, list) and len(exp) > 0
                and all(isinstance(e, str) and re.findall(r"[a-z0-9+._-]+", e.lower()) for e in exp))
baked_lits_fail = sorted(pid for pid, pn in c["panels"].items() if baked_lits_bad(pn))
engine_authored = sorted(pid for pid, pn in c["panels"].items()                 # authored ENGINE fields MUST be empty
                         if pn.get("image_path") or pn.get("active_attempt_id") or pn.get("wiki_node_id"))
# bubble.anchor (if set) MUST resolve to a panel safe_zone id. safe_zones is a panel TOP-LEVEL field
# (comic.schema.json) — NOT under condition; jsonschema only TYPES anchor as a string, so assert it inline.
def sz_ids(pn): return {z.get("id") for z in (pn.get("safe_zones") or [])}
bad_anchor = sorted(f"{pid}:{b.get('anchor')}" for pid, pn in c["panels"].items()
                    for b in (pn.get("html_bubbles") or pn.get("bubbles") or [])
                    if isinstance(b, dict) and b.get("anchor") and b["anchor"] not in sz_ids(pn))
# DECLARED recap reuse is the ONLY legitimate multi-reference — read it from the storyboard, never guess.
gp = sb.get("global_policies", {})
sanctioned = set(gp.get("recap_panel_ids") or gp.get("sanctioned_recap") or [])   # storyboard-declared recap/grid set
dups = sorted({pid for pid in page_refs if page_refs.count(pid) > 1} - sanctioned)  # UNDECLARED panel dup → BLOCKER
blockers = {"orphans": orphans, "dangling": dangling, "order_mismatch": order_mismatch, "order_missing": order_missing,
            "order_extra": order_extra, "page_id_dups": page_id_dups, "endcard_unfolded": endcard_unfolded,
            "bad_grid2x2": bad_grid, "empty_pages": empty_pg,
            "undeclared_dups": dups, "content_svg_null": no_csvg, "baked_literals_invalid": baked_lits_fail,
            "engine_fields_authored": engine_authored,
            "bubble_anchor_unresolved": bad_anchor}
if any(blockers.values()):
    sys.exit(f"RECONCILE BLOCKERS: {blockers}  (pages {len(comic_order)} vs storyboard {len(sb_order_cmp)} after endcard fold)")
print("reconcile OK")
  • The orphan check counts closing.image. In the reference comic.json, S22 (the wordless constellation finale) is the endcard of the finale page P_B12 (P_B12.closing.image = "S22") — a legitimate use, NOT an orphan. (An earlier draft of this check read only panel_ids and FALSE-flagged it.) A TRUE orphan is a panel in panels{} referenced by neither a page's panel_ids NOR any closing.image; that is a real BLOCKER the compiler must catch, not ship — page it, or (if genuinely cut) make an upstream storyboard edit + re-lock, never a quiet drop. When a locked storyboard_spec node is present, its page_order is the element-wise page authority (reorder / missing / extra / duplicate page id all block) — after excluding its declared endcard pages, which by contract have no pages[] entry (the fold itself is asserted separately via endcard_unfolded).
  • Recap reuse is the ONE legitimate multi-reference and must be declared, not inferred. In the reference comic, S12,S13,S14,S15 are each referenced twice — once as individual single pages (P_B08_1..4) and once in the grid2x2 recap/hero page (P02_b08). That is by-design (the storyboard's mirror_locks / recap policy), so a panel referenced >1 time is a veto ONLY if it is not on the storyboard's declared recap/grid set. The diff reads the storyboard's global_policies (the sanctioned recap ids) to know which reuses are sanctioned — an undeclared duplicate reference is still a BLOCKER.

③ VALIDATE against the schema (SHAPE only — it does NOT enforce the fail-closed contracts)

python3 -c "import json,jsonschema,sys; jsonschema.validate(json.load(open(sys.argv[1]+'/comic.json')), json.load(open('schemas/comic.schema.json')))" <project_dir>   # run from repo root

Know exactly what this does and does NOT catch. schemas/comic.schema.json leaves condition, condition.content_svg, and condition.expected_literals all OPTIONAL (it constrains their shape when present but never requires them). So jsonschema PASSES a panel with content_svg: null/absent or a baked panel with empty expected_literals — exactly the states vetoes #5/#6 and the two fail-closed contracts forbid. Therefore the schema is NOT the contract enforcer — step ④ (this skill's own assertion) and the run_comic.py --dry-run cfg_usable fail-closed (step ⑤) are what bite. Do not rely on jsonschema for them.

  • build_comic.py is NOT a compile-time fallback. It lives at packages/viewer/build_comic.py and is the Phase-3 post-bake viewer build: it requires every panel's image_path to be present and a real image on disk. A freshly-authored comic.json (engine fields EMPTY, per this skill) would make build_comic.py hard-fail on the missing images — it is the wrong tool here, and it never checks orphans. Cite it only for the post-bake viewer stage (Phase 3), with its real packages/viewer/ path; the compile-time deterministic floor is cli/validate_wiki.py + run_comic.py --dry-run (step ⑤), not build_comic.py.

④ Enforce the two FAIL-CLOSED engine contracts (see the dedicated section below)

Every panel has a real content_svg (never null); every baked figure-panel has non-empty, ASCII-tokenizable expected_literals. The one nuance — the zero-text panel (S22) — is spelled out below.

⑤ Run the compile gate (DETERMINISTIC = both scripts exit 0; no reviewer fan-out)

The --gate compile floor is two real on-disk scripts. run_comic.py is a per-page spiral runner — its argparse marks both --page and --panels as required, so it must be invoked once per page with that page's panel ids (the project-only command crashes with an argparse error). It does NOT validate jsonschema and never sees the whole pages[]/panels{} set — so it CANNOT catch the orphan/page-count (that was step ②). Its --dry-run job is per-page bake-readiness: it asserts every text_mode:"baked" figure-panel on that page carries ascii-tokenizable expected_literals (the cfg_usable fail-closed) and prints each concrete bake prompt (real scene + real literals, no placeholders).

# (a) per page in the storyboard page_order — bake-readiness + fail-closed literals (NOT schema/orphan):
python3 skills/comic-director/scripts/run_comic.py --project examples/comic_m3_audit --page P02_b08 --panels S12,S13,S14,S15 --dry-run
#   …repeat for every page; a baked figure-panel with no ascii expected_literals → run_comic EXITS non-zero
# (b) wiki node/edge/payload/node_id/privacy conformance (the whole project, against node_schema.json):
python3 cli/validate_wiki.py examples/comic_m3_audit

First write the compile decision node + its edges (§ "Node it reads / writes"), then defer to the gate. comic-cross-layer-gate decision:compile_<slug> --gate compile is purely deterministic (per the gate skill): NO review:* nodes, NO Codex adjudication — it simply runs the two scripts above and returns approve iff both exit 0, else revise carrying their stderr as the blocker list. (This is the one gate where the "create review nodes first" pre-step does NOT apply — the scripts ARE the judge.) On approve → flip the decision to final and emit; on revise → fix the offending upstream node and recompile. The gate (the deterministic scripts, a different acquittal organ) — never this compiling agent — acquits (acceptance-gate).

⑥ EMIT

Write comic.json + movie.project.json via output-versioning (timestamped + fixed-name latest; downstream reads the fixed name). Emit the asset-copy manifest — the flat list of every content_svg / identity_ref the IR references and where each lives — so Phase 2/3 can stage them. Append the wiki nodes/edges (§ below) and trace the gate round to trace.jsonl (review-tracing).

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-json-compiler
Source
github.com/wanshuiyin/aris-movie-director