File size: 7,802 Bytes
e85709c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2aab769
e85709c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#!/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('<div class="studio-frame">'
                '<iframe src="/studio/studio.html" style="width: 100%; height: 860px; border: 0; border-radius: 8px;"></iframe></div>')
    with gr.Tab("Layer from image (CPU, verified)"):
        with gr.Row():
            with gr.Column():
                in_img = gr.Image(type="pil", label="image")
                in_na = gr.Slider(32, MAX_ATOMS, 140, step=4, label="atom budget")
                in_it = gr.Slider(100, MAX_ITERS_CPU, 400, step=50, label="fit iterations")
                btn_i = gr.Button("Fit layer", variant="primary")
            with gr.Column():
                out_prev_i = gr.Image(label="layer preview (atoms only)")
                out_led_i = gr.Textbox(label="ledger", lines=8)
                out_json_i = gr.Textbox(label="layer JSON (paste into the Studio)",
                                        lines=4, max_lines=4)
        btn_i.click(ui_from_image, [in_img, in_na, in_it],
                    [out_prev_i, out_led_i, out_json_i])
    with gr.Tab("Layer from text (GPU, untested)"):
        gr.Markdown(
            "Score-distillation of a fresh atom population against Stable "
            "Diffusion 2.1 — a line-for-line adaptation of the Bet-5 SDS "
            "loop that was verified on GPU, but **this exact function has "
            "not been executed yet**; the first run is a smoke test. On CPU "
            "hardware this tab refuses honestly. Known carried-over risk: "
            "SD2.1 mode-seeking oversaturation at high CFG.")
        with gr.Row():
            with gr.Column():
                in_pr = gr.Textbox(label="prompt", placeholder="a red tractor, side view, flat background")
                in_ng = gr.Textbox(label="negative prompt", value="blurry, low quality, deformed")
                in_na2 = gr.Slider(32, MAX_ATOMS, 192, step=4, label="atom budget")
                in_it2 = gr.Slider(100, MAX_ITERS_GPU, 900, step=50, label="SDS iterations")
                in_cfg = gr.Slider(5, 60, 30, step=1, label="CFG")
                btn_t = gr.Button("Distill layer", variant="primary")
            with gr.Column():
                out_prev_t = gr.Image(label="layer preview (atoms only)")
                out_led_t = gr.Textbox(label="ledger", lines=8)
                out_json_t = gr.Textbox(label="layer JSON (paste into the Studio)",
                                        lines=4, max_lines=4)
        btn_t.click(ui_from_text, [in_pr, in_ng, in_na2, in_it2, in_cfg],
                    [out_prev_t, out_led_t, out_json_t])

    # API-only endpoints for the studio client (string in/out, no FileData)
    api_b64_in = gr.Textbox(visible=False)
    api_na = gr.Number(visible=False, value=140)
    api_it = gr.Number(visible=False, value=400)
    api_out = gr.Textbox(visible=False)
    gr.Button(visible=False).click(layer_from_image_b64,
                                   [api_b64_in, api_na, api_it], api_out,
                                   api_name="layer_from_image_b64")
    api_pr = gr.Textbox(visible=False)
    api_ng = gr.Textbox(visible=False)
    api_na2 = gr.Number(visible=False, value=192)
    api_it2 = gr.Number(visible=False, value=900)
    api_cfg = gr.Number(visible=False, value=30)
    api_out2 = gr.Textbox(visible=False)
    gr.Button(visible=False).click(layer_from_text,
                                   [api_pr, api_ng, api_na2, api_it2, api_cfg],
                                   api_out2, api_name="layer_from_text")

# ---------------- FastAPI mount: static studio + gradio ---------------------
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()
app.mount("/studio", StaticFiles(directory=os.path.join(
    os.path.dirname(__file__), "studio")), name="studio")
app = gr.mount_gradio_app(app, demo, path="/")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=7860)