guts/pipeline/shot_sink.py
jing efea7d98f3 [lane D] Round 1: first texture pack + audio proof, exact-tiling pipeline
Textures (6 walls + normals + wet matcap, $0, ~64s on the m3ultra MPS):
- FLUX ignores "seamless tiling" (seam_error 3.7-17.9); a 4-way cosine
  partition-of-unity blend of the four half-rolls fixes it exactly -> 0.87-1.32
- levels-normalised the pack to one exposure (means were 0.18-0.32); these are
  detail maps multiplied into a biome tint, so per-prompt exposure read as a
  broken tint rather than as dark tissue
- prompt-kit experiment rejected + recorded: PROCITY's "uniform coverage" clause
  in the STEM cured centred subjects but flattened the pack; framing words now
  live only in the two subject prompts that needed them

Audio (1 bed + 4 sfx, 4.2s render, 0.61/10MB, ogg+m4a dual-ship):
- bed-esophagus 24.000s, loop seam 0.32 (wrap step 3x smaller than inner step)
- cascaded lowpass poles: one-pole at 900Hz left 16kHz only ~25dB down = hiss
- rewrote the spectrogram sheet (per-clip peak, log freq) — the first one was a
  saturated rectangle, and evidence you can't read is not evidence

assets.js: miss ledger + misses() — Lane A found that a drifted slug falls back
procedurally forever with no error. Each distinct miss now announces itself.

Also: shot_sink.py (canvas -> docs/shots/laneD, correctly named) and a dev
texture viewer that imports the stub world read-only for the eyeball law.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:17:17 +10:00

91 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""Lane D shot sink — write canvas frames from a dev page straight into docs/shots/laneD/.
Why: the eyeball law wants in-engine PNGs committed as evidence every round, but `DBG.shot()`
relies on an <a download> click, and in a headless/embedded browser pane those land as unnamed
half-written temp blobs in ~/Downloads. Naming evidence by guessing at file sizes is not
evidence. So: the page POSTs the canvas here and the file lands with the right name, first try.
python3 pipeline/shot_sink.py & # listens on :8141, writes docs/shots/laneD/
python3 pipeline/shot_sink.py --dir docs/shots/laneD --port 8141
From the page (any origin — CORS is wide open, it's a localhost dev tool):
canvas.toBlob(b => fetch('http://localhost:8141/shot/my_name.png', {method:'POST', body:b}));
Stdlib only. Refuses anything that isn't a PNG, and sanitises the name to a bare basename so a
page can't post its way out of the shots directory.
"""
import argparse, os, re, sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
OUTDIR = os.path.join(ROOT, "docs", "shots", "laneD")
class Sink(BaseHTTPRequestHandler):
def _cors(self):
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "*")
def do_OPTIONS(self):
self.send_response(204)
self._cors()
self.end_headers()
def _read_body(self):
"""Content-Length when present, else chunked. fetch(body: Blob) normally sends a
Content-Length, but some embedded browsers stream it chunked instead — and
read(Content-Length or 0) silently returns b"" for those, which looks exactly like
"not a PNG". Handle both rather than rejecting a perfectly good frame."""
cl = self.headers.get("Content-Length")
if cl is not None:
return self.rfile.read(int(cl))
if (self.headers.get("Transfer-Encoding") or "").lower() != "chunked":
return b""
buf = bytearray()
while True:
n = int(self.rfile.readline().split(b";")[0] or b"0", 16)
if n == 0:
self.rfile.readline() # trailing CRLF
return bytes(buf)
buf += self.rfile.read(n)
self.rfile.readline() # CRLF after each chunk
def do_POST(self):
m = re.match(r"^/shot/([\w.-]+)$", self.path)
if not m:
self.send_response(404); self._cors(); self.end_headers(); return
name = os.path.basename(m.group(1))
if not name.endswith(".png"):
name += ".png"
body = self._read_body()
if not body.startswith(PNG_MAGIC): # only ever write real PNGs
print(f" reject {name}: {len(body)}B, head={body[:8]!r}, "
f"cl={self.headers.get('Content-Length')!r}, "
f"te={self.headers.get('Transfer-Encoding')!r}", flush=True)
self.send_response(415); self._cors(); self.end_headers()
self.wfile.write(b"not a png"); return
path = os.path.join(OUTDIR, name)
with open(path, "wb") as f:
f.write(body)
print(f" shot {name} {len(body)//1024}KB -> {os.path.relpath(path, ROOT)}", flush=True)
self.send_response(200); self._cors(); self.end_headers()
self.wfile.write(b"ok")
def log_message(self, *a): # the prints above are the only log we want
pass
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=8141)
ap.add_argument("--dir", default=OUTDIR)
a = ap.parse_args()
OUTDIR = os.path.abspath(a.dir)
os.makedirs(OUTDIR, exist_ok=True)
print(f"shot sink: POST http://localhost:{a.port}/shot/<name>.png -> {OUTDIR}", flush=True)
ThreadingHTTPServer(("127.0.0.1", a.port), Sink).serve_forever()