| 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) |