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>
91 lines
4.0 KiB
Python
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()
|