ovi054's picture
Create app.py
705ae0d verified
Raw
History Blame Contribute Delete
6.96 kB
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)