flow-extension/local_server_example.py
type-two 84fa103125 docs: correct 3cff97d — the 'ratelimited' bug was not real
I claimed done_ids() was binning rate-limited tasks forever. It wasn't:
/task-completed has always refused to persist 'ratelimited' and 'requeue'
rows (they stay PENDING by design), so such an id can never reach
results.jsonl. Verified against the copy that was actually running.

The done_ids() guard stays as defence in depth against a hand-appended row,
but its comment no longer claims to fix a bug that didn't exist. The real
bug in that commit — no circuit breaker on a failure streak — stands, as
does the actual root cause in 82d5ff4 (promptEchoed matched a 60-char prefix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 19:09:22 +10:00

190 lines
7.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Flow Auto-Pilot local queue.
Source of work: queue.jsonl — one task per line: {"prompt": "...", "kind": "video"|"image", "model": "Veo 3.1 - Fast"}
(id optional; auto-assigned by line number)
Output: results.jsonl — one record per finished task: {id, status, assets:[urls], error, ts}
Behaviour:
* /next-task -> hands out the next PENDING task, video-first (rinse the pricey credits first).
* /task-completed -> appends to results.jsonl, marks done.
* On startup, tasks already in results.jsonl are skipped -> safe to restart during a month-long run.
Harvested `assets` are googleusercontent URLs; they need your logged-in session to download,
so this just records them. ponytail: fetch+save the blobs from the extension later if you want local files.
"""
import base64, json, os, time
from http.server import HTTPServer, BaseHTTPRequestHandler
HERE = os.path.dirname(os.path.abspath(__file__))
QUEUE_FILE = os.path.join(HERE, "queue.jsonl")
RESULTS_FILE = os.path.join(HERE, "results.jsonl")
DOWNLOADS = os.path.join(HERE, "downloads")
EXT = {"video/mp4": "mp4", "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/gif": "gif"}
def save_asset(rec):
"""Write downloaded bytes to downloads/<category>/<id>_<idx>.<ext> (+ dest copy)."""
data = base64.b64decode(rec.get("dataBase64", ""))
ct = (rec.get("contentType") or "").split(";")[0].strip()
ext = EXT.get(ct, "mp4" if "video" in ct else "png")
name = f'{rec.get("id", "asset")}_{rec.get("idx", 0)}.{ext}'
outdir = os.path.join(DOWNLOADS, rec.get("category") or "misc")
os.makedirs(outdir, exist_ok=True)
path = os.path.join(outdir, name)
with open(path, "wb") as f:
f.write(data)
dest = rec.get("dest") # game asked for a copy in its own folder
if dest:
os.makedirs(dest, exist_ok=True)
with open(os.path.join(dest, name), "wb") as f:
f.write(data)
return path
def enqueue_task(rec):
"""Append a game-requested task to the queue (persisted, picked up on next poll)."""
t = {
"id": rec.get("id") or f'req_{int(time.time() * 1000)}',
"kind": rec.get("kind", "image"),
"model": rec.get("model") or ("Veo 3.1 - Fast" if rec.get("kind") == "video" else "Nano Banana 2"),
"prompt": rec["prompt"],
"category": rec.get("category", "request"),
}
if rec.get("dest"):
t["dest"] = rec["dest"]
with open(QUEUE_FILE, "a") as f:
f.write(json.dumps(t) + "\n")
return t
# Fallback demo queue if queue.jsonl is absent (so it runs out of the box).
DEMO = [
{"prompt": "Cinematic synthwave keyboard warrior fighting digital demons, 8k", "kind": "video", "model": "Veo 3.1 - Fast"},
{"prompt": "Op-shop record crate, warm 90s film grain, product hero shot", "kind": "image", "model": "Nano Banana 2"},
]
def load_queue():
rows = []
if os.path.exists(QUEUE_FILE):
with open(QUEUE_FILE) as f:
for line in f:
line = line.strip()
if line:
rows.append(json.loads(line))
else:
rows = list(DEMO)
# assign stable ids by position; video first so the expensive credits burn first
for i, r in enumerate(rows):
r.setdefault("id", f"task_{i:05d}")
r.setdefault("kind", "image")
rows.sort(key=lambda r: 0 if r.get("kind") == "video" else 1)
return rows
def done_ids():
"""Ids that must NOT be handed out again.
'failed' IS counted: a timeout may have burned credits invisibly, so nothing is retried
automatically. To re-run a failed task, delete its line from results.jsonl (requeue.py).
'ratelimited'/'requeue' are skipped belt-and-braces: /task-completed already refuses to
persist those rows (they stay PENDING by design), so they can't reach this file. This is
defence in depth against someone appending a row by hand — not a bug fix.
"""
ids = set()
if os.path.exists(RESULTS_FILE):
with open(RESULTS_FILE) as f:
for line in f:
line = line.strip()
if line:
row = json.loads(line)
if row.get("status") != "ratelimited":
ids.add(row.get("id"))
return ids
class Handler(BaseHTTPRequestHandler):
def _send(self, code=200, body=None):
self.send_response(code)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.send_header("Content-Type", "application/json")
self.end_headers()
if body is not None:
self.wfile.write(json.dumps(body).encode())
def do_OPTIONS(self):
self._send(200, {})
def do_GET(self):
if self.path != "/next-task":
return self._send(404, {"error": "not found"})
done = done_ids()
for t in load_queue():
# in-flight entries expire after 15 min: a hard-refreshed tab that never
# reported back must get its task re-served, not stranded
if t["id"] not in done and (t["id"] not in INFLIGHT or time.time() - INFLIGHT[t["id"]] > 900):
INFLIGHT[t["id"]] = time.time()
remaining = sum(1 for x in load_queue() if x["id"] not in done)
print(f"[SERVER] -> {t['id']} ({t.get('kind')}) | {remaining} left")
return self._send(200, t)
return self._send(204) # queue drained
def do_POST(self):
n = int(self.headers.get("Content-Length", 0))
rec = json.loads(self.rfile.read(n).decode() or "{}")
if self.path == "/task-completed":
INFLIGHT.pop(rec.get("id"), None)
if rec.get("status") not in ("ratelimited", "requeue"): # both stay PENDING, retried after cooldown/reload
rec["ts"] = time.time()
with open(RESULTS_FILE, "a") as f:
f.write(json.dumps(rec) + "\n")
print(f"[SERVER] <- {rec.get('id')} {str(rec.get('status')).upper()} | {len(rec.get('assets') or [])} asset(s)")
return self._send(200, {"ok": True})
if self.path == "/save":
path = save_asset(rec)
print(f"[SERVER] saved {rec.get('id')} -> {path}")
return self._send(200, {"ok": True, "path": path})
if self.path == "/enqueue":
t = enqueue_task(rec)
print(f"[SERVER] enqueued {t['id']} ({t['kind']})")
return self._send(200, {"ok": True, "id": t["id"]})
return self._send(404, {"error": "not found"})
def log_message(self, *a): # quiet the default noise
pass
INFLIGHT = {}
def selftest():
# video-first ordering + resume-skip
global load_queue
_orig = load_queue
load_queue = lambda: [ # noqa: E731
{"id": "a", "kind": "image"}, {"id": "b", "kind": "video"}]
assert [t["id"] for t in load_queue()] == ["a", "b"] # loader keeps as-is here
q = sorted(load_queue(), key=lambda r: 0 if r["kind"] == "video" else 1)
assert q[0]["id"] == "b", "video must be handed out first"
load_queue = _orig
print("selftest ok")
def run(port=8017):
print(f"--- Flow queue on http://localhost:{port} ---")
print(f"queue: {QUEUE_FILE if os.path.exists(QUEUE_FILE) else '(demo — no queue.jsonl)'}")
print(f"pending: {sum(1 for t in load_queue() if t['id'] not in done_ids())}")
HTTPServer(("", port), Handler).serve_forever()
if __name__ == "__main__":
import sys
if "--selftest" in sys.argv:
selftest()
else:
try:
run()
except KeyboardInterrupt:
print("\nstopped")