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/` The extension now does the character dance itself: for each task it clicks the
(built by `tools/make_flow_bank.py` — rerun it after editing any sheet). Each composer "+", opens the Characters tab, picks the task's saved cards (by the
bank = one (model tier, attach-set) combo, 2 takes per shot, ~4,160 credits names in the project: Dez, The Dead Wax, Mildred, The Boring Guy, The Name
total. The extension types prompt text only, so per batch YOU set the composer Dropper, The Postman, The Accountant), clicks "Add to Prompt" — every step
state once; then it drains unattended. 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 Banks: `tools/make_flow_bank.py``~/Documents/flowext/banks/rsg/`
1. Brave → the RECORD STORE GUY Flow project (not djsim/shop!). 01__fast.jsonl 68 shots ×1 take (~680 cr) — **already enqueued**
2. Composer: attach the bank's characters (in the bank filename), model per 02__quality.jsonl 14 shots ×1 take (~1400 cr) — enqueue after the fast pass
tier — `fast__*` = Veo 3.1 Fast, `quality__*` = Veo 3.1 Quality — outputs 1x, One take per shot on pass one; review keepers, then `flowext/requeue.py` or
Agent settings "Confirm before generating: Never". re-enqueue for extra takes.
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.
## Bank order (biggest first) ## To start the fast pass (one-time, ~60 seconds)
01 fast Dez ×120 gens · 02 fast Dez+NameDropper ×6 · 03 fast Dez+BoringGuy ×4 · 1. brave://extensions → reload **Flow Auto-Pilot** (loads the attach patch).
04 fast DeadWax ×2 · 05 fast no-chars ×2 · 06 fast BoringGuy ×2 · 2. The RECORD STORE GUY Flow tab: hard refresh (clears any leftover chips),
07 QUALITY Dez+DeadWax ×14 · 08 QUALITY Dez ×8 · 09 QUALITY Mildred ×2 · composer settings: model **Veo 3.1 Fast**, outputs **1x**, Agent
10 QUALITY Dez+Postman ×2 · 11 QUALITY Dez+Accountant ×2 "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 ## Gotchas
- 96 stale djsim wardrobe IMAGE tasks are still pending in the live queue. - ~92 stale djsim/shop IMAGE tasks are still pending in the live queue. Videos
Videos are served first so RSG preempts them, but when the videos drain the are served first, so all RSG runs before any of them — but when the videos
images will fire into whatever tab is open — untick Auto-start when the rsg drain, those images will fire into the RSG tab. **Stop the extension when the
category stops growing, or strip them from rsg downloads stop growing**, or clean them: stop the server, delete non-rsg
`~/Library/Application Support/flowrinse/queue.jsonl` (backup first; ids not pending lines from `~/Library/Application Support/flowrinse/queue.jsonl`
in results.jsonl are the pending ones). They're also stashed-by-copy at (anything whose id isn't in results.jsonl), restart.
`~/Documents/flowext/banks/djsim_wardrobe_pending_20260720.jsonl`. - Server was patched (characters passthrough) + restarted 2026-07-20 ~17:00;
- Keepers: postprocess into the engine — plist FLOW_URL now points at the RECORD STORE GUY project (0627b7cd…), so the
`tools/postprocess.sh <raw> <shot_id> <action|death>``engine/clips/`, 10-min self-heal reopens the right project if the tab ever dies.
then tune windows in `?debug=1`. Clip filenames already match scenes.json. - 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 - mf1 prompts end with a different lettering clause on purpose (the letters are
the hazard). Everything else ends with the standard no-lettering sentence. the hazard). Log keepers in production/SHOTLOG.csv.
- Log keepers in production/SHOTLOG.csv per the sheets.

View File

@ -1,15 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Compile production/flow/*_SHOTS.md into Flow Auto-Pilot prompt banks. """Compile production/flow/*_SHOTS.md into Flow Auto-Pilot prompt banks.
The flowext extension types prompt TEXT only attached characters and the The flowext extension (v2 + attach patch) attaches each task's saved character
model/output-count are composer state set by hand once per batch. So shots are cards itself (task "characters" field drives the + / Characters / Add-to-Prompt
grouped into one bank per (tier, attach-set); John opens the RSG Flow project, dance), then types the prompt. Each prompt ALSO embeds the characters' visual
attaches that bank's characters, sets the model (Fast/Quality) and 1x, then the descriptions so a silently-failed attach degrades consistency instead of losing
bank is enqueued and drains unattended. 2 takes per shot per the spend doctrine. 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: Usage:
make_flow_bank.py # write banks to ~/Documents/flowext/banks/rsg/ 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 json
import re import re
@ -22,9 +24,41 @@ SHEETS = ["TRACK1", "TRACK2", "TRACK3", "TRACK4", "TRACK5", "TRACK6", "TRACK7",
"SHIFTS", "EXTRAS"] "SHIFTS", "EXTRAS"]
BANKS = Path.home() / "Documents/flowext/banks/rsg" BANKS = Path.home() / "Documents/flowext/banks/rsg"
SKIP = {"Logging", "t1_whitelabel", "attract_loop", "intro_qual"} # done or cut 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" 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(): def parse():
shots = [] # (id, chars tuple, quality bool, prompt) shots = [] # (id, chars tuple, quality bool, prompt)
@ -39,7 +73,9 @@ def parse():
am = re.search(r"\[attach: ([^\]]+)\]", header) or \ am = re.search(r"\[attach: ([^\]]+)\]", header) or \
re.search(r"\[([^\]]+)\]", header) re.search(r"\[([^\]]+)\]", header)
chars = () if not am or am.group(1) == "no characters" else \ 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()] lines = [l.strip() for l in body.strip().splitlines() if l.strip()]
if lines and lines[0].startswith("(") and lines[0].endswith(")"): if lines and lines[0].startswith("(") and lines[0].endswith(")"):
lines = lines[1:] # production note, not paste text lines = lines[1:] # production note, not paste text
@ -47,39 +83,29 @@ def parse():
if len(prompt) < 80: # header wrap fragments / non-prompt sections if len(prompt) < 80: # header wrap fragments / non-prompt sections
print(f" !! skipping {sid}: body too short ({len(prompt)} ch)") print(f" !! skipping {sid}: body too short ({len(prompt)} ch)")
continue 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 return shots
def slug(chars):
return "+".join(re.sub(r"^the ", "", c.lower()).replace(" ", "") for c in chars) or "nochars"
def main(): def main():
shots = parse() 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) BANKS.mkdir(parents=True, exist_ok=True)
for old in BANKS.glob("*.jsonl"): for old in BANKS.glob("*.jsonl"):
old.unlink() old.unlink()
for n, key in enumerate(order, 1): banks = [("01__fast.jsonl", "Veo 3.1 - Fast", [s for s in shots if not s[2]]),
quality, chars = key ("02__quality.jsonl", "Veo 3.1 - Quality", [s for s in shots if s[2]])]
model = "Veo 3.1 - Quality" if quality else "Veo 3.1 - Fast" for fname, model, rows in banks:
path = BANKS / f"{n:02d}__{'quality' if quality else 'fast'}__{slug(chars)}.jsonl" with open(BANKS / fname, "w") as f:
with open(path, "w") as f: for sid, chars, _, prompt in rows:
for sid, _, _, prompt in groups[key]:
for take in range(1, TAKES + 1): for take in range(1, TAKES + 1):
f.write(json.dumps({ f.write(json.dumps({
"id": f"rsg_{sid}__t{take}", "kind": "video", "model": model, "id": f"rsg_{sid}__t{take}", "kind": "video", "model": model,
"title": f"{sid} take {take}", "prompt": prompt, "title": f"{sid} take {take}", "prompt": prompt,
"category": "rsg"}) + "\n") "characters": list(chars), "category": "rsg"}) + "\n")
gens = len(groups[key]) * TAKES gens = len(rows) * TAKES
cr = gens * (100 if quality else 10) cr = gens * (100 if "Quality" in model else 10)
print(f"{path.name}: {len(groups[key])} shots x{TAKES} = {gens} gens ~{cr} cr" print(f"{fname}: {len(rows)} shots x{TAKES} = {gens} gens ~{cr} cr")
f" [attach: {', '.join(chars) or 'none'}]")
if len(sys.argv) > 2 and sys.argv[1] == "--enqueue": if len(sys.argv) > 2 and sys.argv[1] == "--enqueue":
bank = sorted(BANKS.glob(f"{int(sys.argv[2]):02d}__*.jsonl"))[0] bank = sorted(BANKS.glob(f"{int(sys.argv[2]):02d}__*.jsonl"))[0]
for line in bank.read_text().splitlines(): for line in bank.read_text().splitlines():