Spaces:
Running on Zero
Running on Zero
Restore working harness app (SFT prompt, per-step GPU requests); add bird example
Browse files- README.md +13 -13
- app.py +504 -399
- packages.txt +1 -0
- requirements.txt +3 -4
README.md
CHANGED
|
@@ -6,26 +6,26 @@ colorTo: pink
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.31.0
|
| 8 |
app_file: app.py
|
| 9 |
-
short_description: Agent writes Blender code to build a draft video
|
| 10 |
python_version: "3.12"
|
| 11 |
startup_duration_timeout: 1h
|
| 12 |
license: apache-2.0
|
| 13 |
models:
|
| 14 |
- Video-Coder/Video-Coder-9B
|
| 15 |
-
- MiniMaxAI/MiniMax-H3
|
| 16 |
---
|
| 17 |
|
| 18 |
-
# Video-Coder
|
| 19 |
|
| 20 |
-
Demo of [Video-Coder
|
| 21 |
-
[code](https://github.com/CYWang735/Video-Coder)
|
| 22 |
-
it writes a Blender 4.5 Python script for your scene description, checks and renders it with `check_script` /
|
| 23 |
-
`run_blender` (bpy 4.5.14, Workbench, Mesa llvmpipe on CPU), inspects the rendered frames, revises, and submits. The
|
| 24 |
-
submitted script is re-rendered from scratch and becomes the **draft video**.
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.31.0
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Agent writes Blender code to build a 3D draft video
|
| 10 |
python_version: "3.12"
|
| 11 |
startup_duration_timeout: 1h
|
| 12 |
license: apache-2.0
|
| 13 |
models:
|
| 14 |
- Video-Coder/Video-Coder-9B
|
|
|
|
| 15 |
---
|
| 16 |
|
| 17 |
+
# Video-Coder
|
| 18 |
|
| 19 |
+
Demo of [Video-Coder: Reinforcing Agentic Coding for Video Generation](https://huggingface.co/papers/2611.09894)
|
| 20 |
+
([code](https://github.com/CYWang735/Video-Coder), [model](https://huggingface.co/Video-Coder/Video-Coder-9B)).
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
**Video-Coder-9B** runs inside a port of the authors' *Video-Coder Harness*: it writes a Blender 4.5 Python script
|
| 23 |
+
for the scene description, renders it (`run_blender`, bpy 4.5.14 / Workbench on CPU), looks at the rendered frames,
|
| 24 |
+
fixes problems and submits. The output is the **draft video** (white-model render with the scene's layout, motion and
|
| 25 |
+
camera).
|
| 26 |
|
| 27 |
+
The paper then turns the draft video into a final video with MiniMax-H3 (Ref2VA). That ~33B audio-video generator
|
| 28 |
+
(plus its large text encoder) does not fit alongside the agent in this Space, so this demo stops at the draft video.
|
| 29 |
+
|
| 30 |
+
The harness code in `harness/` is taken from the Video-Coder repository (`Video-Coder-RL/uni-agent`, Apache-2.0).
|
| 31 |
+
Example prompts are from the Video-Coder project page and repository.
|
app.py
CHANGED
|
@@ -1,27 +1,19 @@
|
|
| 1 |
-
"""Video-Coder: an agent that writes Blender code to build a draft video (Video-Coder-9B + the Video-Coder Harness).
|
| 2 |
-
|
| 3 |
-
Port of the authors' inference path (examples/video_coder in https://github.com/CYWang735/Video-Coder):
|
| 4 |
-
* same system / instance prompts, tool (`bash`), observation and format-error templates (harness/teacher.yaml),
|
| 5 |
-
* same `check_script` / `run_blender` tools (copied harness code: bpy 4.5.14, Workbench on Mesa llvmpipe, CPU),
|
| 6 |
-
with visual feedback on (the rendered frames go back to the model as images), as in their inference config,
|
| 7 |
-
* same sampling (temperature 1.0, top_p 0.95) and acceptance (submit + static check + fresh re-render).
|
| 8 |
-
The policy runs with transformers on ZeroGPU, one GPU call per agent turn; Blender runs on the CPU in between.
|
| 9 |
-
Stage 2 (optional) sends the draft to MiniMax-H3 Ref2VA with the authors' prompt template through a public Space.
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
import os
|
| 13 |
|
| 14 |
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 15 |
|
| 16 |
-
import spaces # noqa: E402 (before torch)
|
| 17 |
|
|
|
|
| 18 |
import difflib # noqa: E402
|
|
|
|
|
|
|
| 19 |
import json # noqa: E402
|
| 20 |
import re # noqa: E402
|
| 21 |
import shutil # noqa: E402
|
|
|
|
| 22 |
import subprocess # noqa: E402
|
| 23 |
import sys # noqa: E402
|
| 24 |
-
import sysconfig # noqa: E402
|
| 25 |
import tempfile # noqa: E402
|
| 26 |
import threading # noqa: E402
|
| 27 |
import time # noqa: E402
|
|
@@ -30,64 +22,90 @@ from pathlib import Path # noqa: E402
|
|
| 30 |
|
| 31 |
import gradio as gr # noqa: E402
|
| 32 |
import torch # noqa: E402
|
| 33 |
-
import yaml # noqa: E402
|
| 34 |
from jinja2 import StrictUndefined, Template # noqa: E402
|
|
|
|
| 35 |
from transformers import AutoModelForImageTextToText, AutoProcessor, TextIteratorStreamer # noqa: E402
|
| 36 |
|
| 37 |
-
|
| 38 |
-
HARNESS_ROOT =
|
| 39 |
sys.path.insert(0, str(HARNESS_ROOT))
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
from video_coder_harness.static_check import check_file # noqa: E402
|
| 44 |
|
| 45 |
MODEL_ID = "Video-Coder/Video-Coder-9B"
|
| 46 |
-
H3_SPACE = os.environ.get("H3_SPACE", "multimodalart/minimax-h3-reference")
|
| 47 |
-
RUNS_DIR = Path(tempfile.gettempdir()) / "video_coder_runs"
|
| 48 |
-
RUNS_DIR.mkdir(parents=True, exist_ok=True)
|
| 49 |
|
| 50 |
# --------------------------------------------------------------------------------------------------------------------
|
| 51 |
-
# Blender
|
| 52 |
-
#
|
| 53 |
# --------------------------------------------------------------------------------------------------------------------
|
| 54 |
-
BPY_ENV = Path(os.environ.get("BPY_ENV", str(Path.home() / "
|
| 55 |
-
|
| 56 |
|
| 57 |
|
| 58 |
def setup_bpy() -> None:
|
| 59 |
-
if
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
| 62 |
t0 = time.time()
|
| 63 |
-
subprocess.run([uv, "venv", "-q", "-p", "3.11", str(BPY_ENV)], check=True)
|
| 64 |
-
subprocess.run([uv, "pip", "install", "-q", "-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
|
| 68 |
setup_bpy()
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
"
|
| 79 |
-
"blender_available": HARNESS["blender_available"],
|
| 80 |
-
"fps": HARNESS["fps"],
|
| 81 |
-
"frame_count": HARNESS["frame_count"],
|
| 82 |
-
"max_frame_count": HARNESS.get("max_frame_count", HARNESS["frame_count"]),
|
| 83 |
-
"resolution": HARNESS["resolution"],
|
| 84 |
-
"script_name": HARNESS["script_name"],
|
| 85 |
-
"output_video": HARNESS["output_video"],
|
| 86 |
"visual_feedback": True,
|
| 87 |
-
"preview_frames":
|
| 88 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
}
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
"type": "function",
|
| 92 |
"function": {
|
| 93 |
"name": "bash",
|
|
@@ -98,422 +116,509 @@ BASH_TOOL = { # mini-swe-agent's tool definition (minisweagent.models.utils.act
|
|
| 98 |
"required": ["command"],
|
| 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 |
-
w, h = HARNESS["resolution"]
|
| 131 |
-
with tempfile.TemporaryDirectory() as tmp:
|
| 132 |
-
p = Path(tmp) / "preflight.py"
|
| 133 |
-
p.write_text(script.format(frames=HARNESS["frame_count"], w=w, h=h, fps=HARNESS["fps"], out=HARNESS["output_video"]))
|
| 134 |
-
r = execute(p, tmp, dict(HARNESS, resolution=[w // 10, h // 10]), preview=False)
|
| 135 |
-
print(f"[bpy] preflight ok={r['ok']} in {r['seconds']}s (cpus={os.cpu_count()}) problems={r['problems']} "
|
| 136 |
-
f"error={r.get('error')} tb={r.get('traceback')} log={r.get('log_tail')}", flush=True)
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
preflight()
|
| 140 |
|
| 141 |
# --------------------------------------------------------------------------------------------------------------------
|
| 142 |
-
#
|
| 143 |
# --------------------------------------------------------------------------------------------------------------------
|
| 144 |
processor = AutoProcessor.from_pretrained(MODEL_ID)
|
|
|
|
| 145 |
model = AutoModelForImageTextToText.from_pretrained(MODEL_ID, dtype=torch.bfloat16).to("cuda").eval()
|
| 146 |
-
TOKENS_PER_SECOND = 22.0 # measured decode speed on ZeroGPU (large), used to size each turn's GPU reservation
|
| 147 |
-
|
| 148 |
|
| 149 |
-
|
| 150 |
-
return int(min(600, 25 + int(max_new_tokens) / TOKENS_PER_SECOND))
|
| 151 |
|
| 152 |
|
| 153 |
-
@spaces.GPU(duration=
|
| 154 |
-
def
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
| 160 |
result = {}
|
| 161 |
|
| 162 |
-
def
|
| 163 |
try:
|
| 164 |
-
out = model.generate(
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
)
|
| 168 |
-
|
| 169 |
-
except Exception as e: # surfaced after the stream ends
|
| 170 |
result["error"] = f"{type(e).__name__}: {e}"
|
| 171 |
streamer.end()
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
text = ""
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
| 180 |
if "error" in result:
|
| 181 |
-
raise gr.Error(
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
finish = "stop"
|
| 185 |
-
text = text.replace("<|im_end|>", "").replace("<|endoftext|>", "")
|
| 186 |
-
yield text, True, finish
|
| 187 |
|
| 188 |
|
| 189 |
# --------------------------------------------------------------------------------------------------------------------
|
| 190 |
-
#
|
|
|
|
| 191 |
# --------------------------------------------------------------------------------------------------------------------
|
| 192 |
-
_FUNC = re.compile(r"<function=([^>\n]+)>(.*?)</function>", re.S)
|
| 193 |
-
_PARAM = re.compile(r"<parameter=([^>\n]+)>(.*?)</parameter>", re.S)
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
def parse_reply(text: str):
|
| 197 |
-
"""Qwen3-coder tool-call format -> (content before the first call, [(name, args)])."""
|
| 198 |
-
idx = text.find("<tool_call>")
|
| 199 |
-
content = text if idx < 0 else text[:idx]
|
| 200 |
-
calls = []
|
| 201 |
-
if idx >= 0:
|
| 202 |
-
for m in _FUNC.finditer(text[idx:]):
|
| 203 |
-
args = {}
|
| 204 |
-
for p in _PARAM.finditer(m.group(2)):
|
| 205 |
-
v = p.group(2)
|
| 206 |
-
v = v[1:] if v.startswith("\n") else v
|
| 207 |
-
v = v[:-1] if v.endswith("\n") else v
|
| 208 |
-
args[p.group(1).strip()] = v
|
| 209 |
-
calls.append((m.group(1).strip(), args))
|
| 210 |
-
return content.rstrip(), calls
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
def split_reasoning(content: str):
|
| 214 |
-
if "</think>" in content:
|
| 215 |
-
r, c = content.split("</think>", 1)
|
| 216 |
-
return r.replace("<think>", "").strip(), c.strip()
|
| 217 |
-
return "", content.strip()
|
| 218 |
-
|
| 219 |
-
|
| 220 |
def agent_env() -> dict:
|
| 221 |
-
env =
|
| 222 |
-
env
|
|
|
|
| 223 |
env["PATH"] = f"{HARNESS_ROOT / 'bin'}:{os.environ.get('PATH', '')}"
|
| 224 |
env["VCSFT_PYTHON"] = sys.executable
|
| 225 |
env["VCSFT_HARNESS"] = json.dumps(HARNESS)
|
| 226 |
return env
|
| 227 |
|
| 228 |
|
| 229 |
-
def run_command(command: str, cwd:
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
"For a quick experiment, write a small script in the current directory and run `run_blender "
|
| 235 |
-
"experiment.py`; its print() output appears in the blender log.").format(reason=reason)
|
| 236 |
-
return {"output": msg, "returncode": 1, "exception_info": ""}
|
| 237 |
try:
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
| 251 |
images = []
|
| 252 |
|
| 253 |
def repl(m):
|
| 254 |
if m.group(1).strip() != "image_url":
|
| 255 |
return m.group(0)
|
| 256 |
before = text[: m.start()].rstrip().splitlines()
|
| 257 |
-
|
|
|
|
| 258 |
return f"[image {len(images)} attached below]"
|
| 259 |
|
| 260 |
-
|
| 261 |
-
body = render_template(MODEL_CFG["observation_template"], output={**output, "output": stripped})
|
| 262 |
-
if not images:
|
| 263 |
-
return body, body, []
|
| 264 |
-
content = [{"type": "text", "text": body}]
|
| 265 |
-
for k, (caption, url) in enumerate(images, 1):
|
| 266 |
-
content += [{"type": "text", "text": f"image {k}: {caption}" if caption else f"image {k}:"},
|
| 267 |
-
{"type": "image", "image": url}]
|
| 268 |
-
return content, body, images
|
| 269 |
|
| 270 |
|
| 271 |
-
def
|
| 272 |
-
return
|
| 273 |
|
| 274 |
|
| 275 |
-
def
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
-
|
| 283 |
-
|
|
|
|
| 284 |
return p.read_text(errors="replace") if p.is_file() else ""
|
| 285 |
|
| 286 |
|
| 287 |
-
def
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
Args:
|
| 291 |
-
prompt:
|
| 292 |
-
max_steps: maximum number of agent turns (
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
(agent trace, Blender script, draft video path, status)
|
| 297 |
"""
|
| 298 |
if not prompt or not prompt.strip():
|
| 299 |
-
raise gr.Error("
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
]
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
if
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
messages.append({"role": "tool", "content": obs})
|
| 376 |
-
chat.append({"role": "assistant", "content": f"```\n{truncate(obs_text)}\n```",
|
| 377 |
-
"metadata": {"title": f"π οΈ Output (exit {out['returncode']})"}})
|
| 378 |
-
script = read_script(ws) or script
|
| 379 |
-
preview = ws / "preview.png"
|
| 380 |
-
if images and preview.is_file():
|
| 381 |
-
keep = root / f"preview_{n_calls}.png"
|
| 382 |
-
shutil.copy(preview, keep)
|
| 383 |
-
chat.append({"role": "assistant", "content": gr.Image(str(keep), label=f"Render after step {n_calls}")})
|
| 384 |
-
if (ws / HARNESS["output_video"]).is_file() and "RUN OK" in (out.get("output") or ""):
|
| 385 |
-
keep = root / f"render_{n_calls}.mp4"
|
| 386 |
-
shutil.copy(ws / HARNESS["output_video"], keep)
|
| 387 |
-
draft = str(keep)
|
| 388 |
-
yield chat, script, draft, f"Step {n_calls} done ({time.time() - t_start:.0f}s)"
|
| 389 |
-
if stop:
|
| 390 |
-
break
|
| 391 |
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
else:
|
| 411 |
-
status =
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
-
from video_coder_harness.video import probe
|
| 439 |
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
video, _ = client.predict(
|
| 448 |
-
prompt=H3_TEMPLATE.format(scene=(prompt or "").strip()),
|
| 449 |
-
video_path={"video": handle_file(draft_video)},
|
| 450 |
-
canvas="960x544 Β· 16:9 fast", match=False, duration=int(max(2, min(14, round(seconds)))),
|
| 451 |
-
steps=28, seed=42, upsample=False, api_name="/generate",
|
| 452 |
-
)
|
| 453 |
-
except Exception as e:
|
| 454 |
-
raise gr.Error(f"MiniMax-H3 ({H3_SPACE}) failed: {type(e).__name__}: {e}")
|
| 455 |
-
return video["video"] if isinstance(video, dict) else video
|
| 456 |
|
| 457 |
|
| 458 |
# --------------------------------------------------------------------------------------------------------------------
|
| 459 |
# UI
|
| 460 |
# --------------------------------------------------------------------------------------------------------------------
|
| 461 |
-
EXAMPLES = [ # the authors' qualitative examples (project page) and their H3 demo scene
|
| 462 |
-
"The video captures a small bird perched on a tree branch. The bird, with its brown and white feathers, yellow beak, and black and white head, is the main focus of the video. The bird is seen in three different positions on the branch, each time facing a different direction. The background of the video is filled with green foliage, providing a natural and serene setting for the bird. The style of the video is a close-up shot, allowing the viewer to appreciate the details of the bird and its surroundings. The video does not contain any text or additional objects. The bird's movements and the changing angles of the shot create a sense of dynamism and life in the video.",
|
| 463 |
-
"The video shows a sequence of three identical white cars driving on a road. The first car is in the foreground, the second car is slightly behind and to the right, and the third car is in the background. The cars are moving from left to right across the frame. The road appears to be a city street with a dark grey surface. The background features a city skyline with buildings of various heights, and the sky is a gradient of orange and yellow, suggesting either sunrise or sunset. The style of the video is realistic with a focus on the cars and their movement.",
|
| 464 |
-
"A single polished walnut-wood chess pawn stands on a small square pedestal of white Carrara marble. No other objects are in the scene. Dark charcoal studio backdrop, soft key light from the upper left with a gentle rim light, subtle reflections on the marble. The camera makes one complete 360-degree orbit around the pawn while slowly rising.",
|
| 465 |
-
"The video captures a lively scene on the ocean, with three boats in motion. The first boat, a large white yacht, is seen in the distance, sailing towards the horizon. The second and third boats are closer to the viewer, both moving at high speeds, leaving trails of white water behind them. The ocean is a deep blue, and the sky above is clear, suggesting a sunny day. The boats are the main focus of the video, with their movement and the water's reaction to their speed creating a dynamic and exciting scene. The overall style of the video is a dynamic aerial shot, capturing the movement and beauty of the ocean and the boats.",
|
| 466 |
-
"In the video, a woman with long dark hair and glasses is seen enjoying a meal. She is wearing a black jacket and is seated in a room with a white wall. In the first frame, she is holding a bowl of food and appears to be in the process of eating. In the second frame, she is seen taking a bite of her food. In the third frame, she is seen holding a spoon and appears to be in the process of eating again. The video captures the woman's enjoyment of her meal and the casual atmosphere of the setting.",
|
| 467 |
-
]
|
| 468 |
-
|
| 469 |
-
INTRO = """# π¬ Video-Coder
|
| 470 |
-
**Video-Coder-9B** is a coding agent: given a scene description it writes a Blender Python program, runs it, looks at the
|
| 471 |
-
rendered frames, fixes what is wrong, and submits a **draft video**, an untextured 3D render whose layout, motion and
|
| 472 |
-
camera then guide a video generator ([MiniMax-H3](https://huggingface.co/MiniMaxAI/MiniMax-H3)).
|
| 473 |
-
|
| 474 |
-
[π Paper](https://huggingface.co/papers/2611.09894) Β· [π» Code](https://github.com/CYWang735/Video-Coder) Β·
|
| 475 |
-
[π€ Model](https://huggingface.co/Video-Coder/Video-Coder-9B) Β· [π Project page](https://cywang735.github.io/Video-Coder/)
|
| 476 |
-
|
| 477 |
-
A run takes several minutes: each agent turn is a GPU call and every `run_blender` renders 5β10 s of 720p video on CPU.
|
| 478 |
-
"""
|
| 479 |
-
|
| 480 |
CSS = """
|
| 481 |
-
#col-container { max-width:
|
| 482 |
.dark .gradio-container { color: var(--body-text-color); }
|
| 483 |
"""
|
| 484 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
with gr.Blocks(title="Video-Coder") as demo:
|
| 486 |
with gr.Column(elem_id="col-container"):
|
| 487 |
-
gr.Markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
with gr.Row():
|
| 489 |
-
with gr.Column(scale=
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
final_btn.click(render_final, [prompt, draft], [final], api_name="render_final")
|
| 517 |
|
| 518 |
if __name__ == "__main__":
|
| 519 |
-
demo.queue(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
|
| 3 |
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 4 |
|
| 5 |
+
import spaces # noqa: E402 (must come before torch)
|
| 6 |
|
| 7 |
+
import base64 # noqa: E402
|
| 8 |
import difflib # noqa: E402
|
| 9 |
+
import hashlib # noqa: E402
|
| 10 |
+
import io # noqa: E402
|
| 11 |
import json # noqa: E402
|
| 12 |
import re # noqa: E402
|
| 13 |
import shutil # noqa: E402
|
| 14 |
+
import signal # noqa: E402
|
| 15 |
import subprocess # noqa: E402
|
| 16 |
import sys # noqa: E402
|
|
|
|
| 17 |
import tempfile # noqa: E402
|
| 18 |
import threading # noqa: E402
|
| 19 |
import time # noqa: E402
|
|
|
|
| 22 |
|
| 23 |
import gradio as gr # noqa: E402
|
| 24 |
import torch # noqa: E402
|
|
|
|
| 25 |
from jinja2 import StrictUndefined, Template # noqa: E402
|
| 26 |
+
from PIL import Image # noqa: E402
|
| 27 |
from transformers import AutoModelForImageTextToText, AutoProcessor, TextIteratorStreamer # noqa: E402
|
| 28 |
|
| 29 |
+
ROOT = Path(__file__).resolve().parent
|
| 30 |
+
HARNESS_ROOT = ROOT / "harness"
|
| 31 |
sys.path.insert(0, str(HARNESS_ROOT))
|
| 32 |
+
for _tool in (HARNESS_ROOT / "bin").iterdir(): # the Hub does not keep the executable bit
|
| 33 |
+
_tool.chmod(0o755)
|
| 34 |
+
|
| 35 |
+
from video_coder_harness.blender_exec import execute as blender_execute # noqa: E402
|
| 36 |
+
from video_coder_harness.guard import ( # noqa: E402
|
| 37 |
+
BLOCKED,
|
| 38 |
+
REPEAT_WARNING,
|
| 39 |
+
_normalize_output,
|
| 40 |
+
harness_violation,
|
| 41 |
+
)
|
| 42 |
from video_coder_harness.static_check import check_file # noqa: E402
|
| 43 |
|
| 44 |
MODEL_ID = "Video-Coder/Video-Coder-9B"
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
# --------------------------------------------------------------------------------------------------------------------
|
| 47 |
+
# Blender 4.5 (bpy 4.5.14 on Python 3.11, same as the authors' bpy-agent.sif sandbox), rendered on CPU with Mesa
|
| 48 |
+
# llvmpipe. Installed into its own venv at startup because the Space itself runs Python 3.12.
|
| 49 |
# --------------------------------------------------------------------------------------------------------------------
|
| 50 |
+
BPY_ENV = Path(os.environ.get("BPY_ENV", str(Path.home() / "bpyenv")))
|
| 51 |
+
BPY_PY = BPY_ENV / "bin" / "python"
|
| 52 |
|
| 53 |
|
| 54 |
def setup_bpy() -> None:
|
| 55 |
+
if BPY_PY.exists():
|
| 56 |
+
r = subprocess.run([str(BPY_PY), "-c", "import bpy; print(bpy.app.version_string)"],
|
| 57 |
+
capture_output=True, text=True)
|
| 58 |
+
if r.returncode == 0:
|
| 59 |
+
print("bpy ready:", r.stdout.strip())
|
| 60 |
+
return
|
| 61 |
t0 = time.time()
|
| 62 |
+
subprocess.run([sys.executable, "-m", "uv", "venv", "-q", "-p", "3.11", "--clear", str(BPY_ENV)], check=True)
|
| 63 |
+
subprocess.run([sys.executable, "-m", "uv", "pip", "install", "-q", "-p", str(BPY_PY), "bpy==4.5.14"],
|
| 64 |
+
check=True)
|
| 65 |
+
r = subprocess.run([str(BPY_PY), "-c", "import bpy; print(bpy.app.version_string)"],
|
| 66 |
+
capture_output=True, text=True)
|
| 67 |
+
print(f"bpy install took {time.time() - t0:.0f}s:", r.stdout.strip(), r.stderr[-2000:])
|
| 68 |
|
| 69 |
|
| 70 |
setup_bpy()
|
| 71 |
|
| 72 |
+
# Harness settings: configs/teacher.yaml + configs/uni_container_visual.yaml (the RL / inference setting).
|
| 73 |
+
HARNESS = {
|
| 74 |
+
"blender_version": "4.5",
|
| 75 |
+
"blender_available": True,
|
| 76 |
+
"bpy_python": str(BPY_PY),
|
| 77 |
+
"bpy_libs": "/usr",
|
| 78 |
+
"exec_timeout": 600,
|
| 79 |
+
"summary_samples": 6,
|
| 80 |
+
"content_samples": 8,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
"visual_feedback": True,
|
| 82 |
+
"preview_frames": 8,
|
| 83 |
+
"preview_width": 640,
|
| 84 |
+
"judge": {"enabled": False},
|
| 85 |
+
"output_video": "output.mp4",
|
| 86 |
+
"fps": 24,
|
| 87 |
+
"frame_count": 120,
|
| 88 |
+
"max_frame_count": 240,
|
| 89 |
+
"resolution": [1280, 720],
|
| 90 |
+
"script_name": "main.py",
|
| 91 |
}
|
| 92 |
+
SCRIPT = HARNESS["script_name"]
|
| 93 |
+
CMD_TIMEOUT = 900
|
| 94 |
+
REPEAT_WARN, REPEAT_STOP, REPEAT_SIM = 3, 6, 0.9
|
| 95 |
+
MAX_FORMAT_ERRORS = 3
|
| 96 |
+
RUNS_DIR = Path(tempfile.gettempdir()) / "video_coder_runs"
|
| 97 |
+
RUNS_DIR.mkdir(parents=True, exist_ok=True)
|
| 98 |
+
_SECRET_ENV = re.compile(r"(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)", re.I)
|
| 99 |
+
HARNESS_MARKERS = [str(HARNESS_ROOT), str(BPY_ENV)]
|
| 100 |
+
|
| 101 |
+
SYSTEM_PROMPT = (
|
| 102 |
+
"You are an expert Blender technical artist and Python programmer. You write Blender Python (bpy) scripts that "
|
| 103 |
+
"build and animate 3D scenes and render them as untextured white-model (clay) videos. You work in a Linux shell "
|
| 104 |
+
"through a bash tool."
|
| 105 |
+
)
|
| 106 |
+
# The exact user prompt of the SFT trajectories (Video-Coder/Train-Data sft_data.json), with the scene as __TASK__.
|
| 107 |
+
INSTANCE_TEMPLATE = (HARNESS_ROOT / "instance_template.txt").read_text()
|
| 108 |
+
TOOLS = [{
|
| 109 |
"type": "function",
|
| 110 |
"function": {
|
| 111 |
"name": "bash",
|
|
|
|
| 116 |
"required": ["command"],
|
| 117 |
},
|
| 118 |
},
|
| 119 |
+
}]
|
| 120 |
+
OBSERVATION_TEMPLATE = Template("""{%- if output.output | length < 10000 -%}
|
| 121 |
+
{
|
| 122 |
+
"returncode": {{ output.returncode }},
|
| 123 |
+
"output": {{ output.output | tojson }}
|
| 124 |
+
{%- if output.exception_info %}, "exception_info": {{ output.exception_info | tojson }}{% endif %}
|
| 125 |
}
|
| 126 |
+
{%- else -%}
|
| 127 |
+
{
|
| 128 |
+
"returncode": {{ output.returncode }},
|
| 129 |
+
"output_head": {{ output.output[:5000] | tojson }},
|
| 130 |
+
"output_tail": {{ output.output[-5000:] | tojson }},
|
| 131 |
+
"elided_chars": {{ output.output | length - 10000 }},
|
| 132 |
+
"warning": "Output too long."
|
| 133 |
+
{%- if output.exception_info %}, "exception_info": {{ output.exception_info | tojson }}{% endif %}
|
| 134 |
+
}
|
| 135 |
+
{%- endif -%}
|
| 136 |
+
""", undefined=StrictUndefined)
|
| 137 |
+
FORMAT_ERROR_TEMPLATE = Template("""{% if finish_reason is defined and (finish_reason == "length" or (finish_reason == "tool_calls" and not has_tool_calls)) -%}
|
| 138 |
+
Your previous response reached the output token limit (finish_reason={{ finish_reason }}) before you produced a tool call, so it was cut off. Respond more concisely and finish with exactly one bash tool call.
|
| 139 |
+
{%- else -%}
|
| 140 |
+
Tool call error:
|
| 141 |
+
|
| 142 |
+
<error>
|
| 143 |
+
{{error}}
|
| 144 |
+
</error>
|
| 145 |
+
|
| 146 |
+
Every response must call the 'bash' tool exactly once with {"command": "..."}.
|
| 147 |
+
To finish, run `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT` on its own.
|
| 148 |
+
{%- endif %}
|
| 149 |
+
""")
|
| 150 |
+
MM_TAG = re.compile(r"(?s)<MSWEA_MULTIMODAL_CONTENT><CONTENT_TYPE>(.+?)</CONTENT_TYPE>(.+?)</MSWEA_MULTIMODAL_CONTENT>")
|
| 151 |
+
TOOL_CALL_RE = re.compile(r"<tool_call>\s*<function=([^>\s]+)>(.*?)</function>\s*</tool_call>", re.S)
|
| 152 |
+
PARAM_RE = re.compile(r"<parameter=([^>\s]+)>\n?(.*?)\n?</parameter>", re.S)
|
| 153 |
+
END_TOKENS = ("<|im_end|>", "<|endoftext|>")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
# --------------------------------------------------------------------------------------------------------------------
|
| 156 |
+
# Model: Video-Coder-9B (Qwen3.5-9B, vision-language) in bf16, fully on the GPU.
|
| 157 |
# --------------------------------------------------------------------------------------------------------------------
|
| 158 |
processor = AutoProcessor.from_pretrained(MODEL_ID)
|
| 159 |
+
tokenizer = processor.tokenizer
|
| 160 |
model = AutoModelForImageTextToText.from_pretrained(MODEL_ID, dtype=torch.bfloat16).to("cuda").eval()
|
|
|
|
|
|
|
| 161 |
|
| 162 |
+
GPU_CHUNK_SECONDS = 90 # one GPU call generates for at most this long; long turns continue in the next call
|
|
|
|
| 163 |
|
| 164 |
|
| 165 |
+
@spaces.GPU(duration=120)
|
| 166 |
+
def llm_chunk(messages: list, images: list, prefix: str = "", max_new_tokens: int = 16384,
|
| 167 |
+
temperature: float = 1.0, top_p: float = 0.95):
|
| 168 |
+
"""Continue the agent's current turn for up to GPU_CHUNK_SECONDS; yields (text so far, finished, n_tokens)."""
|
| 169 |
+
prompt = tokenizer.apply_chat_template(messages, tools=TOOLS, add_generation_prompt=True, tokenize=False)
|
| 170 |
+
prompt += prefix
|
| 171 |
+
inputs = processor(text=[prompt], images=images or None, return_tensors="pt").to("cuda")
|
| 172 |
+
n_in = inputs["input_ids"].shape[1]
|
| 173 |
+
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=False)
|
| 174 |
result = {}
|
| 175 |
|
| 176 |
+
def _run():
|
| 177 |
try:
|
| 178 |
+
out = model.generate(**inputs, streamer=streamer, max_new_tokens=int(max_new_tokens), do_sample=True,
|
| 179 |
+
temperature=float(temperature), top_p=float(top_p), top_k=0, use_cache=True,
|
| 180 |
+
max_time=GPU_CHUNK_SECONDS)
|
| 181 |
+
result["n"] = int(out.shape[1] - n_in)
|
| 182 |
+
except Exception as e: # surfaced below
|
|
|
|
| 183 |
result["error"] = f"{type(e).__name__}: {e}"
|
| 184 |
streamer.end()
|
| 185 |
|
| 186 |
+
th = threading.Thread(target=_run, daemon=True)
|
| 187 |
+
th.start()
|
| 188 |
text = ""
|
| 189 |
+
last = 0.0
|
| 190 |
+
for piece in streamer:
|
| 191 |
+
text += piece
|
| 192 |
+
if time.time() - last > 0.4:
|
| 193 |
+
last = time.time()
|
| 194 |
+
yield text, False, None
|
| 195 |
+
th.join()
|
| 196 |
if "error" in result:
|
| 197 |
+
raise gr.Error(result["error"])
|
| 198 |
+
finished = any(t in text for t in END_TOKENS)
|
| 199 |
+
yield text, finished, result.get("n", 0)
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
|
| 202 |
# --------------------------------------------------------------------------------------------------------------------
|
| 203 |
+
# Agent loop: mini-swe-agent DefaultAgent + video_coder_harness GuardedAgent semantics, re-implemented without the
|
| 204 |
+
# litellm/vLLM gateway (the model is called in-process).
|
| 205 |
# --------------------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
def agent_env() -> dict:
|
| 207 |
+
env = dict(os.environ)
|
| 208 |
+
env.update({k: "" for k in os.environ if _SECRET_ENV.search(k)})
|
| 209 |
+
env.update({"PAGER": "cat", "MANPAGER": "cat", "PIP_PROGRESS_BAR": "off", "TQDM_DISABLE": "1"})
|
| 210 |
env["PATH"] = f"{HARNESS_ROOT / 'bin'}:{os.environ.get('PATH', '')}"
|
| 211 |
env["VCSFT_PYTHON"] = sys.executable
|
| 212 |
env["VCSFT_HARNESS"] = json.dumps(HARNESS)
|
| 213 |
return env
|
| 214 |
|
| 215 |
|
| 216 |
+
def run_command(command: str, cwd: str) -> dict:
|
| 217 |
+
"""mini-swe-agent LocalEnvironment.execute."""
|
| 218 |
+
proc = subprocess.Popen(command, shell=True, text=True, cwd=cwd, env=agent_env(), encoding="utf-8",
|
| 219 |
+
errors="replace", stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
| 220 |
+
start_new_session=True)
|
|
|
|
|
|
|
|
|
|
| 221 |
try:
|
| 222 |
+
stdout, _ = proc.communicate(timeout=CMD_TIMEOUT)
|
| 223 |
+
return {"output": stdout, "returncode": proc.returncode, "exception_info": ""}
|
| 224 |
+
except subprocess.TimeoutExpired as e:
|
| 225 |
+
os.killpg(proc.pid, signal.SIGKILL)
|
| 226 |
+
stdout, _ = proc.communicate()
|
| 227 |
+
return {"output": stdout or "", "returncode": -1,
|
| 228 |
+
"exception_info": f"An error occurred while executing the command: {e}"}
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def is_submit(output: dict) -> bool:
|
| 232 |
+
lines = output.get("output", "").lstrip().splitlines()
|
| 233 |
+
return bool(lines) and lines[0].strip() == "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT" and output["returncode"] == 0
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def split_images(text: str):
|
| 237 |
+
"""video_coder_harness.models.split_images: cut frame images out of the tool output."""
|
| 238 |
images = []
|
| 239 |
|
| 240 |
def repl(m):
|
| 241 |
if m.group(1).strip() != "image_url":
|
| 242 |
return m.group(0)
|
| 243 |
before = text[: m.start()].rstrip().splitlines()
|
| 244 |
+
caption = before[-1].strip() if before else ""
|
| 245 |
+
images.append((caption, m.group(2).strip()))
|
| 246 |
return f"[image {len(images)} attached below]"
|
| 247 |
|
| 248 |
+
return MM_TAG.sub(repl, text), images
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
|
| 251 |
+
def data_url_to_pil(url: str) -> Image.Image:
|
| 252 |
+
return Image.open(io.BytesIO(base64.b64decode(url.split(",", 1)[1]))).convert("RGB")
|
| 253 |
|
| 254 |
|
| 255 |
+
def parse_response(raw: str):
|
| 256 |
+
"""Split a Qwen3.5 response into (reasoning, content, tool calls) like vLLM's qwen3 reasoning + qwen3_coder parsers."""
|
| 257 |
+
text = raw
|
| 258 |
+
for t in END_TOKENS:
|
| 259 |
+
text = text.split(t)[0]
|
| 260 |
+
reasoning = ""
|
| 261 |
+
if "</think>" in text:
|
| 262 |
+
reasoning, text = text.split("</think>", 1)
|
| 263 |
+
reasoning = reasoning.replace("<think>", "").strip()
|
| 264 |
+
calls = []
|
| 265 |
+
for m in TOOL_CALL_RE.finditer(text):
|
| 266 |
+
args = {k: v for k, v in PARAM_RE.findall(m.group(2))}
|
| 267 |
+
calls.append({"name": m.group(1).strip(), "arguments": args})
|
| 268 |
+
content = TOOL_CALL_RE.sub("", text)
|
| 269 |
+
content = content.split("<tool_call>")[0].strip()
|
| 270 |
+
return reasoning, content, calls
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def sha(path: Path) -> str | None:
|
| 274 |
+
return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None
|
| 275 |
+
|
| 276 |
|
| 277 |
+
def short(text: str, n: int = 60) -> str:
|
| 278 |
+
lines = text.splitlines()
|
| 279 |
+
return text if len(lines) <= n else "\n".join(lines[:n]) + f"\n... ({len(lines) - n} more lines)"
|
| 280 |
|
| 281 |
+
|
| 282 |
+
def read_script(workdir: Path) -> str:
|
| 283 |
+
p = workdir / SCRIPT
|
| 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 = 16, 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.
|
| 331 |
+
max_steps: maximum number of agent turns (one bash command each).
|
| 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 |
# --------------------------------------------------------------------------------------------------------------------
|
| 543 |
# UI
|
| 544 |
# --------------------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
CSS = """
|
| 546 |
+
#col-container { max-width: 1280px; margin: 0 auto; }
|
| 547 |
.dark .gradio-container { color: var(--body-text-color); }
|
| 548 |
"""
|
| 549 |
|
| 550 |
+
EXAMPLES = [
|
| 551 |
+
"The video captures a small bird perched on a tree branch. The bird, with its brown and white feathers, yellow "
|
| 552 |
+
"beak, and black and white head, is the main focus of the video. The bird is seen in three different positions on "
|
| 553 |
+
"the branch, each time facing a different direction. The background of the video is filled with green foliage, "
|
| 554 |
+
"providing a natural and serene setting for the bird. The style of the video is a close-up shot.",
|
| 555 |
+
"A single polished walnut-wood chess pawn stands on a small square pedestal of white Carrara marble. No other "
|
| 556 |
+
"objects are in the scene. The camera makes one complete 360-degree orbit around the pawn while slowly rising.",
|
| 557 |
+
"The video shows a sequence of three identical white cars driving on a road. The first car is in the foreground, "
|
| 558 |
+
"the second car is slightly behind and to the right, and the third car is in the background. The cars are moving "
|
| 559 |
+
"from left to right across the frame. The road appears to be a city street with a dark grey surface. The "
|
| 560 |
+
"background features a city skyline with buildings of various heights, and the sky is a gradient of orange and "
|
| 561 |
+
"yellow, suggesting either sunrise or sunset. The style of the video is realistic with a focus on the cars and "
|
| 562 |
+
"their movement.",
|
| 563 |
+
"a man standing in front of a machine.",
|
| 564 |
+
"The video captures a lively scene on the ocean, with three boats in motion. The first boat, a large white yacht, "
|
| 565 |
+
"is seen in the distance, sailing towards the horizon. The second and third boats are closer to the viewer, both "
|
| 566 |
+
"moving at high speeds, leaving trails of white water behind them. The ocean is a deep blue, and the sky above is "
|
| 567 |
+
"clear, suggesting a sunny day. The boats are the main focus of the video, with their movement and the water's "
|
| 568 |
+
"reaction to their speed creating a dynamic and exciting scene. The overall style of the video is a dynamic "
|
| 569 |
+
"aerial shot, capturing the movement and beauty of the ocean and the boats.",
|
| 570 |
+
]
|
| 571 |
+
|
| 572 |
with gr.Blocks(title="Video-Coder") as demo:
|
| 573 |
with gr.Column(elem_id="col-container"):
|
| 574 |
+
gr.Markdown(
|
| 575 |
+
"# π¬ Video-Coder: agentic coding for video generation\n"
|
| 576 |
+
"**Video-Coder-9B** writes and runs a Blender Python program for your scene description, inspects the "
|
| 577 |
+
"rendered frames, fixes what is wrong and submits a **draft video** (white-model 3D render with the "
|
| 578 |
+
"layout, motion and camera of the scene) β the structure/motion signal the paper feeds to a downstream "
|
| 579 |
+
"video generator.\n\n"
|
| 580 |
+
"[π Paper](https://huggingface.co/papers/2611.09894) Β· [π Project page](https://cywang735.github.io/"
|
| 581 |
+
"Video-Coder/) Β· [π» Code](https://github.com/CYWang735/Video-Coder) Β· [π€ Model](https://huggingface.co/"
|
| 582 |
+
"Video-Coder/Video-Coder-9B)\n\n"
|
| 583 |
+
"β³ A full agent run takes several minutes: the first turn writes a long script, and every `run_blender` "
|
| 584 |
+
"renders the animation with Blender on CPU. The trajectory streams live; keep this tab open."
|
| 585 |
+
)
|
| 586 |
+
with gr.Row():
|
| 587 |
+
prompt = gr.Textbox(label="Scene description", lines=3, scale=4,
|
| 588 |
+
placeholder="Describe the scene and its motion, e.g. 'a red ball rolls down a ramp "
|
| 589 |
+
"and knocks over three dominoes'")
|
| 590 |
+
with gr.Column(scale=1, min_width=180):
|
| 591 |
+
run_btn = gr.Button("Generate draft video", variant="primary")
|
| 592 |
+
stop_btn = gr.Button("Stop", variant="stop")
|
| 593 |
+
status = gr.Markdown()
|
| 594 |
with gr.Row():
|
| 595 |
+
with gr.Column(scale=1):
|
| 596 |
+
video_out = gr.Video(label="Draft video (Blender render)", autoplay=True)
|
| 597 |
+
preview_out = gr.Image(label="Latest render: frames 1, β
, β
, last", type="filepath")
|
| 598 |
+
with gr.Column(scale=1):
|
| 599 |
+
with gr.Tabs():
|
| 600 |
+
with gr.Tab("Agent trajectory"):
|
| 601 |
+
chat_out = gr.Chatbot(label="Video-Coder harness", height=640, show_label=False)
|
| 602 |
+
with gr.Tab("main.py"):
|
| 603 |
+
code_out = gr.Code(label="Blender script written by the agent", language="python",
|
| 604 |
+
lines=30)
|
| 605 |
+
with gr.Accordion("Advanced settings", open=False):
|
| 606 |
+
max_steps = gr.Slider(2, 40, value=16, step=1, label="Max agent steps (one bash command each)")
|
| 607 |
+
temperature = gr.Slider(0.1, 1.5, value=1.0, step=0.05, label="Temperature")
|
| 608 |
+
top_p = gr.Slider(0.5, 1.0, value=0.95, step=0.01, label="Top-p")
|
| 609 |
+
max_new_tokens = gr.Slider(2048, 32768, value=16384, step=1024, label="Max tokens per agent turn")
|
| 610 |
+
|
| 611 |
+
state = gr.State(None)
|
| 612 |
+
tick = gr.Number(value=0, visible=False)
|
| 613 |
+
outputs = [state, chat_out, video_out, code_out, preview_out, status, tick]
|
| 614 |
+
inputs = [prompt, max_steps, temperature, top_p, max_new_tokens]
|
| 615 |
+
run_btn.click(start, inputs=inputs, outputs=outputs, api_name="start")
|
| 616 |
+
prompt.submit(start, inputs=inputs, outputs=outputs, api_visibility="private")
|
| 617 |
+
# Each agent step is its own request (fresh ZeroGPU token); the tick change chains the next one.
|
| 618 |
+
adv = tick.change(advance, inputs=[state], outputs=outputs, api_name="advance", concurrency_limit=None)
|
| 619 |
+
stop_btn.click(stop_run, inputs=[state], outputs=outputs, cancels=[adv], api_name="stop")
|
| 620 |
+
gr.Examples(examples=[[e] for e in EXAMPLES], inputs=[prompt], outputs=outputs, fn=start,
|
| 621 |
+
cache_examples=False, run_on_click=True)
|
|
|
|
| 622 |
|
| 623 |
if __name__ == "__main__":
|
| 624 |
+
demo.queue().launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)
|
packages.txt
CHANGED
|
@@ -3,6 +3,7 @@ libxrender1
|
|
| 3 |
libxxf86vm1
|
| 4 |
libxfixes3
|
| 5 |
libxi6
|
|
|
|
| 6 |
libxkbcommon0
|
| 7 |
libsm6
|
| 8 |
libgl1
|
|
|
|
| 3 |
libxxf86vm1
|
| 4 |
libxfixes3
|
| 5 |
libxi6
|
| 6 |
+
libxext6
|
| 7 |
libxkbcommon0
|
| 8 |
libsm6
|
| 9 |
libgl1
|
requirements.txt
CHANGED
|
@@ -1,9 +1,8 @@
|
|
| 1 |
transformers>=5.17.0
|
| 2 |
-
accelerate
|
| 3 |
torchvision
|
|
|
|
| 4 |
flash-linear-attention
|
| 5 |
-
pillow
|
| 6 |
-
pyyaml
|
| 7 |
-
jinja2
|
| 8 |
imageio-ffmpeg
|
| 9 |
uv
|
|
|
|
|
|
|
|
|
| 1 |
transformers>=5.17.0
|
|
|
|
| 2 |
torchvision
|
| 3 |
+
accelerate
|
| 4 |
flash-linear-attention
|
|
|
|
|
|
|
|
|
|
| 5 |
imageio-ffmpeg
|
| 6 |
uv
|
| 7 |
+
pillow
|
| 8 |
+
pyyaml
|