Files

131 lines
5.6 KiB
Python

# Generates h3_short_film_manual.json: the turbo short-film render graph with
# the Claude writer replaced by APNext H3 Manual Scenes - the film is a
# hand-authored script (the Lighthouse Letter example), no model is called.
import json
import os
EX = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "examples", "h3")
SCRIPT_TXT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lighthouse_letter_script.txt")
WRITER_ID = 163
CHARACTERS_ID = 157
NOTE_ID = 161
MANUAL_ID = 217 # last_node_id in the turbo file is 216
NOTE_TEXT = """# H3 Short Film - manual scenes
1. The film is written BY HAND: the **H3 Manual Scenes** node holds the whole script - one `=== SCENE NN | duration: S.S ===` envelope per clip, each a complete four-section H3 prompt (`subject_definitions`, `integrated_multimodal_description`, `overall_soundscape`, `non_diegetic_music`). Edit the text, change durations, add or remove scenes - no model is called, the script IS the film.
2. The node turns the script into the same matching lists the writers emit (`scenes`, `durations`, `lengths`), so the render side is identical to the turbo workflow: each scene renders as one clip of exactly its duration.
3. The default script is *The Lighthouse Letter*: 11 scenes, ~2 minutes - keep character and location anchor lines word-for-word identical across scenes (as the example does) so faces, wardrobe and sets stay consistent from clip to clip.
"""
def load(name):
with open(os.path.join(EX, name), encoding="utf-8") as f:
return json.load(f)
def save(wf, name):
wf["last_node_id"] = max(n["id"] for n in wf["nodes"])
wf["last_link_id"] = max((l[0] for l in wf["links"]), default=0)
with open(os.path.join(EX, name), "w", encoding="utf-8") as f:
json.dump(wf, f, indent=2, ensure_ascii=False)
print(f"wrote {name}")
def validate(wf, name):
nodes = {n["id"]: n for n in wf["nodes"]}
seen = set()
for lid, src, sslot, dst, dslot, _t in wf["links"]:
assert lid not in seen, f"{name}: duplicate link {lid}"
seen.add(lid)
s, d = nodes[src], nodes[dst]
assert lid in (s["outputs"][sslot].get("links") or []), \
f"{name}: link {lid} missing on {src}.outputs[{sslot}]"
assert d["inputs"][dslot].get("link") == lid, \
f"{name}: link {lid} mismatch on {dst}.inputs[{dslot}]"
for n in wf["nodes"]:
for i, inp in enumerate(n.get("inputs", [])):
if inp.get("link") is not None:
assert inp["link"] in seen, f"{name}: node {n['id']} input {i} dangling link {inp['link']}"
for o, out in enumerate(n.get("outputs", [])):
for lid in out.get("links") or []:
assert lid in seen, f"{name}: node {n['id']} output {o} dangling link {lid}"
print(f"validated {name}: {len(wf['nodes'])} nodes, {len(wf['links'])} links")
def main():
with open(SCRIPT_TXT, encoding="utf-8") as f:
script_text = f.read().strip()
wf = load("h3_short_film_turbo.json")
wf["id"] = "8f2a5f7e-4b1c-4b9e-9d3a-manualfilm01"
nodes = {n["id"]: n for n in wf["nodes"]}
writer = nodes[WRITER_ID]
# links leaving the writer, keyed by output name; they are rewired below
out_by_name = {o["name"]: o for o in writer["outputs"]}
kept = {} # link id -> new source slot on the manual node
manual_slots = {"scenes": 0, "durations": 1, "lengths": 2, "scenes_text": 3}
for name, slot in manual_slots.items():
for lid in (out_by_name.get(name, {}).get("links") or []):
kept[lid] = slot
# drop the writer, the characters node and every link touching them that
# is not rewired
dropped_ids = {WRITER_ID, CHARACTERS_ID}
dead_links = {
l[0] for l in wf["links"]
if (l[1] in dropped_ids or l[3] in dropped_ids) and l[0] not in kept
}
wf["nodes"] = [n for n in wf["nodes"] if n["id"] not in dropped_ids]
wf["links"] = [l for l in wf["links"] if l[0] not in dead_links]
for n in wf["nodes"]:
for inp in n.get("inputs", []):
if inp.get("link") in dead_links:
inp["link"] = None
for out in n.get("outputs", []):
if out.get("links"):
out["links"] = [l for l in out["links"] if l not in dead_links]
# the manual node takes the writer's place on the canvas
manual = {
"id": MANUAL_ID,
"type": "H3ManualScenes",
"pos": [writer["pos"][0], writer["pos"][1]],
"size": [520, 780],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{"name": "scenes", "type": "STRING", "links": [], "shape": 6},
{"name": "durations", "type": "FLOAT", "links": [], "shape": 6},
{"name": "lengths", "type": "INT", "links": [], "shape": 6},
{"name": "scenes_text", "type": "STRING", "links": []},
{"name": "scene_count", "type": "INT", "links": []},
{"name": "total_seconds", "type": "FLOAT", "links": []},
],
"properties": {"Node name for S&R": "H3ManualScenes"},
"widgets_values": [script_text, 12.0, ""],
}
wf["nodes"].append(manual)
# rewire the kept links onto the manual node's outputs
for l in wf["links"]:
if l[0] in kept:
l[1] = MANUAL_ID
l[2] = kept[l[0]]
manual["outputs"][kept[l[0]]]["links"].append(l[0])
nodes = {n["id"]: n for n in wf["nodes"]}
nodes[NOTE_ID]["widgets_values"][0] = NOTE_TEXT
nodes[NOTE_ID]["title"] = "How this works"
save(wf, "h3_short_film_manual.json")
validate(load("h3_short_film_manual.json"), "h3_short_film_manual.json")
if __name__ == "__main__":
main()