recordstoreguy/tools/make_flow_bank.py
type-two 91930f820b Flow Auto-Pilot banks: compile sheets into 11 attach-grouped prompt banks + runbook
82 remaining shots x2 takes = 164 gens (~4,160 cr of 9,080). Banks live in
~/Documents/flowext/banks/rsg/, grouped by (tier, attach-set) since the
extension types text only — composer attachments/model set once per batch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 05:07:22 +10:00

94 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Compile production/flow/*_SHOTS.md into Flow Auto-Pilot prompt banks.
The flowext extension types prompt TEXT only — attached characters and the
model/output-count are composer state set by hand once per batch. So shots are
grouped into one bank per (tier, attach-set); John opens the RSG Flow project,
attaches that bank's characters, sets the model (Fast/Quality) and 1x, then the
bank is enqueued and drains unattended. 2 takes per shot per the spend doctrine.
Usage:
make_flow_bank.py # write banks to ~/Documents/flowext/banks/rsg/
make_flow_bank.py --enqueue N # POST bank number N 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 = 2
SERVER = "http://localhost:8017"
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(c.strip() for c in am.group(1).split(","))
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
shots.append((sid, chars, quality, prompt))
return shots
def slug(chars):
return "+".join(re.sub(r"^the ", "", c.lower()).replace(" ", "") for c in chars) or "nochars"
def main():
shots = parse()
groups = {} # (quality, chars) -> [shots] — keep sheet order within a group
for s in shots:
groups.setdefault((s[2], s[1]), []).append(s)
# fast banks first (bulk of the game), quality after; big groups first
order = sorted(groups, key=lambda k: (k[0], -len(groups[k])))
BANKS.mkdir(parents=True, exist_ok=True)
for old in BANKS.glob("*.jsonl"):
old.unlink()
for n, key in enumerate(order, 1):
quality, chars = key
model = "Veo 3.1 - Quality" if quality else "Veo 3.1 - Fast"
path = BANKS / f"{n:02d}__{'quality' if quality else 'fast'}__{slug(chars)}.jsonl"
with open(path, "w") as f:
for sid, _, _, prompt in groups[key]:
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,
"category": "rsg"}) + "\n")
gens = len(groups[key]) * TAKES
cr = gens * (100 if quality else 10)
print(f"{path.name}: {len(groups[key])} shots x{TAKES} = {gens} gens ~{cr} cr"
f" [attach: {', '.join(chars) or 'none'}]")
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()