Spaces:
Running on Zero
Running on Zero
Per-step requests (fresh ZeroGPU token each GPU call)
Browse files
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
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 305 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
script = workdir / SCRIPT
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
return
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
else:
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 517 |
-
"on CPU
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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__":
|