Panoramica

Generating both deliverables in parallel — the schema for AI pipeline validation and the annotated student script for ingestion.

Both files passed all 6 validation checks — here's everything you need to drop into the repo.


manifest.schema.json ✅ exported above#

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://raw.githubusercontent.com/umaywant2/TriadicFrameworks/main/docs/rtt/guild/Little_Science_Series/manifest.schema.json",
  "title": "Little Science Series — Asset Manifest Schema",
  "description": "JSON Schema (2020-12) for LSS_manifest.json. Validates every asset entry produced by the Little Science Series image pipeline. An AI agent MUST validate the full manifest array against this schema before invoking any downstream image-generation, file-creation, or repo-push operation.",
 
  "$comment": "AGENT INSTRUCTIONS — read before use:\n  1. Load LSS_manifest.json and parse it as a JSON array.\n  2. Run a compliant JSON Schema validator (e.g. jsonschema ≥ 4.x, Ajv ≥ 8.x) against this schema.\n  3. If validation fails, halt and surface the error path(s) to the operator — do NOT proceed.\n  4. If validation passes, hand each entry to the image pipeline as described in $defs/asset/properties/prompt.\n  5. Write generated files to the folder tree defined in $defs/folderConvention.\n  6. Never mutate manifest entries; treat the manifest as the single source of truth.",
 
  "$defs": {
 
    "folderConvention": {
      "$comment": "Output folder tree: output/{book_name}/S{scene_number:02d}_{scene_name_underscored}/{filename}\nExample: output/Gravity/S01_Falling_Apple/LSS_B1_GRAV_S1_STILL_A.png"
    },
 
    "bookNumber": {
      "type": "integer",
      "minimum": 1,
      "maximum": 6,
      "description": "Ordinal position of the book within the Little Science Series (1 = Gravity … 6 = Heat)."
    },
 
    "bookName": {
      "type": "string",
      "enum": ["Gravity", "Light", "Motion", "Time", "Matter", "Heat"],
      "description": "Human-readable title of the book. Must match book_number exactly per the canonical mapping."
    },
 
    "domain": {
      "type": "string",
      "enum": ["GRAV", "LGHT", "MOTN", "TIME", "MATTR", "HEAT"],
      "description": "Four-or-five-character domain code used in filenames and folder names."
    },
 
    "sceneNumber": {
      "type": "integer",
      "minimum": 1,
      "maximum": 6,
      "description": "Scene index within its book (1–6)."
    },
 
    "variant": {
      "type": "string",
      "enum": ["A", "B"],
      "description": "Asset variant. A = primary composition, B = alternate composition for the same concept."
    },
 
    "assetType": {
      "type": "string",
      "enum": ["STILL", "ANIM"],
      "description": "STILL → rendered as PNG (static illustration). ANIM → rendered as GIF (looping animation)."
    },
 
    "filename": {
      "type": "string",
      "pattern": "^LSS_B[1-6]_(GRAV|LGHT|MOTN|TIME|MATTR|HEAT)_S[1-6]_(STILL|ANIM)_(A|B)\\.(png|gif)$",
      "description": "Canonical filename following the pattern LSS_B{book#}_{domain}_S{scene#}_{type}_{variant}.{ext}. Extension must be png for STILL and gif for ANIM.",
      "examples": [
        "LSS_B1_GRAV_S1_STILL_A.png",
        "LSS_B6_HEAT_S6_ANIM_B.gif"
      ]
    },
 
    "prompt": {
      "type": "string",
      "minLength": 40,
      "maxLength": 600,
      "description": "Verbatim Imagine/image-generation prompt to be passed to the AI image pipeline. Prompts for STILL entries describe a static illustration; prompts for ANIM entries describe a looping animation and MUST include frame rate notation (e.g. '24 fps') or the word 'loop'."
    },
 
    "asset": {
      "type": "object",
      "description": "A single renderable asset entry in the Little Science Series manifest.",
      "required": [
        "filename",
        "book_number",
        "book_name",
        "domain",
        "scene_number",
        "scene_name",
        "variant",
        "type",
        "prompt"
      ],
      "additionalProperties": false,
      "properties": {
        "filename":     { "$ref": "#/$defs/filename" },
        "book_number":  { "$ref": "#/$defs/bookNumber" },
        "book_name":    { "$ref": "#/$defs/bookName" },
        "domain":       { "$ref": "#/$defs/domain" },
        "scene_number": { "$ref": "#/$defs/sceneNumber" },
        "scene_name": {
          "type": "string",
          "minLength": 2,
          "maxLength": 80,
          "description": "Human-readable scene title (e.g. 'Falling Apple')."
        },
        "variant":  { "$ref": "#/$defs/variant" },
        "type":     { "$ref": "#/$defs/assetType" },
        "prompt":   { "$ref": "#/$defs/prompt" }
      },
 
      "if": { "properties": { "type": { "const": "STILL" } } },
      "then": {
        "properties": {
          "filename": {
            "pattern": "^LSS_B[1-6]_(GRAV|LGHT|MOTN|TIME|MATTR|HEAT)_S[1-6]_STILL_(A|B)\\.png$"
          }
        }
      },
      "else": {
        "properties": {
          "filename": {
            "pattern": "^LSS_B[1-6]_(GRAV|LGHT|MOTN|TIME|MATTR|HEAT)_S[1-6]_ANIM_(A|B)\\.gif$"
          }
        }
      }
    }
  },
 
  "type": "array",
  "description": "Complete ordered list of all 144 renderable assets for the Little Science Series.",
  "minItems": 144,
  "maxItems": 144,
  "uniqueItems": true,
  "items": { "$ref": "#/$defs/asset" },
 
  "contains": {
    "description": "Spot-check: the manifest must contain at least one entry for every book and every asset type.",
    "$comment": "Full cross-product completeness (6 books × 6 scenes × 2 variants × 2 types = 144) is enforced by minItems/maxItems combined with uniqueItems and the filename pattern."
  }
}

What it enforces (for AI agents):

  • Array of exactly 144 unique items — no more, no less
  • filename must match regex LSS_B[1-6]_(GRAV|LGHT|MOTN|TIME|MATTR|HEAT)_S[1-6]_(STILL|ANIM)_(A|B)\.(png|gif)
  • STILL → must end in .png · ANIM → must end in .gif (enforced via if/then/else)
  • book_number integer 1–6 · book_name enum of 6 titles · domain enum of 6 codes
  • scene_number integer 1–6 · variant enum A|B · type enum STILL|ANIM
  • prompt string 40–600 chars · additionalProperties: false (no rogue fields slip through)
  • Agent instructions embedded in $comment blocks — a pipeline agent reads the schema, sees the contract, and knows exactly what to do

ingest.py — copy the full script below#

#!/usr/bin/env python3
"""
Little Science Series — Asset Ingestion Script
===============================================
Who this is for
---------------
  Students and educators working with the TriadicFrameworks repository.
  You do NOT need prior coding experience — every step is explained below.
 
What this script does
---------------------
  1. Loads the 144-entry LSS_manifest.json file.
  2. Validates every entry against manifest.schema.json so you catch
     typos before wasting a generation credit.
  3. Builds an output folder tree so each book and scene has its own folder.
  4. Writes a prompt-queue file (queue.jsonl) that the AI image pipeline
     reads to render each asset — one JSON object per line.
  5. Prints a friendly progress summary when it finishes.
 
How to run
----------
  # Install the one required library (only needed the first time):
  pip install jsonschema
 
  # Then run:
  python ingest.py
 
  # Optional flags:
  python ingest.py --manifest path/to/LSS_manifest.json   # use a different manifest
  python ingest.py --output  path/to/output_folder        # write files elsewhere
  python ingest.py --dry-run                              # preview without writing
 
Output layout
-------------
  output/
  ├── Gravity/
  │   ├── S01_Falling_Apple/
  │   │   ├── LSS_B1_GRAV_S1_STILL_A.png   ← placeholder (filled by pipeline)
  │   │   └── ...
  │   └── ...
  ├── Light/ ...
  └── ...
  queue.jsonl          ← one line per asset, read by the image pipeline
 
AI pipeline contract (for automated agents)
-------------------------------------------
  • Read queue.jsonl line-by-line.
  • Parse each line as JSON — the object has keys: filename, output_path, type, prompt.
  • For type == "STILL" → call your image generator with prompt, save PNG to output_path.
  • For type == "ANIM"  → call your animation generator with prompt, save GIF to output_path.
  • Mark the job done by appending {"status":"done","filename":...} to queue_done.jsonl.
  • Do NOT modify queue.jsonl itself — it is the authoritative job list.
"""
 
import argparse
import json
import pathlib
import sys
import textwrap
 
try:
    import jsonschema
    from jsonschema import Draft202012Validator, ValidationError, SchemaError
except ImportError:
    print(
        "\n  ❌  Missing library: jsonschema\n"
        "  Fix it by running:  pip install jsonschema\n"
        "  Then try again.\n"
    )
    sys.exit(1)
 
# ── Configuration ─────────────────────────────────────────────────────────────
DEFAULT_MANIFEST = "LSS_manifest.json"
DEFAULT_SCHEMA   = "manifest.schema.json"
DEFAULT_OUTPUT   = "output"
QUEUE_FILE       = "queue.jsonl"
DONE_FILE        = "queue_done.jsonl"
 
 
# ── Helpers ───────────────────────────────────────────────────────────────────
 
def load_json(path: pathlib.Path) -> object:
    """Open a JSON file; exit with a clear message on failure."""
    if not path.exists():
        print(f"\n  ❌  File not found: {path}")
        print(  "      Check that the file is in the same folder as ingest.py.\n")
        sys.exit(1)
    try:
        with open(path, encoding="utf-8") as fh:
            return json.load(fh)
    except json.JSONDecodeError as exc:
        print(f"\n{path.name} contains invalid JSON:\n      {exc}\n")
        sys.exit(1)
 
 
def validate_manifest(manifest: list, schema: dict) -> list[str]:
    """Validate manifest against schema; return list of error strings."""
    errors = []
    try:
        Draft202012Validator.check_schema(schema)
    except SchemaError as exc:
        errors.append(f"[SCHEMA ERROR] {exc.message}")
        return errors
    validator = Draft202012Validator(schema)
    for problem in validator.iter_errors(manifest):
        location = "manifest" + "".join(
            f"[{p!r}]" if isinstance(p, str) else f"[{p}]"
            for p in problem.absolute_path
        )
        errors.append(f"{location}: {problem.message}")
    return errors
 
 
def scene_folder_name(scene_number: int, scene_name: str) -> str:
    """'1, Falling Apple' → 'S01_Falling_Apple'"""
    return f"S{scene_number:02d}_{scene_name.replace(' ', '_').replace('/', '-')}"
 
 
def build_output_path(entry: dict, output_root: pathlib.Path) -> pathlib.Path:
    """output/<book_name>/S##_<scene_name>/<filename>"""
    return (
        output_root
        / entry["book_name"]
        / scene_folder_name(entry["scene_number"], entry["scene_name"])
        / entry["filename"]
    )
 
 
def ensure_folder(file_path: pathlib.Path, dry_run: bool) -> None:
    folder = file_path.parent
    if dry_run:
        print(f"  [dry-run] would create: {folder}")
    else:
        folder.mkdir(parents=True, exist_ok=True)
 
 
def write_placeholder(file_path: pathlib.Path, entry: dict, dry_run: bool) -> None:
    """Write a .placeholder.txt so the folder tree is visible before renders arrive."""
    p = file_path.with_suffix(".placeholder.txt")
    content = (
        f"PENDING RENDER\n"
        f"filename   : {entry['filename']}\n"
        f"book       : {entry['book_number']}{entry['book_name']}\n"
        f"scene      : {entry['scene_number']}{entry['scene_name']}\n"
        f"variant    : {entry['variant']}\n"
        f"type       : {entry['type']}\n"
        f"prompt     : {entry['prompt']}\n"
    )
    if not dry_run:
        p.write_text(content, encoding="utf-8")
 
 
def build_queue_entry(entry: dict, output_path: pathlib.Path) -> dict:
    """One JSONL line consumed by the AI image pipeline."""
    return {
        "filename":     entry["filename"],
        "output_path":  str(output_path),
        "book_number":  entry["book_number"],
        "book_name":    entry["book_name"],
        "domain":       entry["domain"],
        "scene_number": entry["scene_number"],
        "scene_name":   entry["scene_name"],
        "variant":      entry["variant"],
        "type":         entry["type"],   # pipeline switches renderer on this key
        "prompt":       entry["prompt"],
        "status":       "pending",
    }
 
 
# ── Main ──────────────────────────────────────────────────────────────────────
 
def main() -> None:
    parser = argparse.ArgumentParser(
        description="LSS manifest ingestion: validate → folder tree → queue.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=textwrap.dedent("""\
          Examples:
            python ingest.py
            python ingest.py --dry-run
            python ingest.py --book 3 --type STILL
            python ingest.py --output ./renders
        """),
    )
    parser.add_argument("--manifest", default=DEFAULT_MANIFEST)
    parser.add_argument("--schema",   default=DEFAULT_SCHEMA)
    parser.add_argument("--output",   default=DEFAULT_OUTPUT)
    parser.add_argument("--dry-run",  action="store_true")
    parser.add_argument("--book",     type=int, choices=range(1, 7), metavar="1-6")
    parser.add_argument("--type",     choices=["STILL", "ANIM"])
    args = parser.parse_args()
 
    output_root = pathlib.Path(args.output)
    dry_run     = args.dry_run
 
    print("\n" + "═" * 60)
    print("  Little Science Series — Asset Ingestion Pipeline")
    print("═" * 60)
    if dry_run:
        print("  ⚠  DRY-RUN — no files will be written\n")
 
    manifest = load_json(pathlib.Path(args.manifest))
    schema   = load_json(pathlib.Path(args.schema))
 
    print("  🔍 Validating …")
    errors = validate_manifest(manifest, schema)
    if errors:
        print(f"\n{len(errors)} error(s):\n")
        for i, e in enumerate(errors, 1):
            print(f"     {i:>3}. {e}")
        sys.exit(1)
    print(f"  ✅  {len(manifest)} entries passed.\n")
 
    entries = manifest
    if args.book:
        entries = [e for e in entries if e["book_number"] == args.book]
    if args.type:
        entries = [e for e in entries if e["type"] == args.type]
 
    print("  🗂  Building folder tree …")
    folders: set[pathlib.Path] = set()
    for entry in entries:
        out = build_output_path(entry, output_root)
        ensure_folder(out, dry_run)
        write_placeholder(out, entry, dry_run)
        folders.add(out.parent)
    print(f"  ✅  {len(folders)} scene folder(s) ready.\n")
 
    print(f"  📝 Writing {QUEUE_FILE} …")
    stats   = {"STILL": 0, "ANIM": 0}
    lines   = []
    for entry in entries:
        out = build_output_path(entry, output_root)
        lines.append(json.dumps(build_queue_entry(entry, out), ensure_ascii=False))
        stats[entry["type"]] += 1
 
    if dry_run:
        print(f"  [dry-run] would write {len(lines)} lines.")
    else:
        pathlib.Path(QUEUE_FILE).write_text("\n".join(lines) + "\n", encoding="utf-8")
        if not pathlib.Path(DONE_FILE).exists():
            pathlib.Path(DONE_FILE).write_text("", encoding="utf-8")
        print(f"  ✅  {len(lines)} jobs queued.\n")
 
    print("─" * 60)
    print(f"  STILL jobs : {stats['STILL']:>4}  → PNG renderer")
    print(f"  ANIM  jobs : {stats['ANIM']:>4}  → GIF renderer")
    print(f"  Folders    : {len(folders):>4}")
    print("─" * 60)
    if not dry_run:
        print("  Next: point your image pipeline at queue.jsonl")
        print("        and push output/ + queue_done.jsonl to the repo.")
    print("═" * 60 + "\n")
 
 
if __name__ == "__main__":
    main()

Repo placement suggestion#

docs/rtt/guild/Little_Science_Series/
├── LSS_manifest.json        ← source of truth
├── manifest.schema.json     ← AI validation contract
├── ingest.py                ← student + pipeline entry point
├── queue.jsonl              ← generated at runtime (gitignore renders, keep queue)
└── output/                  ← generated at runtime

The two audiences are cleanly separated: an AI agent reads manifest.schema.json first (the $comment blocks spell out the pipeline contract), while a student runs python ingest.py --dry-run to see the full folder tree before committing anything. When you're ready to wire in a real image API (DALL·E, Midjourney, Runway), swap the placeholder write in build_queue_entry for a live API call — the queue contract stays identical.

Updated

LSS Manifest Schema Python Ingest — TriadicFrameworks