multimodalart HF Staff commited on
Commit
e88b235
·
verified ·
1 Parent(s): 1fc82bd

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +28 -6
  2. __pycache__/app.cpython-312.pyc +0 -0
  3. app.py +197 -0
  4. index.html +203 -0
  5. requirements.txt +1 -0
README.md CHANGED
@@ -1,13 +1,35 @@
1
  ---
2
  title: StreamDiffusionV2 Realtime
3
- emoji: 🌖
4
- colorFrom: red
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.19.0
8
- python_version: '3.13'
9
  app_file: app.py
 
 
 
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: StreamDiffusionV2 Realtime
3
+ emoji: 🌀
4
+ colorFrom: indigo
5
+ colorTo: pink
6
  sdk: gradio
7
+ sdk_version: 6.10.0
 
8
  app_file: app.py
9
+ python_version: "3.10"
10
+ short_description: Realtime webcam video diffusion (StreamDiffusionV2 / Wan2.1)
11
+ startup_duration_timeout: 1h
12
  pinned: false
13
  ---
14
 
15
+ # StreamDiffusionV2 · Realtime Webcam Diffusion
16
+
17
+ Live ZeroGPU demo of [**StreamDiffusionV2**](https://streamdiffusionv2.github.io/)
18
+ (MLSys 2026 Best Paper) on **Wan2.1-T2V-1.3B**, with a custom `gradio.Server`
19
+ frontend.
20
+
21
+ Unlike a fixed-length generator, StreamDiffusionV2 is **designed for continuous
22
+ streaming**: a causal Diffusion-Transformer with a **sink-token-guided rolling KV
23
+ cache**, a motion-aware noise controller, and StreamVAE. Your webcam is streamed
24
+ through it prompt-by-prompt and the stylized result flows back live, without the
25
+ window-shift burst that fixed-horizon models show.
26
+
27
+ The browser captures the webcam and posts frames to a lightweight FastAPI route;
28
+ a held `@spaces.GPU` session runs StreamDiffusionV2's single-GPU streaming loop
29
+ (`start_stream_session` → `run_stream_batch`) and streams frames back over the
30
+ Gradio JS client, paced by a client jitter buffer.
31
+
32
+ One of three rolling/streaming demos:
33
+ - StreamDiffusionV2 (this) — video-to-video webcam.
34
+ - LongLive — interactive long text-to-video.
35
+ - Rolling Forcing — real-time multi-minute text-to-video.
__pycache__/app.cpython-312.pyc ADDED
Binary file (9.24 kB). View file
 
app.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import base64
4
+ import tempfile
5
+ import queue as pyqueue
6
+ import multiprocessing as mp
7
+ from io import BytesIO
8
+
9
+ import spaces # before torch / CUDA imports
10
+
11
+ import torch
12
+ import numpy as np
13
+ from PIL import Image
14
+ from huggingface_hub import snapshot_download, hf_hub_download
15
+
16
+ from streamdiffusionv2 import StreamDiffusionV2Pipeline
17
+
18
+ # ----------------------------------------------------------------------------
19
+ # Config
20
+ # ----------------------------------------------------------------------------
21
+ WAN_REPO = "Wan-AI/Wan2.1-T2V-1.3B"
22
+ WAN_DIR = "wan_models/Wan2.1-T2V-1.3B"
23
+ SDV2_REPO = "jerryfeng/StreamDiffusionV2"
24
+ CKPT_DIR = "ckpts"
25
+ CKPT_FOLDER = os.path.join(CKPT_DIR, "wan_causal_dmd_v2v") # 1.3B v2v checkpoint
26
+
27
+ HEIGHT, WIDTH = 480, 832
28
+ SESSION_DURATION = 58
29
+ POLL_INTERVAL = 0.005
30
+ DEFAULT_PROMPT = "a psychedelic neon dream, vivid saturated colors, glowing"
31
+ NOISE_SCALE = 0.8
32
+
33
+ SESSION_DIR = tempfile.gettempdir()
34
+ INSTRUCTION_FILE = os.path.join(SESSION_DIR, "sdv2_prompt.txt")
35
+ READY_SENTINEL = "__READY__"
36
+
37
+ # Fork-safe live frame queue (created before any ZeroGPU fork).
38
+ FRAME_Q = mp.get_context("fork").Queue(maxsize=512)
39
+
40
+ # ----------------------------------------------------------------------------
41
+ # Weights + pipeline at module scope (ZeroGPU snapshot preload)
42
+ # ----------------------------------------------------------------------------
43
+ snapshot_download(
44
+ repo_id=WAN_REPO, local_dir=WAN_DIR,
45
+ allow_patterns=[
46
+ "config.json", "diffusion_pytorch_model.safetensors", "Wan2.1_VAE.pth",
47
+ "models_t5_umt5-xxl-enc-bf16.pth", "google/umt5-xxl/*",
48
+ ],
49
+ )
50
+ snapshot_download(repo_id=SDV2_REPO, local_dir=CKPT_DIR,
51
+ allow_patterns=["wan_causal_dmd_v2v/*"])
52
+
53
+ device = torch.device("cuda")
54
+
55
+ # StreamDiffusionV2 single-GPU streaming pipeline (rolling KV + sink tokens are
56
+ # built into the model -> continuous streaming without the window-shift burst).
57
+ stream = StreamDiffusionV2Pipeline(
58
+ checkpoint_folder=CKPT_FOLDER,
59
+ mode="single",
60
+ device=device,
61
+ height=HEIGHT,
62
+ width=WIDTH,
63
+ step=2,
64
+ noise_scale=NOISE_SCALE,
65
+ model_type="T2V-1.3B",
66
+ use_taehv=False,
67
+ )
68
+ PM = stream.pipeline_manager
69
+ CHUNK = PM.base_chunk_size * PM.pipeline.num_frame_per_block # 4 px frames / chunk
70
+ FIRST_BATCH = 1 + CHUNK # 5 px frames first
71
+
72
+
73
+ def _read_prompt():
74
+ try:
75
+ with open(INSTRUCTION_FILE, encoding="utf-8") as f:
76
+ return f.read().strip()
77
+ except FileNotFoundError:
78
+ return ""
79
+
80
+
81
+ def _decode_jpeg_to_tensor(jpeg_bytes):
82
+ """JPEG bytes -> [C, H, W] in [-1, 1]."""
83
+ im = Image.open(BytesIO(jpeg_bytes)).convert("RGB").resize((WIDTH, HEIGHT), Image.BICUBIC)
84
+ arr = torch.from_numpy(np.asarray(im)).float().permute(2, 0, 1) / 255.0
85
+ return arr * 2.0 - 1.0
86
+
87
+
88
+ def _frames_to_video_tensor(frame_list):
89
+ """list of [C,H,W] -> [B, C, T, H, W] bf16 on device."""
90
+ vid = torch.stack(frame_list, dim=1).unsqueeze(0) # [1, C, T, H, W]
91
+ return vid.to(device=device, dtype=torch.bfloat16)
92
+
93
+
94
+ def _to_data_uri(frame01):
95
+ im = Image.fromarray((np.clip(frame01, 0, 1) * 255.0).astype(np.uint8))
96
+ buf = BytesIO()
97
+ im.save(buf, format="JPEG", quality=80)
98
+ return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
99
+
100
+
101
+ # ----------------------------------------------------------------------------
102
+ # Gradio Server
103
+ # ----------------------------------------------------------------------------
104
+ from gradio import Server
105
+ from fastapi import Request
106
+ from fastapi.responses import HTMLResponse
107
+
108
+ app = Server()
109
+
110
+
111
+ @app.api(name="run_session")
112
+ @spaces.GPU(duration=60, size="xlarge")
113
+ @torch.inference_mode()
114
+ def run_session() -> str:
115
+ # Drain stale frames, then signal the client to start streaming.
116
+ try:
117
+ while True:
118
+ FRAME_Q.get_nowait()
119
+ except pyqueue.Empty:
120
+ pass
121
+ yield READY_SENTINEL
122
+
123
+ prompt = _read_prompt() or DEFAULT_PROMPT
124
+ buffer = []
125
+ session = None
126
+ deadline = time.time() + SESSION_DURATION
127
+ last = None
128
+
129
+ while time.time() < deadline:
130
+ drained = 0
131
+ while drained < 256:
132
+ try:
133
+ buffer.append(FRAME_Q.get_nowait())
134
+ except pyqueue.Empty:
135
+ break
136
+ drained += 1
137
+
138
+ need = FIRST_BATCH if session is None else CHUNK
139
+ if len(buffer) < need:
140
+ time.sleep(POLL_INTERVAL)
141
+ continue
142
+
143
+ frames = [_decode_jpeg_to_tensor(b) for b in buffer[:need]]
144
+ buffer = buffer[need:]
145
+ vid = _frames_to_video_tensor(frames)
146
+
147
+ t0 = time.time()
148
+ if session is None:
149
+ session, init_video = PM.start_stream_session(prompt, vid, NOISE_SCALE)
150
+ outs = [init_video]
151
+ else:
152
+ outs = PM.run_stream_batch(session, vid)
153
+ dt = time.time() - t0
154
+
155
+ n = 0
156
+ for arr in outs: # each arr: [T, H, W, C] in [0,1]
157
+ for fr in arr:
158
+ last = _to_data_uri(fr)
159
+ yield last
160
+ n += 1
161
+ if n:
162
+ print(f"[sdv2] {n} frames in {dt:.2f}s ({n/max(1e-3,dt):.1f} fps)", flush=True)
163
+
164
+ if last is not None:
165
+ yield last
166
+
167
+
168
+ @app.post("/frame")
169
+ async def post_frame(request: Request):
170
+ body = await request.body()
171
+ if body:
172
+ try:
173
+ FRAME_Q.put_nowait(body)
174
+ except pyqueue.Full:
175
+ pass
176
+ return {"ok": True}
177
+
178
+
179
+ @app.post("/instruction")
180
+ async def post_instruction(request: Request):
181
+ data = await request.json()
182
+ text = (data.get("instruction", "") or "").strip()
183
+ tmp = INSTRUCTION_FILE + ".tmp"
184
+ with open(tmp, "w", encoding="utf-8") as f:
185
+ f.write(text)
186
+ os.replace(tmp, INSTRUCTION_FILE)
187
+ return {"ok": True}
188
+
189
+
190
+ @app.get("/", response_class=HTMLResponse)
191
+ async def homepage():
192
+ here = os.path.dirname(os.path.abspath(__file__))
193
+ with open(os.path.join(here, "index.html"), encoding="utf-8") as f:
194
+ return f.read()
195
+
196
+
197
+ app.launch(show_error=True)
index.html ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>LiveEdit · Realtime</title>
7
+ <style>
8
+ :root { --bg:#0e0f13; --panel:#171922; --line:#2a2e3a; --fg:#e8e8ee; --accent:#c084fc; --good:#86efac; }
9
+ * { box-sizing: border-box; }
10
+ body { margin:0; background:var(--bg); color:var(--fg); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; }
11
+ .wrap { max-width:1100px; margin:0 auto; padding:24px 18px 60px; }
12
+ h1 { font-size:1.5rem; margin:0 0 4px; }
13
+ .sub { color:#9aa0ad; font-size:.95rem; margin:0 0 18px; line-height:1.5; }
14
+ .sub a { color:var(--accent); }
15
+ .controls { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:16px; }
16
+ input[type=text] { flex:1; min-width:260px; background:var(--panel); border:1px solid var(--line); color:var(--fg); padding:12px 14px; border-radius:10px; font-size:1rem; }
17
+ button { background:var(--accent); color:#1a1024; border:0; padding:12px 18px; border-radius:10px; font-size:1rem; font-weight:600; cursor:pointer; }
18
+ button:disabled { opacity:.5; cursor:not-allowed; }
19
+ .timer { font-family:ui-monospace,Menlo,monospace; color:var(--good); background:#11210f; border:1px solid #234; padding:8px 12px; border-radius:8px; display:none; }
20
+ .grid { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
21
+ .card { background:var(--panel); border:1px solid var(--line); border-radius:14px; overflow:hidden; }
22
+ .card h2 { font-size:.85rem; text-transform:uppercase; letter-spacing:.05em; color:#9aa0ad; margin:0; padding:10px 14px; border-bottom:1px solid var(--line); }
23
+ .media { aspect-ratio:832/480; background:#000; display:flex; align-items:center; justify-content:center; }
24
+ .media video, .media img { width:100%; height:100%; object-fit:cover; display:block; }
25
+ .placeholder { color:#5a6070; font-size:.9rem; }
26
+ .status { margin-top:14px; color:#9aa0ad; font-size:.9rem; min-height:1.2em; }
27
+ @media (max-width:780px){ .grid{ grid-template-columns:1fr; } }
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <div class="wrap">
32
+ <h1>🌀 StreamDiffusionV2 · Realtime Webcam Diffusion</h1>
33
+ <p class="sub">
34
+ Live demo of <a href="https://streamdiffusionv2.github.io/" target="_blank">StreamDiffusionV2</a>
35
+ (MLSys 2026 Best Paper) on Wan2.1-T2V-1.3B. It streams your webcam through a causal video-diffusion
36
+ model with a <b>sink-token rolling KV cache</b> &mdash; built for <i>continuous</i> streaming, so it
37
+ keeps flowing without the window-shift burst. Type a style prompt, click <b>Start</b> to grab ZeroGPU
38
+ for ~60s. (Prompt is fixed for the session &mdash; restart to change it.)
39
+ </p>
40
+
41
+ <div class="controls">
42
+ <input id="instruction" type="text" placeholder="style prompt · e.g. psychedelic neon dream · van gogh · cyberpunk city" />
43
+ <button id="startBtn">▶ Start session</button>
44
+ <span id="timer" class="timer">⏱ <span id="count">58</span>s</span>
45
+ </div>
46
+
47
+ <div class="grid">
48
+ <div class="card">
49
+ <h2>Your webcam</h2>
50
+ <div class="media"><video id="cam" autoplay muted playsinline></video></div>
51
+ </div>
52
+ <div class="card">
53
+ <h2>Edited (live)</h2>
54
+ <div class="media"><img id="out" alt="" /><span id="outPh" class="placeholder">edited stream appears here</span></div>
55
+ </div>
56
+ </div>
57
+
58
+ <div id="status" class="status"></div>
59
+ </div>
60
+
61
+ <canvas id="grab" width="832" height="480" style="display:none"></canvas>
62
+
63
+ <script type="module">
64
+ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
65
+
66
+ const camEl = document.getElementById("cam");
67
+ const outEl = document.getElementById("out");
68
+ const outPh = document.getElementById("outPh");
69
+ const startBtn = document.getElementById("startBtn");
70
+ const instr = document.getElementById("instruction");
71
+ const timerEl = document.getElementById("timer");
72
+ const countEl = document.getElementById("count");
73
+ const statusEl = document.getElementById("status");
74
+ const grab = document.getElementById("grab");
75
+ const gctx = grab.getContext("2d");
76
+
77
+ let client = null;
78
+ let stream = null;
79
+ let captureTimer = null;
80
+ let countdownTimer = null;
81
+ let running = false;
82
+
83
+ const FPS = 10; // webcam frames sent per second
84
+ const SESSION_SECONDS = 58;
85
+
86
+ // --- jitter buffer: edited frames arrive in bursts (one chunk at a time) but
87
+ // we play them out at a smooth, adaptive rate so the preview doesn't stutter.
88
+ let playQueue = [];
89
+ const MAX_QUEUE = 36; // bound latency (~3 chunks)
90
+ let lastShown = 0;
91
+ function playLoop(ts){
92
+ if (playQueue.length > MAX_QUEUE) playQueue = playQueue.slice(-MAX_QUEUE);
93
+ // drain faster as the backlog grows, slower when nearly empty
94
+ const n = playQueue.length;
95
+ const fps = n > 24 ? 18 : n > 14 ? 13 : n > 6 ? 9 : 6;
96
+ if (n && ts - lastShown >= 1000 / fps){
97
+ outEl.src = playQueue.shift();
98
+ outPh.style.display = "none";
99
+ lastShown = ts;
100
+ }
101
+ requestAnimationFrame(playLoop);
102
+ }
103
+ requestAnimationFrame(playLoop);
104
+
105
+ function setStatus(t){ statusEl.textContent = t; }
106
+
107
+ async function ensureClient(){
108
+ if (!client) client = await Client.connect(window.location.origin);
109
+ return client;
110
+ }
111
+
112
+ async function ensureCam(){
113
+ if (stream) return;
114
+ stream = await navigator.mediaDevices.getUserMedia({ video: { width: 832, height: 480 }, audio: false });
115
+ camEl.srcObject = stream;
116
+ await camEl.play().catch(()=>{});
117
+ }
118
+
119
+ async function sendInstruction(){
120
+ try {
121
+ await fetch("/instruction", {
122
+ method:"POST", headers:{ "Content-Type":"application/json" },
123
+ body: JSON.stringify({ instruction: instr.value || "" })
124
+ });
125
+ } catch(e){}
126
+ }
127
+
128
+ function startCapture(){
129
+ captureTimer = setInterval(() => {
130
+ if (!camEl.videoWidth) return;
131
+ gctx.drawImage(camEl, 0, 0, grab.width, grab.height);
132
+ grab.toBlob(async (blob) => {
133
+ if (!blob) return;
134
+ try { await fetch("/frame", { method:"POST", body: blob }); } catch(e){}
135
+ }, "image/jpeg", 0.8);
136
+ }, 1000 / FPS);
137
+ }
138
+
139
+ function stopAll(){
140
+ running = false;
141
+ if (captureTimer) { clearInterval(captureTimer); captureTimer = null; }
142
+ if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
143
+ timerEl.style.display = "none";
144
+ startBtn.disabled = false;
145
+ startBtn.textContent = "▶ Start session";
146
+ }
147
+
148
+ function startCountdown(){
149
+ let r = SESSION_SECONDS;
150
+ countEl.textContent = r;
151
+ timerEl.style.display = "inline-block";
152
+ countdownTimer = setInterval(() => {
153
+ r -= 1; countEl.textContent = Math.max(0, r);
154
+ if (r <= 0) clearInterval(countdownTimer);
155
+ }, 1000);
156
+ }
157
+
158
+ instr.addEventListener("change", () => { if (running) sendInstruction(); });
159
+ instr.addEventListener("input", () => { if (running) sendInstruction(); });
160
+
161
+ startBtn.addEventListener("click", async () => {
162
+ if (running) return;
163
+ startBtn.disabled = true;
164
+ try {
165
+ setStatus("Requesting webcam…");
166
+ await ensureCam();
167
+ setStatus("Connecting…");
168
+ await ensureClient();
169
+ running = true;
170
+ playQueue = [];
171
+ startBtn.textContent = "◌ Acquiring ZeroGPU…";
172
+ await sendInstruction();
173
+ setStatus("Queued for ZeroGPU — webcam streaming starts once the GPU is acquired…");
174
+
175
+ const job = client.submit("/run_session", {});
176
+ let frames = 0;
177
+ for await (const msg of job) {
178
+ if (msg.type !== "data" || !msg.data || msg.data[0] == null) continue;
179
+ const payload = msg.data[0];
180
+ if (payload === "__READY__") {
181
+ // GPU is now allocated — only now start capturing & sending frames.
182
+ startBtn.textContent = "● Live";
183
+ await sendInstruction();
184
+ startCapture();
185
+ startCountdown();
186
+ setStatus("ZeroGPU acquired — streaming your webcam through LiveEdit…");
187
+ continue;
188
+ }
189
+ playQueue.push(payload); // jitter buffer paces actual display
190
+ frames += 1;
191
+ if (frames % 12 === 0) setStatus(`Streaming… ${frames} edited frames`);
192
+ }
193
+ setStatus(frames ? "Session ended. Click Start to run another ~60s session."
194
+ : "Session ended before any frames were produced — try again.");
195
+ } catch (e) {
196
+ setStatus("Error: " + (e && e.message ? e.message : e));
197
+ } finally {
198
+ stopAll();
199
+ }
200
+ });
201
+ </script>
202
+ </body>
203
+ </html>
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ streamdiffusionv2