math-to-visual-agent / executor.py
vankhieu's picture
Tune generation speed and timeouts
9cb7d65
Raw
History Blame Contribute Delete
4.78 kB
"""Render generated Manim scenes in an isolated subprocess."""
from __future__ import annotations
import shutil
import subprocess
import tempfile
import time
import uuid
from dataclasses import dataclass
from pathlib import Path
import re
import sys
from typing import Tuple
OUTPUT_DIR = Path("./output")
SCENE_CLASS_PATTERN = re.compile(r"class\s+(\w+)\s*\(\s*\w*Scene\w*\s*\):")
@dataclass
class RenderResult:
success: bool
video_path: str | None
info: str
errors: str
def find_scene_name(code: str) -> str:
"""Extract the first Manim Scene class name from generated Python code."""
match = SCENE_CLASS_PATTERN.search(code)
return match.group(1) if match else "MainScene"
def _manim_command_prefix() -> list[str]:
"""Return a command prefix for Manim.
Hugging Face Spaces occasionally installs the Python package correctly
without exposing the `manim` console script on PATH. `python -m manim`
still works in that case, so keep it as the final fallback.
"""
for command in ("manim", "manimce"):
if shutil.which(command):
return [command]
return [sys.executable, "-m", "manim"]
def render_code(
code: str,
timeout_seconds: int = 600,
output_dir: Path = OUTPUT_DIR,
quality: str = "-ql",
) -> RenderResult:
"""Render Manim code and return a structured result.
This mirrors the robust approach used by manim-trainer: use a temporary
script and a temporary media directory to avoid collisions, then write only
the final MP4 into the app's output folder.
"""
if not code or not code.strip():
return RenderResult(False, None, "", "No Manim code was provided.")
output_dir.mkdir(parents=True, exist_ok=True)
scene_name = find_scene_name(code)
video_name = f"{scene_name}_{uuid.uuid4().hex[:12]}_{int(time.time())}"
final_video_base = output_dir / video_name
temp_script_name = ""
try:
with tempfile.NamedTemporaryFile(
mode="w",
suffix=".py",
prefix="temp_scene_",
encoding="utf-8",
delete=False,
) as temp_script:
temp_script.write(code)
temp_script.flush()
temp_script_name = temp_script.name
with tempfile.TemporaryDirectory(prefix="scivisual_manim_") as temp_media_dir:
command = _manim_command_prefix() + [
temp_script_name,
scene_name,
quality,
"--format=mp4",
"--media_dir",
temp_media_dir,
"-o",
str(final_video_base.absolute()),
]
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
)
stdout = result.stdout.strip()
stderr = result.stderr.strip()
if result.returncode != 0:
error_log = stderr or stdout or f"Manim failed with return code {result.returncode}."
return RenderResult(False, None, stdout, error_log)
final_video_path = final_video_base.with_suffix(".mp4")
if final_video_path.exists():
return RenderResult(True, str(final_video_path), stdout, stderr)
return RenderResult(
False,
None,
stdout,
f"Manim reported success, but no MP4 was found at {final_video_path}. STDERR: {stderr}",
)
except FileNotFoundError as exc:
return RenderResult(
False,
None,
"",
"Manim could not be launched. Ensure the `manim` Python package is installed. "
f"Attempted fallback command: `{sys.executable} -m manim`. Details: {exc}",
)
except subprocess.TimeoutExpired as exc:
stdout = exc.stdout or ""
stderr = exc.stderr or ""
return RenderResult(
False,
None,
stdout,
f"Rendering timed out after {timeout_seconds} seconds.\n\nSTDERR:\n{stderr}",
)
except Exception as exc: # pragma: no cover - runtime safety guard
return RenderResult(False, None, "", f"Unexpected render failure: {type(exc).__name__}: {exc}")
finally:
if temp_script_name:
Path(temp_script_name).unlink(missing_ok=True)
def render_manim_scene(code: str, timeout_seconds: int = 600) -> Tuple[bool, str]:
"""Compatibility wrapper returning the tuple shape used by the app."""
result = render_code(code, timeout_seconds=timeout_seconds)
if result.success and result.video_path:
return True, result.video_path
return False, result.errors or result.info