diff --git a/production/flow/AUTOPILOT_RUNBOOK.md b/production/flow/AUTOPILOT_RUNBOOK.md index 62d8477..a0e40cb 100644 --- a/production/flow/AUTOPILOT_RUNBOOK.md +++ b/production/flow/AUTOPILOT_RUNBOOK.md @@ -1,39 +1,49 @@ -# RSG × Flow Auto-Pilot runbook (2026-07-20) +# RSG × Flow Auto-Pilot runbook (v2 — 2026-07-20, attach automated) -The sheets are compiled into 11 prompt banks at `~/Documents/flowext/banks/rsg/` -(built by `tools/make_flow_bank.py` — rerun it after editing any sheet). Each -bank = one (model tier, attach-set) combo, 2 takes per shot, ~4,160 credits -total. The extension types prompt text only, so per batch YOU set the composer -state once; then it drains unattended. +The extension now does the character dance itself: for each task it clicks the +composer "+", opens the Characters tab, picks the task's saved cards (by the +names in the project: Dez, The Dead Wax, Mildred, The Boring Guy, The Name +Dropper, The Postman, The Accountant), clicks "Add to Prompt" — every step +trusted CDP click, verified, retried 3×, and the task requeues on a hard fail. +Prompts also embed each character's visual description as attach insurance. -## Per-batch loop -1. Brave → the RECORD STORE GUY Flow project (not djsim/shop!). -2. Composer: attach the bank's characters (in the bank filename), model per - tier — `fast__*` = Veo 3.1 Fast, `quality__*` = Veo 3.1 Quality — outputs 1x, - Agent settings "Confirm before generating: Never". -3. Enqueue the bank (N = the number prefix): - `python3 ~/Documents/recordstoreguy/tools/make_flow_bank.py --enqueue N` -4. Extension popup: tick Auto-start. Walk away. Downloads land in - `~/Downloads/flowrinse/rsg/`. -5. Between banks: swap attachments/model, enqueue the next N. +Banks: `tools/make_flow_bank.py` → `~/Documents/flowext/banks/rsg/` + 01__fast.jsonl 68 shots ×1 take (~680 cr) — **already enqueued** + 02__quality.jsonl 14 shots ×1 take (~1400 cr) — enqueue after the fast pass +One take per shot on pass one; review keepers, then `flowext/requeue.py` or +re-enqueue for extra takes. -## Bank order (biggest first) -01 fast Dez ×120 gens · 02 fast Dez+NameDropper ×6 · 03 fast Dez+BoringGuy ×4 · -04 fast DeadWax ×2 · 05 fast no-chars ×2 · 06 fast BoringGuy ×2 · -07 QUALITY Dez+DeadWax ×14 · 08 QUALITY Dez ×8 · 09 QUALITY Mildred ×2 · -10 QUALITY Dez+Postman ×2 · 11 QUALITY Dez+Accountant ×2 +## To start the fast pass (one-time, ~60 seconds) +1. brave://extensions → reload **Flow Auto-Pilot** (loads the attach patch). +2. The RECORD STORE GUY Flow tab: hard refresh (clears any leftover chips), + composer settings: model **Veo 3.1 Fast**, outputs **1x**, Agent + "Confirm before generating" = **Never**. +3. Extension popup → Start (or tick Auto-start). Watch the log for the first + task: you should see `Attaching character card "Dez"…` → `Attached` → + prompt typed → submitted. If the attach loops 3 hard refreshes it marks the + task failed and moves on — check the popup log / results.jsonl. + +Downloads land in `~/Downloads/flowrinse/rsg/`; copy keepers to +`flow-vids-RSG/`, then `tools/postprocess.sh ` → +`engine/clips/` (filenames already match scenes.json), tune in `?debug=1`. + +## Quality pass (after reviewing the fast pass) +1. Composer: model **Veo 3.1 Quality**, still 1x. +2. `python3 ~/Documents/recordstoreguy/tools/make_flow_bank.py --enqueue 2` +3. Start the extension again. 14 shots: boss volleys, win, stinger, postage + executions, needle-drop alts. ## Gotchas -- 96 stale djsim wardrobe IMAGE tasks are still pending in the live queue. - Videos are served first so RSG preempts them, but when the videos drain the - images will fire into whatever tab is open — untick Auto-start when the rsg - category stops growing, or strip them from - `~/Library/Application Support/flowrinse/queue.jsonl` (backup first; ids not - in results.jsonl are the pending ones). They're also stashed-by-copy at - `~/Documents/flowext/banks/djsim_wardrobe_pending_20260720.jsonl`. -- Keepers: postprocess into the engine — - `tools/postprocess.sh ` → `engine/clips/`, - then tune windows in `?debug=1`. Clip filenames already match scenes.json. +- ~92 stale djsim/shop IMAGE tasks are still pending in the live queue. Videos + are served first, so all RSG runs before any of them — but when the videos + drain, those images will fire into the RSG tab. **Stop the extension when the + rsg downloads stop growing**, or clean them: stop the server, delete non-rsg + pending lines from `~/Library/Application Support/flowrinse/queue.jsonl` + (anything whose id isn't in results.jsonl), restart. +- Server was patched (characters passthrough) + restarted 2026-07-20 ~17:00; + plist FLOW_URL now points at the RECORD STORE GUY project (0627b7cd…), so the + 10-min self-heal reopens the right project if the tab ever dies. +- The two "Failed" tiles from hand-testing are Flow-side fails — the queue + versions of those shots will regenerate them. - mf1 prompts end with a different lettering clause on purpose (the letters are - the hazard). Everything else ends with the standard no-lettering sentence. -- Log keepers in production/SHOTLOG.csv per the sheets. + the hazard). Log keepers in production/SHOTLOG.csv. diff --git a/tools/make_flow_bank.py b/tools/make_flow_bank.py index 9f325f6..e87f319 100644 --- a/tools/make_flow_bank.py +++ b/tools/make_flow_bank.py @@ -1,15 +1,17 @@ #!/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. +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 number N to localhost:8017/enqueue + make_flow_bank.py --enqueue N # POST bank N's tasks to localhost:8017/enqueue """ import json import re @@ -22,9 +24,41 @@ 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 +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) @@ -39,7 +73,9 @@ def parse(): 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(",")) + 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 @@ -47,39 +83,29 @@ def parse(): 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)) + lock = " ".join(CHAR_LOCK[c] for c in chars) + shots.append((sid, chars, quality, (lock + " " + prompt).strip())) 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]: + 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, - "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'}]") + "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():