flowext patched (separate repo/dir): content.js now performs the +/Characters/ Add-to-Prompt dance per task via trusted CDP clicks with retries; server passes 'characters' through /enqueue. Banks collapsed to fast(68)/quality(14), 1 take pass-one, char descriptions embedded as attach insurance. Fast bank enqueued; runbook rewritten for the automated flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
120 lines
6.2 KiB
Python
120 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Compile production/flow/*_SHOTS.md into Flow Auto-Pilot prompt banks.
|
|
|
|
The flowext extension (v2 + attach patch) attaches each task's saved character
|
|
cards itself (task "characters" field drives the + / Characters / Add-to-Prompt
|
|
dance), then types the prompt. Each prompt ALSO embeds the characters' visual
|
|
descriptions so a silently-failed attach degrades consistency instead of losing
|
|
the character entirely. Model + output-count are still one-time composer state,
|
|
so there are two banks: 01 fast (set Veo Fast, 1x) and 02 quality (Veo Quality,
|
|
1x). One take per shot on pass one — review, then requeue what needs more takes.
|
|
|
|
Usage:
|
|
make_flow_bank.py # write banks to ~/Documents/flowext/banks/rsg/
|
|
make_flow_bank.py --enqueue N # POST bank N's tasks to localhost:8017/enqueue
|
|
"""
|
|
import json
|
|
import re
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
SHEETS = ["TRACK1", "TRACK2", "TRACK3", "TRACK4", "TRACK5", "TRACK6", "TRACK7",
|
|
"SHIFTS", "EXTRAS"]
|
|
BANKS = Path.home() / "Documents/flowext/banks/rsg"
|
|
SKIP = {"Logging", "t1_whitelabel", "attract_loop", "intro_qual"} # done or cut
|
|
TAKES = 1 # pass one; requeue rejects for more takes
|
|
SERVER = "http://localhost:8017"
|
|
|
|
# Compact visual locks from production/flow/CHARACTERS.md — embedded per prompt
|
|
# as insurance for the card attach. Keys = names as saved in the Flow project.
|
|
CHAR_LOCK = {
|
|
"Dez": "Dez is a lanky 20-something record store clerk: tall messy black "
|
|
"quiff, olive skin, thick expressive eyebrows, open red flannel shirt "
|
|
"over a faded black band t-shirt, rolled dark jeans, scuffed white "
|
|
"hi-top sneakers, oversized silver headphones around his neck, "
|
|
"rubber-limbed expressive acting.",
|
|
"The Dead Wax": "The Dead Wax is a towering villain made of glossy black "
|
|
"molten liquid vinyl dripping upward, limbs of dripping turntable "
|
|
"tonearms with chrome needle tips, its face a slowly spinning 12-inch "
|
|
"record with a hypnotic red label eye, groove lines rippling across "
|
|
"its wet surface, room-filling and looming.",
|
|
"Mildred": "Mildred is a fat gray-striped tabby store cat with half-closed "
|
|
"judgmental eyes and a white chest, utterly indifferent.",
|
|
"The Boring Guy": "The Boring Guy is a beige man in his 50s: tucked-in taupe "
|
|
"polo, pleated khakis, gray combover, lanyard with an unreadable "
|
|
"badge, reusable coffee cup, entirely unremarkable face, oblivious to "
|
|
"any catastrophe.",
|
|
"The Name Dropper": "The Name Dropper is a smug man in an expensive vintage "
|
|
"leather jacket over a deadstock-clean band t-shirt, designer "
|
|
"glasses, precisely groomed stubble, phone always in one hand.",
|
|
"The Postman": "The Postman is a postal worker in an immaculate uniform: "
|
|
"pressed shorts, high socks, mirrored aviator sunglasses, an enormous "
|
|
"parcel-laden satchel, granite jaw, terrifying procedural calm.",
|
|
"The Accountant": "The Accountant is a gaunt figure in a slightly-too-tight "
|
|
"gray suit, green banker's visor, sleeve garters, round glasses "
|
|
"catching the light so his eyes are never visible, leather ledger "
|
|
"under one arm, red pen behind his ear.",
|
|
}
|
|
ALIAS = {"The Name-Dropper": "The Name Dropper"} # sheet spelling -> Flow card name
|
|
|
|
|
|
def parse():
|
|
shots = [] # (id, chars tuple, quality bool, prompt)
|
|
for name in SHEETS:
|
|
text = (ROOT / f"production/flow/{name}_SHOTS.md").read_text()
|
|
for m in re.finditer(r"^## (.+?)\n(.*?)(?=^## |\Z)", text, re.M | re.S):
|
|
header, body = m.group(1), m.group(2)
|
|
sid = header.split()[0]
|
|
if sid in SKIP:
|
|
continue
|
|
quality = "QUALITY" in header
|
|
am = re.search(r"\[attach: ([^\]]+)\]", header) or \
|
|
re.search(r"\[([^\]]+)\]", header)
|
|
chars = () if not am or am.group(1) == "no characters" else \
|
|
tuple(ALIAS.get(c.strip(), c.strip()) for c in am.group(1).split(","))
|
|
for c in chars:
|
|
assert c in CHAR_LOCK, f"{sid}: unknown character {c!r}"
|
|
lines = [l.strip() for l in body.strip().splitlines() if l.strip()]
|
|
if lines and lines[0].startswith("(") and lines[0].endswith(")"):
|
|
lines = lines[1:] # production note, not paste text
|
|
prompt = re.sub(r"\s+", " ", " ".join(lines)).strip()
|
|
if len(prompt) < 80: # header wrap fragments / non-prompt sections
|
|
print(f" !! skipping {sid}: body too short ({len(prompt)} ch)")
|
|
continue
|
|
lock = " ".join(CHAR_LOCK[c] for c in chars)
|
|
shots.append((sid, chars, quality, (lock + " " + prompt).strip()))
|
|
return shots
|
|
|
|
|
|
def main():
|
|
shots = parse()
|
|
BANKS.mkdir(parents=True, exist_ok=True)
|
|
for old in BANKS.glob("*.jsonl"):
|
|
old.unlink()
|
|
banks = [("01__fast.jsonl", "Veo 3.1 - Fast", [s for s in shots if not s[2]]),
|
|
("02__quality.jsonl", "Veo 3.1 - Quality", [s for s in shots if s[2]])]
|
|
for fname, model, rows in banks:
|
|
with open(BANKS / fname, "w") as f:
|
|
for sid, chars, _, prompt in rows:
|
|
for take in range(1, TAKES + 1):
|
|
f.write(json.dumps({
|
|
"id": f"rsg_{sid}__t{take}", "kind": "video", "model": model,
|
|
"title": f"{sid} take {take}", "prompt": prompt,
|
|
"characters": list(chars), "category": "rsg"}) + "\n")
|
|
gens = len(rows) * TAKES
|
|
cr = gens * (100 if "Quality" in model else 10)
|
|
print(f"{fname}: {len(rows)} shots x{TAKES} = {gens} gens ~{cr} cr")
|
|
if len(sys.argv) > 2 and sys.argv[1] == "--enqueue":
|
|
bank = sorted(BANKS.glob(f"{int(sys.argv[2]):02d}__*.jsonl"))[0]
|
|
for line in bank.read_text().splitlines():
|
|
req = urllib.request.Request(SERVER + "/enqueue", line.encode(),
|
|
{"Content-Type": "application/json"})
|
|
urllib.request.urlopen(req, timeout=5).read()
|
|
print(f"enqueued {bank.name}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|