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
filenamemust match regexLSS_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 viaif/then/else)book_numberinteger 1–6 ·book_nameenum of 6 titles ·domainenum of 6 codesscene_numberinteger 1–6 ·variantenumA|B·typeenumSTILL|ANIMpromptstring 40–600 chars ·additionalProperties: false(no rogue fields slip through)- Agent instructions embedded in
$commentblocks — 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.