File size: 12,071 Bytes
35f8b0a
 
 
 
 
 
356823a
35f8b0a
356823a
 
 
 
 
 
4689e9f
35f8b0a
356823a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35f8b0a
356823a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35f8b0a
 
356823a
35f8b0a
356823a
 
35f8b0a
 
 
 
356823a
 
 
 
 
 
 
35f8b0a
356823a
 
 
35f8b0a
356823a
35f8b0a
 
 
 
356823a
 
 
 
35f8b0a
 
 
 
 
 
 
356823a
 
35f8b0a
 
356823a
 
 
 
 
 
 
35f8b0a
356823a
 
 
 
 
 
 
35f8b0a
 
356823a
35f8b0a
 
 
 
356823a
 
 
 
35f8b0a
356823a
a12e311
35f8b0a
356823a
35f8b0a
 
356823a
 
 
 
 
 
 
 
 
35f8b0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356823a
35f8b0a
 
 
 
356823a
35f8b0a
a12e311
35f8b0a
 
 
a12e311
 
 
 
35f8b0a
 
 
 
a12e311
35f8b0a
 
 
 
 
 
 
 
 
 
356823a
 
35f8b0a
 
 
356823a
35f8b0a
 
 
356823a
 
 
 
 
 
 
 
35f8b0a
 
356823a
 
 
35f8b0a
 
 
7ea35cf
35f8b0a
 
 
4689e9f
35f8b0a
 
 
356823a
 
35f8b0a
 
 
 
 
 
 
356823a
35f8b0a
 
 
 
356823a
35f8b0a
356823a
35f8b0a
 
356823a
35f8b0a
 
356823a
35f8b0a
 
 
356823a
 
 
 
35f8b0a
356823a
35f8b0a
 
 
 
 
 
 
 
 
 
 
356823a
 
 
 
 
35f8b0a
 
 
 
 
 
 
 
d7b52f6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
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()