Spaces:
Running on Zero
Running on Zero
cross-theme warning, crossfade, and a swap time bound to the clip
Browse files
app.py
CHANGED
|
@@ -214,6 +214,11 @@ WORLDS = {f"world {w['idx']} · {w['prompt'][:70]}": w["idx"] for w in INFO["wor
|
|
| 214 |
WORLD_LABELS = list(WORLDS)
|
| 215 |
PROMPT_LABELS = list(PROMPTS)
|
| 216 |
W = {i: lbl for lbl, i in WORLDS.items()} # by world id, for the default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
print(f"[boot] ready: step {INFO['step']}, {len(PROMPTS)} prompts, "
|
| 218 |
f"{len(WORLDS)} worlds", flush=True)
|
| 219 |
|
|
@@ -245,7 +250,8 @@ def write_mp4(frames):
|
|
| 245 |
return path
|
| 246 |
|
| 247 |
|
| 248 |
-
def _duration(world, steer_to, steer_text, steer_at, seconds, seed,
|
|
|
|
| 249 |
# Measured: ~12 s to open a world (load, prime the cache, decode its 81 frames)
|
| 250 |
# and ~1.02 s per block, a block being 0.75 s of video. Declared tight on purpose:
|
| 251 |
# ZeroGPU compares the request against the visitor's remaining quota, not the
|
|
@@ -255,7 +261,7 @@ def _duration(world, steer_to, steer_text, steer_at, seconds, seed, *args, **kwa
|
|
| 255 |
|
| 256 |
@spaces.GPU(duration=_duration)
|
| 257 |
def run(world: str, steer_to: str, steer_text: str, steer_at: float,
|
| 258 |
-
seconds: float, seed: int):
|
| 259 |
"""Stream video from a cached world, optionally swapping the prompt mid-stream.
|
| 260 |
|
| 261 |
Args:
|
|
@@ -263,6 +269,7 @@ def run(world: str, steer_to: str, steer_text: str, steer_at: float,
|
|
| 263 |
steer_to: a prompt from the 96-prompt bank to swap to, or the no-steer option.
|
| 264 |
steer_text: free text to swap to instead; takes precedence over steer_to.
|
| 265 |
steer_at: seconds into the clip at which to swap the conditioning.
|
|
|
|
| 266 |
seconds: how much video to generate, at 16 fps.
|
| 267 |
seed: RNG seed for the block sampler.
|
| 268 |
|
|
@@ -270,7 +277,10 @@ def run(world: str, steer_to: str, steer_text: str, steer_at: float,
|
|
| 270 |
(preview frame, HUD line, finished mp4) — the mp4 only on the last yield.
|
| 271 |
"""
|
| 272 |
total_frames = int(float(seconds) * FPS)
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
text = (steer_text or "").strip()
|
| 276 |
if text:
|
|
@@ -319,7 +329,7 @@ def run(world: str, steer_to: str, steer_text: str, steer_at: float,
|
|
| 319 |
cv2.IMREAD_COLOR)[:, :, ::-1])
|
| 320 |
|
| 321 |
if pending_steer and len(frames) >= steer_frame:
|
| 322 |
-
engine.steer(**steer_with)
|
| 323 |
pending_steer = False
|
| 324 |
|
| 325 |
now = time.perf_counter()
|
|
@@ -357,6 +367,31 @@ Running on shared ZeroGPU hardware, so about 0.7x real time here; a dedicated GP
|
|
| 357 |
does about 2.7x. [Code]({GITHUB}) · [Weights](https://huggingface.co/{LIVEWAN_REPO})
|
| 358 |
"""
|
| 359 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
with gr.Blocks(title="LiveWan") as demo:
|
| 361 |
with gr.Column(elem_id="col-container"):
|
| 362 |
gr.Markdown(INTRO)
|
|
@@ -371,10 +406,15 @@ with gr.Blocks(title="LiveWan") as demo:
|
|
| 371 |
label="Or steer to your own text", lines=2,
|
| 372 |
placeholder="A lighthouse in a storm",
|
| 373 |
info="Encoded with umt5-xxl. Overrides the selection above.")
|
| 374 |
-
|
| 375 |
-
label="Swap at (seconds in)")
|
| 376 |
seconds = gr.Slider(5, 30, value=15, step=1,
|
| 377 |
label="Generate (seconds of video)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
seed = gr.Number(value=0, precision=0, label="Seed")
|
| 379 |
run_btn = gr.Button("Stream", variant="primary", size="lg")
|
| 380 |
|
|
@@ -386,7 +426,13 @@ with gr.Blocks(title="LiveWan") as demo:
|
|
| 386 |
video = gr.Video(label="The clip, at 16 fps", autoplay=True,
|
| 387 |
height=368)
|
| 388 |
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
outputs=[preview, stats, video], concurrency_limit=1)
|
| 391 |
|
| 392 |
demo.queue(max_size=12).launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)
|
|
|
|
| 214 |
WORLD_LABELS = list(WORLDS)
|
| 215 |
PROMPT_LABELS = list(PROMPTS)
|
| 216 |
W = {i: lbl for lbl, i in WORLDS.items()} # by world id, for the default
|
| 217 |
+
# Themes are what the local UI uses to warn about a steer that will smear. The four
|
| 218 |
+
# shipped worlds are named by their own bank prompt, so the world id is a prompt id.
|
| 219 |
+
from wanstreamer.serve.conditioning import theme_of # noqa: E402
|
| 220 |
+
PROMPT_THEME = {p["idx"]: p["theme"] for p in INFO["prompts"]}
|
| 221 |
+
WORLD_THEME = {w["idx"]: theme_of(w["idx"]) for w in INFO["worlds"]}
|
| 222 |
print(f"[boot] ready: step {INFO['step']}, {len(PROMPTS)} prompts, "
|
| 223 |
f"{len(WORLDS)} worlds", flush=True)
|
| 224 |
|
|
|
|
| 250 |
return path
|
| 251 |
|
| 252 |
|
| 253 |
+
def _duration(world, steer_to, steer_text, steer_at, crossfade, seconds, seed,
|
| 254 |
+
*args, **kwargs):
|
| 255 |
# Measured: ~12 s to open a world (load, prime the cache, decode its 81 frames)
|
| 256 |
# and ~1.02 s per block, a block being 0.75 s of video. Declared tight on purpose:
|
| 257 |
# ZeroGPU compares the request against the visitor's remaining quota, not the
|
|
|
|
| 261 |
|
| 262 |
@spaces.GPU(duration=_duration)
|
| 263 |
def run(world: str, steer_to: str, steer_text: str, steer_at: float,
|
| 264 |
+
crossfade: int, seconds: float, seed: int):
|
| 265 |
"""Stream video from a cached world, optionally swapping the prompt mid-stream.
|
| 266 |
|
| 267 |
Args:
|
|
|
|
| 269 |
steer_to: a prompt from the 96-prompt bank to swap to, or the no-steer option.
|
| 270 |
steer_text: free text to swap to instead; takes precedence over steer_to.
|
| 271 |
steer_at: seconds into the clip at which to swap the conditioning.
|
| 272 |
+
crossfade: blocks to interpolate the conditioning over; 0 swaps instantly.
|
| 273 |
seconds: how much video to generate, at 16 fps.
|
| 274 |
seed: RNG seed for the block sampler.
|
| 275 |
|
|
|
|
| 277 |
(preview frame, HUD line, finished mp4) — the mp4 only on the last yield.
|
| 278 |
"""
|
| 279 |
total_frames = int(float(seconds) * FPS)
|
| 280 |
+
# A swap at or past the end of the clip would never fire and the run would look
|
| 281 |
+
# like the control did nothing. Keep it at least two seconds from the end so the
|
| 282 |
+
# result of the swap is actually visible.
|
| 283 |
+
steer_frame = int(min(float(steer_at), max(1.0, float(seconds) - 2.0)) * FPS)
|
| 284 |
|
| 285 |
text = (steer_text or "").strip()
|
| 286 |
if text:
|
|
|
|
| 329 |
cv2.IMREAD_COLOR)[:, :, ::-1])
|
| 330 |
|
| 331 |
if pending_steer and len(frames) >= steer_frame:
|
| 332 |
+
engine.steer(crossfade=int(crossfade), **steer_with)
|
| 333 |
pending_steer = False
|
| 334 |
|
| 335 |
now = time.perf_counter()
|
|
|
|
| 367 |
does about 2.7x. [Code]({GITHUB}) · [Weights](https://huggingface.co/{LIVEWAN_REPO})
|
| 368 |
"""
|
| 369 |
|
| 370 |
+
def steer_warning(world, steer_to, steer_text):
|
| 371 |
+
"""The same warning the local UI shows before a steer that will smear.
|
| 372 |
+
|
| 373 |
+
Cross-theme steers ask the model to reconcile a K/V cache full of one kind of
|
| 374 |
+
scene with a prompt describing another, and it smears rather than resolving.
|
| 375 |
+
Free text gets no theme, so it cannot be checked.
|
| 376 |
+
"""
|
| 377 |
+
if (steer_text or "").strip() or not steer_to or steer_to == NO_STEER:
|
| 378 |
+
return gr.update(visible=False)
|
| 379 |
+
wt = WORLD_THEME.get(WORLDS[world])
|
| 380 |
+
pt = PROMPT_THEME.get(PROMPTS[steer_to])
|
| 381 |
+
if not wt or not pt or wt == pt:
|
| 382 |
+
return gr.update(visible=False)
|
| 383 |
+
return gr.update(visible=True, value=(
|
| 384 |
+
f"⚠️ {wt.lower()} to {pt.lower()} is a jump across themes, so expect the "
|
| 385 |
+
f"picture to smear rather than resolve. Raise the crossfade to soften it, "
|
| 386 |
+
f"or steer somewhere closer to where you started."))
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def bound_swap(seconds, steer_at):
|
| 390 |
+
"""A swap must land far enough before the end to be worth watching."""
|
| 391 |
+
hi = max(1.0, float(seconds) - 2.0)
|
| 392 |
+
return gr.update(maximum=hi, value=min(float(steer_at), hi))
|
| 393 |
+
|
| 394 |
+
|
| 395 |
with gr.Blocks(title="LiveWan") as demo:
|
| 396 |
with gr.Column(elem_id="col-container"):
|
| 397 |
gr.Markdown(INTRO)
|
|
|
|
| 406 |
label="Or steer to your own text", lines=2,
|
| 407 |
placeholder="A lighthouse in a storm",
|
| 408 |
info="Encoded with umt5-xxl. Overrides the selection above.")
|
| 409 |
+
warning = gr.Markdown(visible=False)
|
|
|
|
| 410 |
seconds = gr.Slider(5, 30, value=15, step=1,
|
| 411 |
label="Generate (seconds of video)")
|
| 412 |
+
steer_at = gr.Slider(1, 13, value=6, step=0.5,
|
| 413 |
+
label="Swap at (seconds in)")
|
| 414 |
+
crossfade = gr.Slider(0, 12, value=0, step=1,
|
| 415 |
+
label="Crossfade (blocks)",
|
| 416 |
+
info="0 swaps instantly. Blending over a few "
|
| 417 |
+
"blocks softens a big jump.")
|
| 418 |
seed = gr.Number(value=0, precision=0, label="Seed")
|
| 419 |
run_btn = gr.Button("Stream", variant="primary", size="lg")
|
| 420 |
|
|
|
|
| 426 |
video = gr.Video(label="The clip, at 16 fps", autoplay=True,
|
| 427 |
height=368)
|
| 428 |
|
| 429 |
+
for control in (world, steer_to, steer_text):
|
| 430 |
+
control.change(steer_warning, [world, steer_to, steer_text], warning)
|
| 431 |
+
seconds.change(bound_swap, [seconds, steer_at], steer_at)
|
| 432 |
+
|
| 433 |
+
run_btn.click(run,
|
| 434 |
+
inputs=[world, steer_to, steer_text, steer_at, crossfade,
|
| 435 |
+
seconds, seed],
|
| 436 |
outputs=[preview, stats, video], concurrency_limit=1)
|
| 437 |
|
| 438 |
demo.queue(max_size=12).launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)
|