Spaces:
Runtime error
Runtime error
File size: 10,034 Bytes
e992d9f 96abce9 e992d9f 7e1ff4b e992d9f 96abce9 e992d9f 96abce9 e992d9f | 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 | """WASD-controlled push_cube world model (AR shortcut-forcing DiT-S, 5-step inference).
HF Space edition: loads a stripped fp16 checkpoint + Wan-VAE from the repo, initial frames from
frames/*.png. Same stdlib HTTP server + HTML page as the cluster demo. CPU or GPU (auto).
"""
import os, sys, io, json, time, threading, socket, base64, glob
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import numpy as np
import torch
import yaml
ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT)
# weights live in a separate public model repo (Space repo has a 1 GB cap)
from huggingface_hub import hf_hub_download
WREPO = "lamamkh/push-cube-wasd-worldmodel-weights"
CKPT_PATH = hf_hub_download(WREPO, "model_fp16.pt")
VAE_PATH = hf_hub_download(WREPO, "Wan2.1_VAE.pth")
os.environ["WAN_VAE_PATH"] = VAE_PATH
from acwm.model.interface import get_dynamics_class
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
if DEVICE == "cpu":
torch.set_num_threads(max(1, os.cpu_count() or 2))
PORT = int(os.environ.get("PORT", "7860"))
K_STEPS = 5
MAX_LATENT = 10
print(f"[wasd] device={DEVICE}", flush=True)
cfg = yaml.safe_load(open(os.path.join(ROOT, "push_cube_ar.yaml")))
mc = cfg["model_config"]; mc["action_dim"] = 2
mc["use_flash_attn"] = torch.cuda.is_available()
WM = get_dynamics_class(cfg["dynamics_class"])(cfg["model_name"], mc).to(DEVICE).eval()
_ck = torch.load(CKPT_PATH, map_location="cpu", weights_only=False)
_sd = {k: (v.float() if v.is_floating_point() else v) for k, v in _ck["model_state_dict"].items()}
WM.load_state_dict(_sd, strict=False)
WM = WM.float()
print(f"[wasd] loaded step={_ck.get('step','?')}", flush=True)
ACR = WM.model.action_compress_rate
KMAX = WM.k_max
FRAMES = sorted(glob.glob(os.path.join(ROOT, "frames", "*.png")))
def load_frame(idx):
import imageio.v2 as iio
img = iio.imread(FRAMES[idx % len(FRAMES)]).astype(np.float32) / 255.0
return torch.from_numpy(img) # [H,W,3]
@torch.no_grad()
def ar_one_frame(z_all, a_curr):
h, w, D = z_all.shape[2], z_all.shape[3], z_all.shape[4]
t_len = z_all.shape[1] + 1
K = K_STEPS; d = 1.0 / K; d_min = 1.0 / KMAX
z = torch.randn(1, 1, h, w, D, device=DEVICE)
for i in range(K):
seq = torch.cat([z_all, z], dim=1)
sig = torch.ones(1, t_len, device=DEVICE); sig[:, -1] = i / K
ds = torch.full((1, t_len), d_min, device=DEVICE); ds[:, -1] = d
b = WM.model(seq, sig, ds, a_curr)
z = z + b[:, -1:] * d
return z
@torch.no_grad()
def decode_last(z_all):
vid = WM.vae.decode_to_pixel(z_all.permute(0, 1, 4, 2, 3).contiguous())
vid = ((vid + 1) / 2).clamp(0, 1)[0].permute(0, 2, 3, 1).cpu().numpy()
return vid[-1]
def png_b64(img01):
import imageio.v2 as iio
buf = io.BytesIO()
iio.imwrite(buf, (np.clip(img01, 0, 1) * 255).astype(np.uint8), format="png")
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
LOCK = threading.Lock()
S = {"z": None, "acts": []}
@torch.no_grad()
def reset(idx):
f0 = load_frame(int(idx))
o0 = f0.unsqueeze(0).to(DEVICE) # [1,H,W,3]
S["z"] = WM.encode_obs(o0.unsqueeze(1)); S["acts"] = []
return png_b64(f0.numpy()), 1
@torch.no_grad()
def step(dx, dy):
v = np.array([dx, dy], np.float32); n = float(np.linalg.norm(v))
a = v / n if n > 1e-6 else np.array([1.0, 0.0], np.float32)
S["acts"].extend([a] * ((ACR + 1) if len(S["acts"]) == 0 else ACR))
a_curr = torch.tensor(np.stack(S["acts"]), device=DEVICE).unsqueeze(0).float()
S["z"] = torch.cat([S["z"], ar_one_frame(S["z"], a_curr)], dim=1)
if S["z"].shape[1] > MAX_LATENT:
S["z"] = S["z"][:, 1:]; S["acts"] = S["acts"][ACR:]
return png_b64(decode_last(S["z"])), S["z"].shape[1], [float(a[0]), float(a[1])]
PAGE = """<!doctype html><html><head><meta charset="utf-8"><title>push_cube · WASD world model</title><style>
:root{--bg:#0e0f13;--panel:#16181f;--ink:#ecebe6;--soft:#9aa0ad;--amber:#f59e42;--line:#24262f;
--mono:ui-monospace,Menlo,Consolas,monospace;--sans:ui-sans-serif,system-ui,sans-serif}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);
display:flex;min-height:100vh;align-items:center;justify-content:center}
.app{display:flex;gap:28px;padding:28px;flex-wrap:wrap;justify-content:center}
.stage{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:16px}
#frame{width:min(480px,88vw);aspect-ratio:1;border-radius:8px;display:block;background:#000}
#frame:not([src]){opacity:0}
.hud{font-family:var(--mono);font-size:12.5px;color:var(--soft);margin-top:10px;display:flex;
justify-content:space-between}
.side{max-width:290px;display:flex;flex-direction:column;gap:16px}
h1{font-size:20px;margin:0}p{color:var(--soft);font-size:13.5px;line-height:1.5;margin:0}
.keys{display:grid;grid-template-columns:repeat(3,54px);gap:6px;justify-content:center}
.key{height:54px;border:1px solid var(--line);border-radius:9px;display:flex;align-items:center;
justify-content:center;font-family:var(--mono);font-size:16px;color:var(--soft);background:var(--panel);
transition:all .08s;cursor:pointer;user-select:none}.key.on{background:var(--amber);color:#14161c;border-color:var(--amber)}
select,button{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;
padding:8px 12px;font-family:var(--mono);font-size:13px;cursor:pointer}button:hover{border-color:var(--amber)}
.row{display:flex;gap:8px;align-items:center}.busy #frame{opacity:.7}
#spin{display:none;color:var(--amber)}.busy #spin{display:inline}</style></head><body>
<div class="app" id="app">
<div class="stage"><img id="frame" alt="world model frame">
<div class="hud"><span id="stat">press R or Reset to start</span><span><span id="spin">thinking…</span> <span id="lat"></span></span></div></div>
<div class="side"><h1>Drive the world model</h1>
<p><b style="color:var(--amber)">WASD</b> (or tap the keys) pushes the disk. Each press imagines ~4
frames with 5-step denoising in an AR shortcut DiT — everything is generated, no simulator.
On the free CPU tier a step takes a while; on GPU it's ~0.3 s.</p>
<div class="keys"><span></span><div class="key" data-k="w">W</div><span></span>
<div class="key" data-k="a">A</div><div class="key" data-k="s">S</div><div class="key" data-k="d">D</div></div>
<div class="row"><select id="ep"></select><button id="reset">⟲ Reset (R)</button></div>
<p id="msg"></p></div></div><script>
const $=i=>document.getElementById(i);const DIR={w:[0,-1],s:[0,1],a:[-1,0],d:[1,0]};
let held=new Set(),busy=false,alive=false;
for(let i=0;i<16;i++){const o=document.createElement('option');o.value=i;o.textContent='scene '+i;$('ep').appendChild(o)}
async function post(u,b){try{const r=await fetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});if(!r.ok){const t=await r.text();$('msg').textContent='server error: '+t.slice(0,200);throw new Error(t)}return r.json()}catch(e){$('msg').textContent='request failed: '+e.message;busy=false;$('app').classList.remove('busy');throw e}}
async function doReset(){busy=true;$('app').classList.add('busy');$('stat').textContent='resetting…';
const j=await post('reset',{idx:+$('ep').value});$('frame').src=j.frame;
$('stat').textContent='latent 1 · ready';alive=true;busy=false;$('app').classList.remove('busy')}
async function doStep(dx,dy){if(busy||!alive)return;busy=true;$('app').classList.add('busy');
const t0=performance.now();const j=await post('step',{dx,dy});$('frame').src=j.frame;
$('stat').textContent=`latent ${j.n} · a=[${j.a[0].toFixed(2)},${j.a[1].toFixed(2)}]`;
$('lat').textContent=`${Math.round(performance.now()-t0)} ms`;busy=false;$('app').classList.remove('busy');
if(held.size){let dx2=0,dy2=0;for(const k of held){dx2+=DIR[k][0];dy2+=DIR[k][1]}if(dx2||dy2)doStep(dx2,dy2)}}
addEventListener('keydown',e=>{const k=e.key.toLowerCase();if(k==='r'){doReset();return}
if(DIR[k]&&!held.has(k)){held.add(k);mark();let dx=0,dy=0;for(const kk of held){dx+=DIR[kk][0];dy+=DIR[kk][1]}doStep(dx,dy)}});
addEventListener('keyup',e=>{const k=e.key.toLowerCase();if(DIR[k]){held.delete(k);mark()}});
function mark(){document.querySelectorAll('.key').forEach(el=>el.classList.toggle('on',held.has(el.dataset.k)))}
document.querySelectorAll('.key').forEach(el=>el.onclick=()=>{const d=DIR[el.dataset.k];doStep(d[0],d[1])});
$('reset').onclick=doReset;
doReset();</script></body></html>"""
class H(BaseHTTPRequestHandler):
def log_message(self, *a): pass
def _json(self, obj, code=200):
b = json.dumps(obj).encode()
self.send_response(code); self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)
def do_GET(self):
b = PAGE.encode()
self.send_response(200); self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)
def do_POST(self):
n = int(self.headers.get("Content-Length", 0))
req = json.loads(self.rfile.read(n) or b"{}")
try:
with LOCK:
if self.path.endswith("/reset"):
frame, nlat = reset(req.get("idx", 0)); self._json({"frame": frame, "n": nlat})
elif self.path.endswith("/step"):
if S["z"] is None: self._json({"error": "reset first"}, 400); return
frame, nlat, a = step(float(req.get("dx", 0)), float(req.get("dy", 0)))
self._json({"frame": frame, "n": nlat, "a": a})
else: self._json({"error": "unknown"}, 404)
except Exception as e:
import traceback; traceback.print_exc()
self._json({"error": str(e)}, 500)
if __name__ == "__main__":
print(f"[wasd] serving :{PORT}", flush=True)
ThreadingHTTPServer(("0.0.0.0", PORT), H).serve_forever()
|