Chand11's picture
Update app.py
d7b52f6 verified
import gradio as gr
import subprocess
import os
import re
import shutil
from pathlib import Path
import tempfile
# Try to import google.generativeai
try:
import google.generativeai as genai
GENAI_AVAILABLE = True
except ImportError:
GENAI_AVAILABLE = False
print("")
# Get API key from environment
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
def fix_all_known_errors(code):
"""Fix ALL known Manim errors"""
# 1. Fix colors to base 10 only
color_map = {
'ORANGE_A': 'ORANGE', 'ORANGE_B': 'ORANGE', 'ORANGE_C': 'ORANGE', 'ORANGE_D': 'ORANGE', 'ORANGE_E': 'ORANGE',
'GRAY_A': 'WHITE', 'GRAY_B': 'WHITE', 'GRAY_C': 'WHITE', 'GRAY_D': 'WHITE', 'GRAY_E': 'WHITE',
'GREY': 'WHITE', 'GRAY': 'WHITE', 'GREY_A': 'WHITE', 'GREY_B': 'WHITE',
'PURPLE_A': 'PURPLE', 'PURPLE_B': 'PURPLE', 'PURPLE_C': 'PURPLE', 'PURPLE_D': 'PURPLE', 'PURPLE_E': 'PURPLE',
'RED_A': 'RED', 'RED_B': 'RED', 'RED_C': 'RED', 'RED_D': 'RED', 'RED_E': 'RED',
'BLUE_A': 'BLUE', 'BLUE_B': 'BLUE', 'BLUE_C': 'BLUE', 'BLUE_D': 'BLUE', 'BLUE_E': 'BLUE',
'GREEN_A': 'GREEN', 'GREEN_B': 'GREEN', 'GREEN_C': 'GREEN', 'GREEN_D': 'GREEN', 'GREEN_E': 'GREEN',
'YELLOW_A': 'YELLOW', 'YELLOW_B': 'YELLOW', 'YELLOW_C': 'YELLOW', 'YELLOW_D': 'YELLOW', 'YELLOW_E': 'YELLOW',
'BROWN': 'ORANGE', 'GOLD': 'YELLOW', 'SILVER': 'WHITE', 'BRONZE': 'ORANGE',
'MAROON': 'RED', 'NAVY': 'BLUE', 'CYAN': 'TEAL', 'MAGENTA': 'PINK', 'LIME': 'GREEN',
}
for old, new in color_map.items():
code = re.sub(rf'\b{old}\b', new, code)
# 2. Fix invalid objects
code = re.sub(r'Checkmark\([^)]*\)', 'Circle(radius=0.3, color=GREEN, fill_opacity=1)', code)
code = re.sub(r'CheckMark\([^)]*\)', 'Circle(radius=0.3, color=GREEN, fill_opacity=1)', code)
code = re.sub(r'XMark\([^)]*\)', 'Cross(color=RED).scale(0.5)', code)
code = re.sub(r'SVGMobject\([^)]+\)', 'Circle(radius=0.8, color=PINK, fill_opacity=0.8)', code)
code = re.sub(r'ImageMobject\([^)]+\)', 'Square(side_length=2, color=BLUE, fill_opacity=0.5)', code)
# 3. Fix DecimalNumber methods
code = re.sub(r'\.add_prefix\([^)]+\)', '', code)
code = re.sub(r'\.add_suffix\([^)]+\)', '', code)
# 4. Fix axis labels - replace with Text
code = re.sub(r'(\w+)\s*=\s*axes\.get_x_axis_label\([^)]+\)',
r'\1 = Text("X", font_size=24).next_to(axes.x_axis, DOWN, buff=0.3)', code)
code = re.sub(r'(\w+)\s*=\s*axes\.get_y_axis_label\([^)]+\)',
r'\1 = Text("Y", font_size=24).next_to(axes.y_axis, LEFT, buff=0.3).rotate(90*DEGREES)', code)
code = re.sub(r'\.get_x_axis_label\([^)]+\)', '', code)
code = re.sub(r'\.get_y_axis_label\([^)]+\)', '', code)
# 5. Limit font sizes
code = re.sub(r'font_size=(\d+)', lambda m: f'font_size={min(int(m.group(1)), 36)}', code)
# 6. Fix shifts to safe values
code = re.sub(r'\.shift\(UP\s*\*\s*\d+\.?\d*\)', '.shift(UP*1.5)', code)
code = re.sub(r'\.shift\(DOWN\s*\*\s*\d+\.?\d*\)', '.shift(DOWN*1.5)', code)
code = re.sub(r'\.shift\(LEFT\s*\*\s*\d+\.?\d*\)', '.shift(LEFT*3)', code)
code = re.sub(r'\.shift\(RIGHT\s*\*\s*\d+\.?\d*\)', '.shift(RIGHT*3)', code)
# 7. Force buff on all to_edge
code = re.sub(r'\.to_edge\((UP|DOWN|LEFT|RIGHT)\)(?!\s*,)', r'.to_edge(\1, buff=1)', code)
# 8. Replace Tex with MathTex
code = re.sub(r'\bTex\(', 'MathTex(', code)
return code
def generate_code_with_gemini(prompt):
"""Generate Manim code using Gemini API"""
if not GENAI_AVAILABLE:
return None, "❌ google-generativeai package not installed"
if not GEMINI_API_KEY:
return None, "❌ GEMINI_API_KEY not set in environment variables"
try:
genai.configure(api_key=GEMINI_API_KEY)
models = [m.name for m in genai.list_models() if 'generateContent' in m.supported_generation_methods]
model_name = next((m for m in models if 'flash' in m.lower()), models[0])
model = genai.GenerativeModel(model_name)
full_prompt = f"""Generate Manim code. CRITICAL RULES:
1. COLORS - ONLY these 10: BLUE, RED, GREEN, YELLOW, ORANGE, PURPLE, PINK, TEAL, WHITE, BLACK
NO variants (_A, _B, _C, _D, _E)
2. TEXT OVERLAP PREVENTION (MOST IMPORTANT):
- ALWAYS FadeOut text before showing new text
- NEVER reuse the same position without FadeOut first
- Pattern for EVERY text:
```python
title = Text("Title", font_size=36).to_edge(UP, buff=1)
self.play(Write(title))
self.wait(1.5)
self.play(FadeOut(title)) # ← MANDATORY!
# Now safe to reuse UP position
subtitle = Text("Next", font_size=32).to_edge(UP, buff=1)
self.play(Write(subtitle))
self.wait(1.5)
self.play(FadeOut(subtitle)) # ← MANDATORY!
```
3. SAFE BOUNDARIES (CRITICAL):
- Font size: MAX 36 (never larger!)
- Shifts: UP*1.5, DOWN*1.5, LEFT*3, RIGHT*3 (MAX!)
- Always use buff=1 with to_edge()
- Safe zone: X[-4, 4], Y[-2, 2]
4. NEVER use:
- get_x_axis_label(), get_y_axis_label()
- Checkmark, SVGMobject, ImageMobject
- .add_prefix(), .add_suffix()
5. For axis labels:
x_label = Text("Time", font_size=24).next_to(axes.x_axis, DOWN, buff=0.5)
y_label = Text("Value", font_size=24).next_to(axes.y_axis, LEFT, buff=0.5)
6. STRUCTURE - Follow this pattern:
```python
from manim import *
class MyScene(Scene):
def construct(self):
self.camera.background_color = WHITE
# Section 1
text1 = Text("First", font_size=36).to_edge(UP, buff=1)
self.play(Write(text1))
self.wait(1.5)
self.play(FadeOut(text1)) # ← Clean up!
# Section 2
text2 = Text("Second", font_size=32).to_edge(UP, buff=1)
self.play(Write(text2))
self.wait(1.5)
self.play(FadeOut(text2)) # ← Clean up!
```
User wants: {prompt}
Generate complete code following ALL rules above. EVERY text MUST have FadeOut before next text!"""
response = model.generate_content(full_prompt)
code = response.text
return code, None
except Exception as e:
return None, f"❌ Gemini API error: {str(e)}"
def render_video(prompt, quality="low"):
"""Main function to generate and render Manim video"""
if not prompt or not prompt.strip():
yield "❌ Please enter a prompt!", None, None
return
try:
# Create temp directory
temp_dir = Path(tempfile.mkdtemp(prefix="manim_"))
yield "🤖 Generating code with Gemini AI...", None, None
# Generate code
code, error = generate_code_with_gemini(prompt)
if error:
yield error, None, None
return
if not code:
yield "❌ Failed to generate code", None, None
return
# Extract Python code if wrapped in markdown
if "```python" in code:
code = code.split("```python")[1].split("```")[0]
elif "```" in code:
code = code.split("```")[1].split("```")[0]
code = code.strip()
# Apply fixes
code = fix_all_known_errors(code)
# Find Scene class
match = re.search(r'class\s+(\w+)\s*\(Scene\)', code)
if not match:
yield "❌ No Scene class found in generated code!", code, None
return
class_name = match.group(1)
# Save code to file
code_file = temp_dir / "animation.py"
with open(code_file, 'w', encoding='utf-8') as f:
f.write(code)
yield f"✓ Code generated (Scene: {class_name})\n🎬 Rendering video (this may take 1-2 minutes)...", code, None
# Render video with absolute paths
quality_map = {'low': '-ql', 'medium': '-qm', 'high': '-qh'}
quality_flag = quality_map.get(quality, '-ql')
abs_code_file = str(code_file.absolute())
media_dir = str((temp_dir / "media").absolute())
command = f"manim {abs_code_file} {class_name} {quality_flag} --disable_caching --media_dir {media_dir}"
process = subprocess.Popen(
command, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True
)
output_lines = []
for line in process.stdout:
output_lines.append(line)
process.wait()
if process.returncode == 0:
# Find the generated video
media_path = temp_dir / "media"
video_files = list(media_path.rglob("*.mp4"))
if video_files:
video_path = str(video_files[0])
yield f"✅ Video rendered successfully! 🎉", code, video_path
else:
yield "❌ Video file not found after rendering", code, None
else:
error_msg = ''.join(output_lines[-30:]) # Last 30 lines
yield f"❌ Rendering failed:\n\n{error_msg}", code, None
# Cleanup
try:
shutil.rmtree(temp_dir)
except:
pass
except Exception as e:
import traceback
error_details = traceback.format_exc()
yield f"❌ Error: {str(e)}\n\nDetails:\n{error_details}", None, None
# Gradio Interface
def create_interface():
with gr.Blocks(title="Manim Video Generator") as demo:
gr.Markdown("""
# 🎬 Manim Video Generator
Create mathematical animations using AI! Describe what you want to see animated.
**Examples:**
- "Explain Pythagorean theorem with animation"
- "Show a sine wave transforming into a cosine wave"
- "Animate a circle morphing into a square"
- "Show the concept of derivatives with a tangent line"
""")
with gr.Row():
with gr.Column():
prompt_input = gr.Textbox(
label="What animation do you want?",
placeholder="e.g., Show a circle morphing into a square",
lines=4
)
quality_input = gr.Radio(
choices=["low", "medium", "high"],
value="low",
label="Quality (low is faster, recommended)"
)
generate_btn = gr.Button("🎬 Generate Video", variant="primary", size="lg")
with gr.Column():
status_output = gr.Textbox(label="Status", lines=4)
video_output = gr.Video(label="Generated Animation")
code_output = gr.Code(label="Generated Manim Code", language="python", lines=15)
gr.Markdown("""
### 💡 Tips:
- Be specific in your description
- Start with simple prompts to test
- Low quality renders faster (30 seconds to 1 minute)
- Medium/High quality may take 2-3 minutes
- Complex animations may timeout on free tier
""")
generate_btn.click(
fn=render_video,
inputs=[prompt_input, quality_input],
outputs=[status_output, code_output, video_output]
)
# Examples
gr.Examples(
examples=[
["Show a blue circle morphing into a red square", "low"],
["Animate the Pythagorean theorem: a² + b² = c²", "low"],
["Show a sine wave moving across the screen", "low"],
["Create an animation showing 3 dots forming a triangle", "low"],
["Show a number counting from 0 to 10", "low"],
],
inputs=[prompt_input, quality_input]
)
return demo
if __name__ == "__main__":
demo = create_interface()
demo.launch()