#!/usr/bin/env python3 """ Slapstack Studio — HuggingFace Space server. Layout: / Gradio app: the Studio (iframe) + generation tabs + ledger /studio/… static: the single-file interactive client (verified JS BP) /gradio_api/… Gradio REST API, called by the client JS: layer_from_image_b64(png_b64, n_atoms, iters) -> JSON layer_from_text(prompt, negative, n_atoms, iters, cfg) -> JSON Division of labor (the whole point): the SERVER knows what things look like (SD oracle / image fitting), the CLIENT knows what is where and how sure (verified BP in the browser). """ import base64 import io import json import os import gradio as gr import numpy as np from PIL import Image from oracle import fit_image, sds_layer, preview_png_bytes MAX_ATOMS = 256 MAX_ITERS_CPU = 800 MAX_ITERS_GPU = 1500 def _layer_payload(atoms, ledger): png = preview_png_bytes(atoms, 192) return json.dumps({ "atoms": np.asarray(atoms).round(5).tolist(), "preview_png_b64": base64.b64encode(png).decode(), "ledger": ledger, }) # ---------------- endpoints (also used by the studio client JS) ------------- def layer_from_image_b64(png_b64: str, n_atoms: float, iters: float) -> str: """b64 PNG/JPEG -> Gabor layer JSON. CPU path, verified.""" raw = base64.b64decode(png_b64.split(",")[-1]) img = Image.open(io.BytesIO(raw)) n_atoms = int(min(max(n_atoms, 16), MAX_ATOMS)) iters = int(min(max(iters, 50), MAX_ITERS_CPU)) atoms, ledger = fit_image(img, n_atoms=n_atoms, iters=iters) return _layer_payload(atoms, ledger) def layer_from_text(prompt: str, negative: str, n_atoms: float, iters: float, cfg: float) -> str: """text -> Gabor layer JSON via SDS. GPU only; honest error on CPU.""" n_atoms = int(min(max(n_atoms, 32), MAX_ATOMS)) iters = int(min(max(iters, 100), MAX_ITERS_GPU)) atoms, ledger = sds_layer(prompt, negative_prompt=negative or "blurry, low quality, deformed", n_atoms=n_atoms, iters=iters, cfg=float(cfg)) return _layer_payload(atoms, ledger) # ---------------- human-facing wrappers for the Gradio tabs ----------------- def ui_from_image(img, n_atoms, iters): if img is None: raise gr.Error("upload an image first") buf = io.BytesIO() img.save(buf, "PNG") out = layer_from_image_b64(base64.b64encode(buf.getvalue()).decode(), n_atoms, iters) d = json.loads(out) prev = Image.open(io.BytesIO(base64.b64decode(d["preview_png_b64"]))) led = dict(d["ledger"]); led.pop("log", None) return prev, json.dumps(led, indent=2), out def ui_from_text(prompt, negative, n_atoms, iters, cfg): if not (prompt or "").strip(): raise gr.Error("write a prompt first") out = layer_from_text(prompt, negative, n_atoms, iters, cfg) d = json.loads(out) prev = Image.open(io.BytesIO(base64.b64decode(d["preview_png_b64"]))) led = dict(d["ledger"]); led.pop("log", None) return prev, json.dumps(led, indent=2), out CSS = """ .studio-frame iframe { width: 100%; height: 860px; border: 0; border-radius: 8px; } """ with gr.Blocks(title="Slapstack Studio", css=CSS) as demo: gr.Markdown( "# Slapstack Studio\n" "**Generate Gabor-atom layers with AI, then move them, occlude them, " "and watch belief propagation keep track.** Every entity in the " "studio is a posterior: layers are recovered from an unlabeled atom " "soup by BP, a drag is a pose clamp, occlusion honestly widens the " "belief. The interactive engine below is a JS port verified against " "the SlapstackBet6 Python to 2e-16 (transform), 8.6e-8 (render MSE), " "identical BP accuracy.") with gr.Tab("Studio"): gr.HTML('