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:
parent
6a7abe1c50
commit
9b22a24b75
@ -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.
|
|
||||||
|
|||||||
@ -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():
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user