import gradio as gr import subprocess import os import tempfile import shutil import time import sys import re def extract_manim_code(text): match = re.search(r"```python\s*(.*?)```", text, re.DOTALL) if match: return match.group(1).strip() lines = text.splitlines() for i, line in enumerate(lines): if line.strip().startswith(("from ", "import ", "class ")): return "\n".join(lines[i:]).strip() return text.strip() class ManimAnimationGenerator: def __init__(self): self.temp_dir = None def setup_directories(self): self.temp_dir = tempfile.mkdtemp() os.makedirs(os.path.join(self.temp_dir, "media", "videos", "480p15"), exist_ok=True) return self.temp_dir def cleanup_directories(self): if self.temp_dir and os.path.exists(self.temp_dir): shutil.rmtree(self.temp_dir) self.temp_dir = None def validate_manim_code(self, code): if not any(imp in code for imp in ["from manim import *", "import manim"]): return False, "Code must include 'from manim import *' or 'import manim'" if "class" not in code: return False, "Code must contain at least one class definition" if "Scene" not in code: return False, "Class must inherit from Scene or a Scene subclass" return True, "OK" def extract_class_name(self, code): for line in code.split("\n"): if line.strip().startswith("class ") and "Scene" in line: return line.strip().split("class ")[1].split("(")[0].strip() return None def find_output_file(self, temp_dir, class_name, format_type): for root, _, files in os.walk(temp_dir): for file in files: if file.startswith(class_name) and file.endswith(f".{format_type}"): return os.path.join(root, file) return None def execute_manim_code(self, code, quality="low", format_type="mp4"): code = extract_manim_code(code) try: is_valid, message = self.validate_manim_code(code) if not is_valid: return None, f"❌ Validation Error: {message}", "" temp_dir = self.setup_directories() python_file = os.path.join(temp_dir, "animation.py") with open(python_file, "w") as f: f.write(code) class_name = self.extract_class_name(code) if not class_name: self.cleanup_directories() return None, "❌ Could not find a valid Scene class in the code", "" quality_map = {"low": "-ql", "medium": "-qm", "high": "-qh"} quality_flag = quality_map.get(quality, "-ql") cmd = [sys.executable, "-m", "manim", quality_flag, python_file, class_name] if format_type == "gif": cmd.append("--format=gif") result = subprocess.run( cmd, cwd=temp_dir, capture_output=True, text=True, timeout=120, ) if result.returncode != 0: self.cleanup_directories() return None, f"❌ Manim execution failed:\n{result.stderr}", result.stdout output_file = self.find_output_file(temp_dir, class_name, format_type) if not output_file: self.cleanup_directories() return None, "❌ Could not find generated animation file", result.stdout permanent_file = f"/tmp/{class_name}_{int(time.time())}.{format_type}" shutil.copy2(output_file, permanent_file) self.cleanup_directories() return permanent_file, "✅ Animation generated successfully!", result.stdout except subprocess.TimeoutExpired: self.cleanup_directories() return None, "❌ Timed out (2 minutes)", "" except Exception as e: self.cleanup_directories() return None, f"❌ Unexpected error: {str(e)}", "" generator = ManimAnimationGenerator() EXAMPLE_CODE = '''from manim import * class MyScene(Scene): def construct(self): square = Square(side_length=2).set_fill(BLUE, opacity=0.5) self.play(Create(square)) self.play(square.animate.rotate(PI / 2)) self.wait()''' CSS = """ body { font-family: 'Inter', sans-serif; } .render-btn { background: linear-gradient(135deg, #7C3AED, #2563EB) !important; border: none !important; } .code-box textarea { font-family: 'JetBrains Mono', 'Fira Code', monospace !important; font-size: 13px !important; } """ def render_animation(code: str, quality: str = "low", format_type: str = "mp4"): """Render Manim code to video. Exposed as /render API endpoint.""" if not code or not code.strip(): return None, "❌ Please enter some Manim code.", "" result_path, status_msg, logs = generator.execute_manim_code(code, quality, format_type) return result_path, status_msg, logs with gr.Blocks(title="Manim Renderer", css=CSS, theme=gr.themes.Soft()) as app: gr.Markdown("# 🎬 Manim Code Renderer") gr.Markdown("Paste Manim Python code below and render it to video.") with gr.Row(): with gr.Column(scale=2): code_input = gr.Code( label="Manim Code", language="python", lines=22, value=EXAMPLE_CODE, elem_classes=["code-box"], ) with gr.Row(): quality = gr.Dropdown( choices=["low", "medium", "high"], value="low", label="Quality", ) format_type = gr.Dropdown( choices=["mp4", "gif"], value="mp4", label="Format", ) render_btn = gr.Button( "🎥 Render Animation", variant="primary", size="lg", elem_classes=["render-btn"], ) with gr.Column(scale=2): output_video = gr.Video(label="Generated Animation") status_output = gr.Textbox(label="Status", lines=2) logs_output = gr.Textbox(label="Manim Logs", lines=8, visible=False) with gr.Row(): show_logs_btn = gr.Button("Show Logs", size="sm") hide_logs_btn = gr.Button("Hide Logs", size="sm") render_btn.click( fn=render_animation, inputs=[code_input, quality, format_type], outputs=[output_video, status_output, logs_output], api_name="render", ) show_logs_btn.click(fn=lambda: gr.update(visible=True), outputs=[logs_output]) hide_logs_btn.click(fn=lambda: gr.update(visible=False), outputs=[logs_output]) if __name__ == "__main__": app.launch(mcp_server=True, debug=True, share=True)