multimodalart HF Staff commited on
Commit
7f4191e
·
verified ·
1 Parent(s): 7dacb17

Per-step requests (fresh ZeroGPU token each GPU call)

Browse files
Files changed (1) hide show
  1. app.py +256 -187
app.py CHANGED
@@ -284,10 +284,47 @@ def read_script(workdir: Path) -> str:
284
  return p.read_text(errors="replace") if p.is_file() else ""
285
 
286
 
287
- def run_agent(prompt: str, max_steps: int = 12, temperature: float = 1.0, top_p: float = 0.95,
288
- max_new_tokens: int = 16384, request: gr.Request = None):
289
- """Turn a scene description into an animated 3D "draft video": Video-Coder-9B writes a Blender script, renders it,
290
- inspects the frames, fixes problems and submits.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
  Args:
293
  prompt: description of the scene / video to build.
@@ -295,185 +332,211 @@ def run_agent(prompt: str, max_steps: int = 12, temperature: float = 1.0, top_p:
295
  temperature: sampling temperature (the authors sample with 1.0).
296
  top_p: nucleus sampling (the authors use 0.95).
297
  max_new_tokens: token budget of a single agent turn.
298
-
299
- Returns:
300
- (agent trajectory, draft video, main.py, latest render preview, status)
301
  """
302
  if not prompt or not prompt.strip():
303
  raise gr.Error("Please enter a scene description.")
304
- try:
305
- _tok = request.headers.get("x-ip-token") if request is not None else None
306
- if _tok:
307
- _p = json.loads(base64.urlsafe_b64decode(_tok.split(".")[1] + "=="))
308
- print("x-ip-token payload:", {k: v for k, v in _p.items() if k in ("exp", "iat", "nbf")}, "now", time.time())
309
- else:
310
- print("no x-ip-token")
311
- except Exception as e:
312
- print("token decode failed", e)
313
- workdir = (RUNS_DIR / uuid.uuid4().hex).resolve()
314
- workdir.mkdir(parents=True)
315
- messages = [
316
- {"role": "system", "content": SYSTEM_PROMPT},
317
- {"role": "user", "content": INSTANCE_TEMPLATE.replace("__TASK__", prompt.strip())},
318
- ]
319
- images: list = []
320
- chat: list = []
321
- video = None
322
- preview = None
323
- last_ok = None # (script hash, video path) of the last run_blender that reported RUN OK
324
- last_key = None
325
- repeats = 0
326
- n_format_errors = 0
327
- exit_status = "StepLimit"
328
- t_start = time.time()
329
-
330
- def status(msg):
331
- return f"**{msg}** · {time.time() - t_start:.0f}s elapsed"
332
-
333
- def ui(msg):
334
- return chat, video, read_script(workdir), preview, status(msg)
335
-
336
- yield ui("Starting…")
337
- for step in range(1, int(max_steps) + 1):
338
- # ---- model turn (possibly split over several GPU calls) ----
339
- raw, used = "", 0
340
- live = {"role": "assistant", "content": "", "metadata": {"title": f"💭 Step {step}: thinking…",
341
- "status": "pending"}}
342
- chat.append(live)
343
- while True:
344
- finished, n = False, None
345
- base = raw
346
- for text, finished, n in llm_chunk(messages, images, base, max_new_tokens - used, temperature, top_p):
347
- raw = base + text
348
- live["content"] = raw[-3000:].replace("<think>", "")
349
- yield ui(f"Step {step}/{max_steps}: Video-Coder is writing ({len(raw)} chars)")
350
- used += n or 0
351
- if finished or used >= max_new_tokens:
352
- break
353
- chat.pop()
354
- reasoning, content, calls = parse_response(raw)
355
- if reasoning:
356
- chat.append({"role": "assistant", "content": reasoning,
357
- "metadata": {"title": f"💭 Step {step}: reasoning", "status": "done"}})
358
- msg = {"role": "assistant", "content": content}
359
- if reasoning:
360
- msg["reasoning_content"] = reasoning
361
- if calls:
362
- msg["tool_calls"] = [{"type": "function", "function": {"name": c["name"], "arguments": c["arguments"]}}
363
- for c in calls]
364
- messages.append(msg)
365
-
366
- # ---- format errors ----
367
- error = None
368
- if not calls:
369
- error = "No tool calls found in the response. Every response MUST include at least one tool call."
370
- elif any(c["name"] != "bash" or "command" not in c["arguments"] for c in calls):
371
- error = "Unknown tool or missing 'command' argument."
372
- if error:
373
- n_format_errors += 1
374
- finish_reason = "length" if not any(t in raw for t in END_TOKENS) else "stop"
375
- err = FORMAT_ERROR_TEMPLATE.render(error=error, finish_reason=finish_reason, has_tool_calls=bool(calls))
376
- if content:
377
- chat.append({"role": "assistant", "content": content})
378
- chat.append({"role": "user", "content": f"⚠️ {err}"})
379
- messages.append({"role": "user", "content": err})
380
- yield ui(f"Step {step}: format error")
381
- if n_format_errors >= MAX_FORMAT_ERRORS:
382
- exit_status = "FormatError"
383
- break
384
- continue
385
- n_format_errors = 0
386
-
387
- # ---- execute the bash command(s) ----
388
- submitted = False
389
- stop = False
390
- for call in calls:
391
- command = call["arguments"]["command"]
392
- shown = f"{content}\n\n" if content else ""
393
- chat.append({"role": "assistant", "content": f"{shown}```bash\n{short(command)}\n```"})
394
- yield ui(f"Step {step}: running `{command.strip().splitlines()[0][:60]}`")
395
- reason = harness_violation(command, HARNESS_MARKERS, str(workdir))
396
- if reason:
397
- out = {"output": BLOCKED.format(reason=reason), "returncode": 1, "exception_info": ""}
398
- else:
399
- out = run_command(command, str(workdir))
400
- if is_submit(out):
401
- submitted = True
402
- break
403
- key = (command, _normalize_output(out.get("output", "")))
404
- same = (last_key is not None and key[1] == last_key[1]
405
- and difflib.SequenceMatcher(None, key[0], last_key[0]).ratio() >= REPEAT_SIM)
406
- repeats = repeats + 1 if same else 1
407
- last_key = key
408
- if repeats >= REPEAT_WARN:
409
- out["output"] = (out.get("output") or "") + REPEAT_WARNING.format(n=repeats, stop=REPEAT_STOP)
410
- if repeats >= REPEAT_STOP:
411
- stop = True
412
-
413
- text, imgs = split_images(out.get("output") or "")
414
- obs = OBSERVATION_TEMPLATE.render(output={**out, "output": text})
415
- if imgs:
416
- parts = [{"type": "text", "text": obs}]
417
- for k, (caption, url) in enumerate(imgs, 1):
418
- parts += [{"type": "text", "text": f"image {k}: {caption}" if caption else f"image {k}:"},
419
- {"type": "image"}]
420
- images.append(data_url_to_pil(url))
421
- messages.append({"role": "tool", "content": parts})
422
- else:
423
- messages.append({"role": "tool", "content": obs})
424
-
425
- if command.strip().startswith("run_blender"):
426
- vid = workdir / HARNESS["output_video"]
427
- if vid.is_file():
428
- keep = workdir / f".render_step{step}.mp4"
429
- shutil.copy(vid, keep)
430
- video = str(keep)
431
- if text.startswith("RUN OK"):
432
- last_ok = (sha(workdir / SCRIPT), str(keep))
433
- if (workdir / "preview.png").is_file():
434
- keep_png = workdir / f".preview_step{step}.png"
435
- shutil.copy(workdir / "preview.png", keep_png)
436
- preview = str(keep_png)
437
- chat.append({"role": "assistant", "content": f"```\n{short(text.strip(), 40)}\n```",
438
- "metadata": {"title": f"🛠️ Output (exit code {out['returncode']})", "status": "done"}})
439
- yield ui(f"Step {step}/{max_steps} done")
440
- if submitted:
441
- exit_status = "Submitted"
442
- break
443
- if stop:
444
- exit_status = "RepeatedActions"
445
- break
446
 
447
- # ---- acceptance: static check + the final script re-rendered from scratch (harness final_execution) ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  script = workdir / SCRIPT
449
- if exit_status == "Submitted" and script.is_file():
450
- chat.append({"role": "assistant", "content": "✅ Submitted. Validating the final script…"})
451
- yield ui("Validating the submitted script")
452
- check = check_file(script)
453
- if not check["ok"]:
454
- yield ui(f"Submitted, but the static check failed: {check['errors']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  return
456
- if last_ok and last_ok[0] == sha(script): # rendering is deterministic: reuse the identical render
457
- video = last_ok[1]
458
- else:
459
- yield ui("Re-rendering the submitted script")
460
- final_dir = workdir / ".final_run"
461
- final_dir.mkdir(exist_ok=True)
462
- shutil.copy(script, final_dir / SCRIPT)
463
- report = blender_execute(final_dir / SCRIPT, final_dir, HARNESS)
464
- if (final_dir / HARNESS["output_video"]).is_file():
465
- video = str(final_dir / HARNESS["output_video"])
466
- if (final_dir / "preview.png").is_file():
467
- preview = str(final_dir / "preview.png")
468
- if not report["ok"]:
469
- yield ui(f"Submitted, but the final render has problems: {report['problems']}")
470
- return
471
- yield ui("Accepted ✅ — draft video rendered from the agent's Blender script")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  else:
473
- note = {"StepLimit": "Step limit reached before the agent submitted (showing the latest render)",
474
- "RepeatedActions": "Stopped: the agent repeated the same command",
475
- "FormatError": "Stopped after repeated format errors"}[exit_status]
476
- yield ui(note)
 
 
 
 
 
 
 
 
 
 
 
477
 
478
 
479
  # --------------------------------------------------------------------------------------------------------------------
@@ -486,6 +549,8 @@ CSS = """
486
 
487
  EXAMPLES = [
488
  "a man standing in front of a machine.",
 
 
489
  "The video shows a sequence of three identical white cars driving on a road. The first car is in the foreground, "
490
  "the second car is slightly behind and to the right, and the third car is in the background. The cars are moving "
491
  "from left to right across the frame. The road appears to be a city street with a dark grey surface. The "
@@ -498,8 +563,6 @@ EXAMPLES = [
498
  "clear, suggesting a sunny day. The boats are the main focus of the video, with their movement and the water's "
499
  "reaction to their speed creating a dynamic and exciting scene. The overall style of the video is a dynamic "
500
  "aerial shot, capturing the movement and beauty of the ocean and the boats.",
501
- "A single polished walnut-wood chess pawn stands on a small square pedestal of white Carrara marble. No other "
502
- "objects are in the scene. The camera makes one complete 360-degree orbit around the pawn while slowly rising.",
503
  ]
504
 
505
  with gr.Blocks(title="Video-Coder") as demo:
@@ -513,14 +576,16 @@ with gr.Blocks(title="Video-Coder") as demo:
513
  "[📖 Paper](https://huggingface.co/papers/2611.09894) · [🌐 Project page](https://cywang735.github.io/"
514
  "Video-Coder/) · [💻 Code](https://github.com/CYWang735/Video-Coder) · [🤗 Model](https://huggingface.co/"
515
  "Video-Coder/Video-Coder-9B)\n\n"
516
- "⏳ A full agent run takes several minutes (each turn generates a long script; each render runs Blender "
517
- "on CPU). The trajectory streams live below."
518
  )
519
  with gr.Row():
520
  prompt = gr.Textbox(label="Scene description", lines=3, scale=4,
521
  placeholder="Describe the scene and its motion, e.g. 'a red ball rolls down a ramp "
522
  "and knocks over three dominoes'")
523
- run_btn = gr.Button("Generate draft video", variant="primary", scale=1)
 
 
524
  status = gr.Markdown()
525
  with gr.Row():
526
  with gr.Column(scale=1):
@@ -539,12 +604,16 @@ with gr.Blocks(title="Video-Coder") as demo:
539
  top_p = gr.Slider(0.5, 1.0, value=0.95, step=0.01, label="Top-p")
540
  max_new_tokens = gr.Slider(2048, 32768, value=16384, step=1024, label="Max tokens per agent turn")
541
 
542
- outputs = [chat_out, video_out, code_out, preview_out, status]
543
- run_btn.click(run_agent, inputs=[prompt, max_steps, temperature, top_p, max_new_tokens], outputs=outputs,
544
- api_name="generate", concurrency_limit=2)
545
- prompt.submit(run_agent, inputs=[prompt, max_steps, temperature, top_p, max_new_tokens], outputs=outputs,
546
- api_visibility="private", concurrency_limit=2)
547
- gr.Examples(examples=[[e] for e in EXAMPLES], inputs=[prompt], outputs=outputs, fn=run_agent,
 
 
 
 
548
  cache_examples=False, run_on_click=True)
549
 
550
  if __name__ == "__main__":
 
284
  return p.read_text(errors="replace") if p.is_file() else ""
285
 
286
 
287
+ def new_state(prompt: str, max_steps: int, temperature: float, top_p: float, max_new_tokens: int) -> dict:
288
+ workdir = (RUNS_DIR / uuid.uuid4().hex).resolve()
289
+ workdir.mkdir(parents=True)
290
+ return {
291
+ "workdir": str(workdir),
292
+ "messages": [
293
+ {"role": "system", "content": SYSTEM_PROMPT},
294
+ {"role": "user", "content": INSTANCE_TEMPLATE.replace("__TASK__", prompt.strip())},
295
+ ],
296
+ "image_urls": [], # frames shown to the model, in order (JPEG data URLs)
297
+ "chat": [],
298
+ "video": None,
299
+ "preview": None,
300
+ "last_ok": None, # (script hash, video) of the last run_blender that reported RUN OK
301
+ "last_key": None,
302
+ "repeats": 0,
303
+ "n_format_errors": 0,
304
+ "step": 1,
305
+ "raw": "", # current (possibly unfinished) model turn
306
+ "used": 0,
307
+ "max_steps": int(max_steps),
308
+ "temperature": float(temperature),
309
+ "top_p": float(top_p),
310
+ "max_new_tokens": int(max_new_tokens),
311
+ "done": False,
312
+ "status": "Starting…",
313
+ "t_start": time.time(),
314
+ }
315
+
316
+
317
+ def ui(state: dict, tick=None):
318
+ workdir = Path(state["workdir"])
319
+ msg = f"**{state['status']}** · {time.time() - state['t_start']:.0f}s elapsed"
320
+ return (state, state["chat"], state["video"], read_script(workdir), state["preview"], msg,
321
+ gr.update() if tick is None else tick)
322
+
323
+
324
+ def start(prompt: str, max_steps: int = 12, temperature: float = 1.0, top_p: float = 0.95,
325
+ max_new_tokens: int = 16384):
326
+ """Start a Video-Coder agent run for a scene description. The run then proceeds turn by turn: call `advance`
327
+ with the returned state (same session) until its status says it finished.
328
 
329
  Args:
330
  prompt: description of the scene / video to build.
 
332
  temperature: sampling temperature (the authors sample with 1.0).
333
  top_p: nucleus sampling (the authors use 0.95).
334
  max_new_tokens: token budget of a single agent turn.
 
 
 
335
  """
336
  if not prompt or not prompt.strip():
337
  raise gr.Error("Please enter a scene description.")
338
+ state = new_state(prompt, max_steps, temperature, top_p, max_new_tokens)
339
+ return ui(state, tick=time.time())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
+
342
+ def _finish_turn(state: dict):
343
+ """Parse the finished model turn, run its command(s) in the harness; a generator of UI updates."""
344
+ workdir = Path(state["workdir"])
345
+ step, raw = state["step"], state["raw"]
346
+ chat, messages = state["chat"], state["messages"]
347
+ if chat and chat[-1].get("metadata", {}).get("status") == "pending":
348
+ chat.pop()
349
+ reasoning, content, calls = parse_response(raw)
350
+ state["raw"], state["used"] = "", 0
351
+ if reasoning:
352
+ chat.append({"role": "assistant", "content": reasoning,
353
+ "metadata": {"title": f"💭 Step {step}: reasoning", "status": "done"}})
354
+ msg = {"role": "assistant", "content": content}
355
+ if reasoning:
356
+ msg["reasoning_content"] = reasoning
357
+ if calls:
358
+ msg["tool_calls"] = [{"type": "function", "function": {"name": c["name"], "arguments": c["arguments"]}}
359
+ for c in calls]
360
+ messages.append(msg)
361
+
362
+ error = None
363
+ if not calls:
364
+ error = "No tool calls found in the response. Every response MUST include at least one tool call."
365
+ elif any(c["name"] != "bash" or "command" not in c["arguments"] for c in calls):
366
+ error = "Unknown tool or missing 'command' argument."
367
+ if error:
368
+ state["n_format_errors"] += 1
369
+ finish_reason = "length" if not any(t in raw for t in END_TOKENS) else "stop"
370
+ err = FORMAT_ERROR_TEMPLATE.render(error=error, finish_reason=finish_reason, has_tool_calls=bool(calls))
371
+ if content:
372
+ chat.append({"role": "assistant", "content": content})
373
+ chat.append({"role": "user", "content": f"⚠️ {err}"})
374
+ messages.append({"role": "user", "content": err})
375
+ if state["n_format_errors"] >= MAX_FORMAT_ERRORS:
376
+ return "FormatError"
377
+ return None
378
+ state["n_format_errors"] = 0
379
+
380
+ for call in calls:
381
+ command = call["arguments"]["command"]
382
+ shown = f"{content}\n\n" if content else ""
383
+ chat.append({"role": "assistant", "content": f"{shown}```bash\n{short(command)}\n```"})
384
+ first = command.strip().splitlines()[0][:60] if command.strip() else ""
385
+ state["status"] = f"Step {step}: running `{first}`"
386
+ yield
387
+ reason = harness_violation(command, HARNESS_MARKERS, str(workdir))
388
+ if reason:
389
+ out = {"output": BLOCKED.format(reason=reason), "returncode": 1, "exception_info": ""}
390
+ else:
391
+ out = run_command(command, str(workdir))
392
+ if is_submit(out):
393
+ return "Submitted"
394
+ key = (command, _normalize_output(out.get("output", "")))
395
+ last = state["last_key"]
396
+ same = (last is not None and key[1] == last[1]
397
+ and difflib.SequenceMatcher(None, key[0], last[0]).ratio() >= REPEAT_SIM)
398
+ state["repeats"] = state["repeats"] + 1 if same else 1
399
+ state["last_key"] = key
400
+ if state["repeats"] >= REPEAT_WARN:
401
+ out["output"] = (out.get("output") or "") + REPEAT_WARNING.format(n=state["repeats"], stop=REPEAT_STOP)
402
+
403
+ text, imgs = split_images(out.get("output") or "")
404
+ obs = OBSERVATION_TEMPLATE.render(output={**out, "output": text})
405
+ if imgs:
406
+ parts = [{"type": "text", "text": obs}]
407
+ for k, (caption, url) in enumerate(imgs, 1):
408
+ parts += [{"type": "text", "text": f"image {k}: {caption}" if caption else f"image {k}:"},
409
+ {"type": "image"}]
410
+ state["image_urls"].append(url)
411
+ messages.append({"role": "tool", "content": parts})
412
+ else:
413
+ messages.append({"role": "tool", "content": obs})
414
+
415
+ if command.strip().startswith("run_blender"):
416
+ vid = workdir / HARNESS["output_video"]
417
+ if vid.is_file():
418
+ keep = workdir / f"render_step{step}.mp4"
419
+ shutil.copy(vid, keep)
420
+ state["video"] = str(keep)
421
+ if text.startswith("RUN OK"):
422
+ state["last_ok"] = (sha(workdir / SCRIPT), str(keep))
423
+ if (workdir / "preview.png").is_file():
424
+ keep_png = workdir / f"preview_step{step}.png"
425
+ shutil.copy(workdir / "preview.png", keep_png)
426
+ state["preview"] = str(keep_png)
427
+ chat.append({"role": "assistant", "content": f"```\n{short(text.strip(), 40)}\n```",
428
+ "metadata": {"title": f"🛠️ Output (exit code {out['returncode']})", "status": "done"}})
429
+ if state["repeats"] >= REPEAT_STOP:
430
+ return "RepeatedActions"
431
+ return None
432
+
433
+
434
+ def _accept(state: dict):
435
+ """Harness acceptance: static check + the submitted script re-rendered from scratch (final_execution)."""
436
+ workdir = Path(state["workdir"])
437
  script = workdir / SCRIPT
438
+ state["chat"].append({"role": "assistant", "content": "✅ Submitted. Validating the final script…"})
439
+ if not script.is_file():
440
+ state["status"] = f"Submitted, but no {SCRIPT} was written"
441
+ return
442
+ check = check_file(script)
443
+ if not check["ok"]:
444
+ state["status"] = f"Submitted, but the static check failed: {check['errors']}"
445
+ return
446
+ last_ok = state["last_ok"]
447
+ if last_ok and last_ok[0] == sha(script): # Workbench rendering is deterministic: reuse the identical render
448
+ state["video"] = last_ok[1]
449
+ else:
450
+ state["status"] = "Re-rendering the submitted script"
451
+ yield
452
+ final_dir = workdir / "final_run"
453
+ final_dir.mkdir(exist_ok=True)
454
+ shutil.copy(script, final_dir / SCRIPT)
455
+ report = blender_execute(final_dir / SCRIPT, final_dir, HARNESS)
456
+ if (final_dir / HARNESS["output_video"]).is_file():
457
+ state["video"] = str(final_dir / HARNESS["output_video"])
458
+ if (final_dir / "preview.png").is_file():
459
+ state["preview"] = str(final_dir / "preview.png")
460
+ if not report["ok"]:
461
+ state["status"] = f"Submitted, but the final render has problems: {report['problems']}"
462
  return
463
+ state["status"] = "Accepted ✅ — draft video rendered from the agent's Blender script"
464
+
465
+
466
+ def advance(state: dict):
467
+ """Run the next piece of the agent: one GPU generation chunk of the current turn and, once the turn is complete,
468
+ its bash command in the Video-Coder harness. Returns a new tick while the run is still going.
469
+
470
+ Args:
471
+ state: the run state returned by `start` / the previous `advance`.
472
+ """
473
+ if not state or state.get("done"):
474
+ yield ui(state) if state else (state, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
475
+ gr.update())
476
+ return
477
+ step = state["step"]
478
+ chat = state["chat"]
479
+ if not (chat and chat[-1].get("metadata", {}).get("status") == "pending"):
480
+ chat.append({"role": "assistant", "content": "",
481
+ "metadata": {"title": f"💭 Step {step}: thinking…", "status": "pending"}})
482
+ live = chat[-1]
483
+ state["status"] = f"Step {step}/{state['max_steps']}: Video-Coder is thinking"
484
+ yield ui(state)
485
+
486
+ # ---- one GPU call (the ZeroGPU token of a request is short-lived, so each request makes exactly one) ----
487
+ images = [data_url_to_pil(u) for u in state["image_urls"]]
488
+ base = state["raw"]
489
+ finished, n = False, None
490
+ t0 = time.time()
491
+ for text, finished, n in llm_chunk(state["messages"], images, base, state["max_new_tokens"] - state["used"],
492
+ state["temperature"], state["top_p"]):
493
+ state["raw"] = base + text
494
+ live["content"] = state["raw"][-3000:].replace("<think>", "")
495
+ state["status"] = f"Step {step}/{state['max_steps']}: Video-Coder is writing ({len(state['raw'])} chars)"
496
+ yield ui(state)
497
+ state["used"] += n or 0
498
+ print(f"step {step}: chunk {time.time() - t0:.1f}s, {n} tokens, finished={finished}")
499
+ if not finished and state["used"] < state["max_new_tokens"]:
500
+ yield ui(state, tick=time.time()) # continue this turn in the next request
501
+ return
502
+
503
+ # ---- the turn is complete: execute it ----
504
+ exit_status = None
505
+ gen = _finish_turn(state)
506
+ while True:
507
+ try:
508
+ next(gen)
509
+ yield ui(state)
510
+ except StopIteration as stop:
511
+ exit_status = stop.value
512
+ break
513
+ state["step"] += 1
514
+ if exit_status is None and state["step"] > state["max_steps"]:
515
+ exit_status = "StepLimit"
516
+ if exit_status is None:
517
+ state["status"] = f"Step {step}/{state['max_steps']} done"
518
+ yield ui(state, tick=time.time())
519
+ return
520
+ state["done"] = True
521
+ if exit_status == "Submitted":
522
+ for _ in _accept(state):
523
+ yield ui(state)
524
  else:
525
+ state["status"] = {
526
+ "StepLimit": "Step limit reached before the agent submitted (showing the latest render)",
527
+ "RepeatedActions": "Stopped: the agent repeated the same command",
528
+ "FormatError": "Stopped after repeated format errors",
529
+ }[exit_status]
530
+ yield ui(state)
531
+
532
+
533
+ def stop_run(state: dict):
534
+ """Stop the current run."""
535
+ if state:
536
+ state["done"] = True
537
+ state["status"] = "Stopped"
538
+ return ui(state)
539
+ return (state, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update())
540
 
541
 
542
  # --------------------------------------------------------------------------------------------------------------------
 
549
 
550
  EXAMPLES = [
551
  "a man standing in front of a machine.",
552
+ "A single polished walnut-wood chess pawn stands on a small square pedestal of white Carrara marble. No other "
553
+ "objects are in the scene. The camera makes one complete 360-degree orbit around the pawn while slowly rising.",
554
  "The video shows a sequence of three identical white cars driving on a road. The first car is in the foreground, "
555
  "the second car is slightly behind and to the right, and the third car is in the background. The cars are moving "
556
  "from left to right across the frame. The road appears to be a city street with a dark grey surface. The "
 
563
  "clear, suggesting a sunny day. The boats are the main focus of the video, with their movement and the water's "
564
  "reaction to their speed creating a dynamic and exciting scene. The overall style of the video is a dynamic "
565
  "aerial shot, capturing the movement and beauty of the ocean and the boats.",
 
 
566
  ]
567
 
568
  with gr.Blocks(title="Video-Coder") as demo:
 
576
  "[📖 Paper](https://huggingface.co/papers/2611.09894) · [🌐 Project page](https://cywang735.github.io/"
577
  "Video-Coder/) · [💻 Code](https://github.com/CYWang735/Video-Coder) · [🤗 Model](https://huggingface.co/"
578
  "Video-Coder/Video-Coder-9B)\n\n"
579
+ "⏳ A full agent run takes several minutes: the first turn writes a long script, and every `run_blender` "
580
+ "renders the animation with Blender on CPU. The trajectory streams live; keep this tab open."
581
  )
582
  with gr.Row():
583
  prompt = gr.Textbox(label="Scene description", lines=3, scale=4,
584
  placeholder="Describe the scene and its motion, e.g. 'a red ball rolls down a ramp "
585
  "and knocks over three dominoes'")
586
+ with gr.Column(scale=1, min_width=180):
587
+ run_btn = gr.Button("Generate draft video", variant="primary")
588
+ stop_btn = gr.Button("Stop", variant="stop")
589
  status = gr.Markdown()
590
  with gr.Row():
591
  with gr.Column(scale=1):
 
604
  top_p = gr.Slider(0.5, 1.0, value=0.95, step=0.01, label="Top-p")
605
  max_new_tokens = gr.Slider(2048, 32768, value=16384, step=1024, label="Max tokens per agent turn")
606
 
607
+ state = gr.State(None)
608
+ tick = gr.Number(value=0, visible=False)
609
+ outputs = [state, chat_out, video_out, code_out, preview_out, status, tick]
610
+ inputs = [prompt, max_steps, temperature, top_p, max_new_tokens]
611
+ run_btn.click(start, inputs=inputs, outputs=outputs, api_name="start")
612
+ prompt.submit(start, inputs=inputs, outputs=outputs, api_visibility="private")
613
+ # Each agent step is its own request (fresh ZeroGPU token); the tick change chains the next one.
614
+ adv = tick.change(advance, inputs=[state], outputs=outputs, api_name="advance", concurrency_limit=None)
615
+ stop_btn.click(stop_run, inputs=[state], outputs=outputs, cancels=[adv], api_name="stop")
616
+ gr.Examples(examples=[[e] for e in EXAMPLES], inputs=[prompt], outputs=outputs, fn=start,
617
  cache_examples=False, run_on_click=True)
618
 
619
  if __name__ == "__main__":