Auto-attach production: banks carry characters + embedded visual locks

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>
This commit is contained in:
type-two 2026-07-20 17:00:33 +10:00
parent 6a7abe1c50
commit 9b22a24b75
2 changed files with 98 additions and 62 deletions

View File

@ -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 <raw> <shot_id> <action|death>`
`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 <raw> <shot_id> <action|death>``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.

View File

@ -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():