Bugsbunny2000/Math-Animation-Generator
0
1"""2Math to Manim - Professional Edition3=====================================4A cinematic mathematical animation generator powered by local LLM and Manim.5Transforms natural language descriptions into breathtaking mathematical visualizations.6 7Author: Professional Edition8Version: 2.0.09Date: December 202510"""11 12import streamlit as st13import ollama14import subprocess15import uuid16from pathlib import Path17import shutil18import time19import re20from typing import Optional, Tuple21from datetime import datetime22 23# ============================================================================24# CONFIGURATION & CONSTANTS25# ============================================================================26 27TEMP_DIR = Path("temp_animations")28CLEANUP_AGE = 3600 # seconds29DEFAULT_MODEL = "qwen2.5-coder:7b"30 31# Manim quality presets32QUALITY_PRESETS = {33 "Ultra (4K - slow)": {"flag": "-qk", "desc": "3840x2160, 60fps"},34 "High (1080p)": {"flag": "-qh", "desc": "1920x1080, 60fps"},35 "Medium (720p)": {"flag": "-qm", "desc": "1280x720, 30fps"},36 "Low (480p - fast)": {"flag": "-ql", "desc": "854x480, 15fps"}37}38 39# Professional example prompts40EXAMPLE_PROMPTS = [41 "Fighter jet breaking sound barrier with shockwaves and vapor cone",42 "Black hole with spinning accretion disk and gravitational lensing",43 "Double pendulum chaos with rainbow trail",44 "Mandelbrot set deep zoom into infinite spirals",45 "Quantum wave function in a box",46 "Fourier series forming a beating heart",47 "Navier-Stokes fluid flow around cylinder",48 "Riemann zeta zeros on critical line"49]50 51# ============================================================================52# UTILITY FUNCTIONS53# ============================================================================54 55def initialize_session_state():56 """Initialize all session state variables."""57 if 'generated_code' not in st.session_state:58 st.session_state.generated_code = None59 if 'enhanced_prompt' not in st.session_state:60 st.session_state.enhanced_prompt = None61 if 'video_path' not in st.session_state:62 st.session_state.video_path = None63 if 'generation_history' not in st.session_state:64 st.session_state.generation_history = []65 66def cleanup_old_files():67 """Remove temporary animation files older than CLEANUP_AGE."""68 if not TEMP_DIR.exists():69 return70 71 current_time = time.time()72 for item in TEMP_DIR.iterdir():73 if item.is_dir():74 try:75 if current_time - item.stat().st_mtime > CLEANUP_AGE:76 shutil.rmtree(item, ignore_errors=True)77 except Exception:78 pass79 80def sanitize_code(raw_code: str) -> str:81 """82 Sanitize and fix common issues in generated Manim code.83 84 Args:85 raw_code: Raw code output from LLM86 87 Returns:88 Cleaned and sanitized Python code89 """90 # Extract code from markdown blocks91 if "```python" in raw_code:92 code = raw_code.split("```python", 1)[1].split("```", 1)[0]93 elif "```" in raw_code:94 code = raw_code.split("```", 1)[1].split("```", 1)[0]95 else:96 code = raw_code97 98 code = code.strip()99 100 # Ensure proper imports101 if "from manim import *" not in code:102 code = "from manim import *\nimport numpy as np\n\n" + code103 elif "import numpy as np" not in code:104 code = code.replace("from manim import *", "from manim import *\nimport numpy as np")105 106 # Check if code uses camera features (MovingCameraScene features)107 uses_camera_frame = "self.camera.frame" in code or "camera.frame" in code108 uses_camera_animate = "self.camera.animate" in code or "camera.animate" in code109 needs_moving_camera = uses_camera_frame or uses_camera_animate110 111 # Keep only the first MathScene class and fix Scene type if needed112 if needs_moving_camera:113 # Change to MovingCameraScene if camera features are used114 scene_matches = list(re.finditer(r"class MathScene\((?:Scene|MovingCameraScene)\):[\s\S]*?(?=class |\Z)", code))115 if scene_matches:116 imports = re.findall(r"^(?:from|import)\s+.*$", code, re.MULTILINE)117 import_block = "\n".join(imports) if imports else "from manim import *\nimport numpy as np"118 scene_code = scene_matches[0].group(0)119 # Force MovingCameraScene120 scene_code = re.sub(r"class MathScene\(Scene\):", "class MathScene(MovingCameraScene):", scene_code)121 122 # Fix self.camera.animate to self.camera.frame.animate123 scene_code = scene_code.replace("self.camera.animate", "self.camera.frame.animate")124 125 code = import_block + "\n\n" + scene_code126 else:127 # Regular Scene128 scene_matches = list(re.finditer(r"class MathScene\((?:Scene|MovingCameraScene)\):[\s\S]*?(?=class |\Z)", code))129 if scene_matches:130 imports = re.findall(r"^(?:from|import)\s+.*$", code, re.MULTILINE)131 import_block = "\n".join(imports) if imports else "from manim import *\nimport numpy as np"132 scene_code = scene_matches[0].group(0)133 # Ensure it's regular Scene134 scene_code = re.sub(r"class MathScene\(MovingCameraScene\):", "class MathScene(Scene):", scene_code)135 code = import_block + "\n\n" + scene_code136 137 # Fix common Manim errors138 code = re.sub(r"\[([-\d.]+),\s*([-\d.]+),\s*0\]", r"[\1, \2, 0]", code)139 140 # Remove or replace unsupported 3D objects141 code = code.replace("Cone(", "Circle(")142 code = code.replace("Cylinder(", "Rectangle(")143 code = code.replace("Sphere(", "Circle(")144 code = code.replace("SVGMobject(", "Text(")145 146 # Remove ImageMobject references (files don't exist)147 # Replace with colored rectangles as placeholders148 lines = code.split('\n')149 filtered_lines = []150 for line in lines:151 if 'ImageMobject(' in line:152 # Extract variable name and try to create a replacement153 match = re.search(r'(\w+)\s*=\s*ImageMobject\([^)]+\)', line)154 if match:155 var_name = match.group(1)156 indent = len(line) - len(line.lstrip())157 # Replace with a colored rectangle158 replacement = ' ' * indent + f'{var_name} = Rectangle(width=4, height=3, color=BLUE, fill_opacity=0.3)'159 if '.scale(' in line:160 scale_match = re.search(r'\.scale\(([\d.]+)\)', line)161 if scale_match:162 replacement += f'.scale({scale_match.group(1)})'163 if '.shift(' in line:164 shift_match = re.search(r'\.shift\([^)]+\)', line)165 if shift_match:166 replacement += shift_match.group(0)167 filtered_lines.append(replacement)168 else:169 # Just comment it out170 filtered_lines.append(' # ' + line.strip() + ' # ImageMobject removed - file not found')171 else:172 filtered_lines.append(line)173 code = '\n'.join(filtered_lines)174 175 # Fix incorrect color_gradient syntax176 # Wrong: .set_color(color_gradient=[BLUE, WHITE])177 # Right: .set_color_by_gradient(BLUE, WHITE) or just .set_color(BLUE)178 code = re.sub(r'\.set_color\(color_gradient=\[([^\]]+)\]\)', r'.set_color_by_gradient(\1)', code)179 180 # Fix incorrect gradient parameter in constructors181 # Wrong: Circle(color_gradient=[RED, BLUE])182 # Right: Circle(color=RED) then .set_color_by_gradient(RED, BLUE)183 code = re.sub(r'color_gradient=\[([^\]]+)\]', r'color=BLUE', code)184 185 # Fix np.random calls without proper array creation186 # Wrong: Dot(np.random.uniform(-8, 8), ...)187 # Right: Dot(np.array([np.random.uniform(-8, 8), ...]))188 code = re.sub(189 r'Dot\(np\.random\.uniform\(([^)]+)\),\s*np\.random\.uniform\(([^)]+)\)',190 r'Dot(np.array([np.random.uniform(\1), np.random.uniform(\2), 0]))',191 code192 )193 194 # Fix MoveToTarget usage (incorrect syntax)195 # Wrong: MoveToTarget(obj, target=...)196 # Right: obj.animate.move_to(...)197 code = re.sub(r'MoveToTarget\(([^,]+),\s*target=([^)]+)\)', r'\1.animate.move_to(\2.get_center())', code)198 199 # Fix standalone .animate.rotate() without self.play()200 # Wrong: earth_plane.animate.rotate(PI / 4).run_time = 1201 # Right: self.play(earth_plane.animate.rotate(PI / 4), run_time=1)202 lines = code.split('\n')203 filtered_lines = []204 for line in lines:205 if '.animate.' in line and 'self.play(' not in line and '.run_time' in line:206 # This is a broken animation line207 match = re.search(r'(\w+)\.animate\.(\w+)\(([^)]+)\)\.run_time\s*=\s*(\d+)', line)208 if match:209 obj, method, args, run_time = match.groups()210 indent = len(line) - len(line.lstrip())211 fixed_line = ' ' * indent + f'self.play({obj}.animate.{method}({args}), run_time={run_time})'212 filtered_lines.append(fixed_line)213 else:214 filtered_lines.append(line)215 else:216 filtered_lines.append(line)217 code = '\n'.join(filtered_lines)218 219 # Fix Matrix objects with numpy arrays - convert to simple lists220 # Wrong: Matrix([[np.cos(...), np.sin(...)], [...]])221 # Right: MathTex(r"\begin{bmatrix} ... \end{bmatrix}")222 # Simply replace Matrix() with MathTex for LaTeX rendering223 def fix_matrix(match):224 content = match.group(1)225 # Try to extract simple pattern226 return 'MathTex(r"\\begin{bmatrix} \\cos(\\theta) & -\\sin(\\theta) \\\\ \\sin(\\theta) & \\cos(\\theta) \\end{bmatrix}")'227 228 code = re.sub(r'Matrix\(\[\s*\[(.*?)\]\s*\]\)', fix_matrix, code, flags=re.DOTALL)229 230 # Fix Polygon with incorrect numpy array dimensions231 # Wrong: Polygon(np.array([[-2, -1.5, 0]], dtype=float), ...)232 # Right: Polygon(np.array([-2, -1.5, 0]), ...)233 # Remove extra brackets from np.array calls234 code = re.sub(235 r'np\.array\(\[\[([-\d.,\s]+)\]\],\s*dtype=float\)',236 r'np.array([\1])',237 code238 )239 # Also fix without dtype240 code = re.sub(241 r'np\.array\(\[\[([-\d.,\s]+)\]\]\)',242 r'np.array([\1])',243 code244 )245 246 # Fix Polygon calls with multiple separate np.array arguments247 # Convert to single points list248 def fix_polygon_call(match):249 full_match = match.group(0)250 # Extract all coordinate sets251 coords = re.findall(r'\[([-\d.,\s]+)\]', full_match)252 if len(coords) >= 3:253 # Build proper Polygon call254 points_str = ', '.join([f'np.array([{c}])' for c in coords[:10]]) # Limit to 10 points255 return f'Polygon({points_str}'256 return full_match257 258 # Look for Polygon calls with multiple separate arrays259 code = re.sub(260 r'Polygon\(np\.array\(\[[^\]]+\]\)[,\s]+np\.array\(\[[^\]]+\]\)[,\s]+np\.array\([^)]+\)',261 fix_polygon_call,262 code263 )264 265 # Fix invalid color names - replace with valid Manim colors266 invalid_colors = {267 'RED_BROWN': 'MAROON',268 'NEON_BLUE': 'BLUE_C',269 'SOFT_RED': 'RED_D',270 'SILVER': 'GRAY',271 'DARK_BLUE': 'BLUE_E',272 'SKYBLUE': 'BLUE_B',273 'DARK_RED': 'DARK_BROWN',274 'LIGHT_BLUE': 'BLUE_A',275 'DARK_GREEN': 'GREEN_E',276 'LIGHT_GREEN': 'GREEN_A',277 'NEON_GREEN': 'GREEN_C',278 'BRIGHT_RED': 'RED_A',279 'BRIGHT_BLUE': 'BLUE_A',280 'BRIGHT_GREEN': 'GREEN_A',281 'GOLD': 'YELLOW_D',282 }283 284 for invalid, valid in invalid_colors.items():285 code = code.replace(invalid, valid)286 287 # Fix Circle with invalid parameters (height, depth)288 # Circle only takes radius, not height/depth/width289 code = re.sub(r'Circle\([^)]*height=[^,)]+[^)]*\)', 'Circle(radius=1)', code)290 code = re.sub(r'Circle\([^)]*depth=[^,)]+[^)]*\)', 'Circle(radius=1)', code)291 code = re.sub(r'Circle\([^)]*width=[^,)]+[^)]*\)', 'Circle(radius=1)', code)292 293 # Fix Sector with invalid parameters294 # Sector needs proper angle range295 code = re.sub(296 r'Sector\(start_angle=([^,]+),\s*angle=([^,]+),\s*radius=([^,)]+)',297 r'Arc(start_angle=\1, angle=\2, radius=\3',298 code299 )300 301 # Fix list comprehension syntax errors - remove trailing commas302 # Wrong: [item, for i in range(10)]303 # Right: [item for i in range(10)]304 code = re.sub(305 r'\[([^,\]]+),\s+(for\s+\w+\s+in\s+)',306 r'[\1 \2',307 code308 )309 310 # Also fix in VGroup311 # Wrong: VGroup(*[item, for _ in range(10)])312 # Right: VGroup(*[item for _ in range(10)])313 code = re.sub(314 r'VGroup\(\*\[([^,\]]+),\s+(for\s+)',315 r'VGroup(*[\1 \2',316 code317 )318 319 # Fix self.time references in updaters (doesn't exist in Manim)320 # Wrong: lambda m: m.shift(UP * np.sin(self.time))321 # Right: Remove updaters with self.time or replace with ValueTracker322 # Simple fix: comment out lines with self.time in updaters323 lines = code.split('\n')324 filtered_lines = []325 for line in lines:326 if 'add_updater' in line and 'self.time' in line:327 # Comment out problematic updater328 indent = len(line) - len(line.lstrip())329 filtered_lines.append(' ' * indent + '# ' + line.strip() + ' # Removed: self.time not available')330 else:331 filtered_lines.append(line)332 code = '\n'.join(filtered_lines)333 334 # Also fix standalone self.time references (replace with 0 or remove)335 code = code.replace('self.time', '0 # self.time not available')336 337 # If NOT using MovingCameraScene, remove camera.frame/camera.animate references338 if not needs_moving_camera or "MovingCameraScene" not in code:339 # Remove lines with self.camera.frame or self.camera.animate340 lines = code.split('\n')341 filtered_lines = []342 for line in lines:343 if 'self.camera.frame' in line or 'self.camera.animate' in line or 'camera.frame' in line or 'camera.animate' in line:344 # Comment out instead of removing345 filtered_lines.append(' # ' + line.strip() + ' # Removed: requires MovingCameraScene')346 else:347 filtered_lines.append(line)348 code = '\n'.join(filtered_lines)349 350 # Ensure background color is set351 if "self.camera.background_color" not in code:352 code = code.replace("def construct(self):", 353 "def construct(self):\n self.camera.background_color = '#0a0a0a'")354 355 # Ensure proper ending with wait356 if "self.wait(" not in code[-200:]:357 code = code.rstrip() + "\n self.wait(2)"358 359 return code360 361def create_fallback_animation(description: str) -> str:362 """363 Create a simple fallback animation when generation fails.364 365 Args:366 description: User's original description367 368 Returns:369 Basic working Manim code370 """371 safe_desc = description[:40].replace('"', "'")372 return f"""from manim import *373import numpy as np374 375class MathScene(Scene):376 def construct(self):377 self.camera.background_color = '#0a0a0a'378 379 title = Text("{safe_desc}...", font_size=36, color=BLUE)380 title.to_edge(UP)381 382 circle = Circle(radius=1.5, color=PURPLE)383 square = Square(side_length=2, color=ORANGE)384 triangle = Triangle(color=GREEN)385 386 shapes = VGroup(circle, square, triangle)387 shapes.arrange(RIGHT, buff=1)388 389 self.play(Write(title))390 self.wait(0.5)391 self.play(Create(shapes), run_time=2)392 self.play(Rotate(shapes, angle=PI, run_time=2))393 self.play(FadeOut(shapes), run_time=1)394 395 msg = Text("Animation in progress...\\nPlease try again!", 396 font_size=28, color=YELLOW)397 self.play(Write(msg))398 self.wait(2)399"""400 401# ============================================================================402# CORE LLM FUNCTIONS403# ============================================================================404 405def generate_enhanced_prompt(user_request: str, model: str = DEFAULT_MODEL) -> str:406 """407 Generate an enhanced, detailed prompt from user's basic description.408 409 Args:410 user_request: User's animation description411 model: Ollama model to use412 413 Returns:414 Enhanced prompt with rich details415 """416 system_prompt = """You are a Manim animation expert.417Take the user's request and make it slightly more specific for code generation.418 419Rules:420- Maximum 3 short sentences421- Only clarify what the user asked for422- Mention colors if not specified (use: RED, BLUE, GREEN, YELLOW, PURPLE, ORANGE, PINK, WHITE)423- No timestamps, no overly detailed descriptions424- Keep it simple and direct425 426Example:427User: "show the formula E=mcยฒ"428Output: Display the equation E=mcยฒ in large text at the center. Use white text on dark background. Animate it appearing with a write effect.429 430Output ONLY the brief clarification."""431 432 try:433 response = ollama.chat(434 model=model,435 messages=[436 {"role": "system", "content": system_prompt},437 {"role": "user", "content": f'Clarify this animation request in 3 sentences max:\n\n"{user_request}"'}438 ],439 options={440 "temperature": 0.4,441 "top_p": 0.9442 }443 )444 return response['message']['content'].strip()445 except Exception as e:446 st.error(f"โ LLM error during prompt enhancement: {str(e)}")447 return user_request448 449def generate_manim_code(enhanced_prompt: str, model: str = DEFAULT_MODEL) -> str:450 """451 Generate working Manim code from enhanced prompt.452 453 Args:454 enhanced_prompt: Detailed animation plan455 model: Ollama model to use456 457 Returns:458 Python code for Manim animation459 """460 system_prompt = """You are an expert Manim v0.18.1 developer specializing in simple, working mathematical visualizations.461 462CRITICAL REQUIREMENTS:4631. Import: from manim import *; import numpy as np4642. Class: class MathScene(Scene): OR class MathScene(MovingCameraScene): if camera movement needed4653. Background: self.camera.background_color = "#0a0a0a"4664. Use ONLY simple 2D objects: Circle, Square, Rectangle, Line, Dot, Arrow, Text, MathTex4675. End with self.wait(2)4686. Keep animations SIMPLE - just a few play() calls4697. NEVER use more than 10-15 lines of actual animation code470 471CRITICAL - VALID COLORS ONLY:472Use ONLY: RED, BLUE, GREEN, YELLOW, PURPLE, ORANGE, PINK, WHITE, BLACK, GRAY473Color variants: RED_A through RED_E, BLUE_A through BLUE_E, etc.474 475CRITICAL - NO COMPLEX FEATURES:476- NO add_updater with self.time477- NO ImageMobject or SVGMobject478- NO Matrix() - use MathTex instead479- NO complex loops or excessive animations480- Keep it SIMPLE and WORKING481 482CRITICAL - NO EXTERNAL FILES:483- NEVER use ImageMobject() - image files don't exist484- NEVER use SVGMobject() - SVG files don't exist 485- Create all visuals using built-in Manim objects (shapes, text, math)486- Use creative combinations of shapes to represent complex objects487- Example: Instead of ImageMobject("star.png"), use Star() or Polygon()488- For fighter jets, use polygons and rectangles to create simple geometric representations489 490CRITICAL - VALID COLORS ONLY:491- Use ONLY these standard Manim colors: RED, BLUE, GREEN, YELLOW, PURPLE, ORANGE, PINK, WHITE, BLACK, GRAY, MAROON492- Color variants: RED_A through RED_E, BLUE_A through BLUE_E, GREEN_A through GREEN_E, etc.493- NEVER use: RED_BROWN, NEON_BLUE, SOFT_RED, SILVER, SKYBLUE, GOLD (these don't exist)494- When in doubt, use basic colors: RED, BLUE, GREEN, YELLOW, PURPLE, ORANGE495 496CRITICAL - CORRECT SYNTAX:497- Color gradients: Use .set_color_by_gradient(COLOR1, COLOR2) NOT .set_color(color_gradient=[...])498- Random positions: Dot(np.array([np.random.uniform(-5, 5), np.random.uniform(-3, 3), 0]))499- Animations: self.play(obj.animate.method(), run_time=1) NOT obj.animate.method().run_time = 1500- Moving objects: self.play(obj.animate.move_to(position))501- NO MoveToTarget - use obj.animate.move_to() instead502- Camera zoom: self.camera.frame.animate.scale(0.5) NOT self.camera.animate.scale(0.5)503- Matrices: Use MathTex(r"\begin{bmatrix} a & b \\ c & d \end{bmatrix}") NOT Matrix([[a,b],[c,d]])504- Polygons: Polygon(np.array([-2, -1, 0]), np.array([2, -1, 0]), np.array([0, 3, 0]))505- Colors: Only use valid Manim color names (RED, BLUE, etc.)506- Circle: Only radius parameter, NO height/width/depth507- List comprehensions: [item for i in range(10)] NOT [item, for i in range(10)]508- NO add_updater with self.time - self.time doesn't exist in Manim scenes509- For pulsating effects, use animations in a loop instead of updaters510 511IMPORTANT - Camera Movement:512- For camera zoom/pan, use: class MathScene(MovingCameraScene)513- Then use: self.camera.frame.animate.scale() or self.camera.frame.animate.move_to()514- NEVER use self.camera.animate - it doesn't exist! Always use self.camera.frame.animate515- If using regular Scene, DO NOT use any camera animations516 517AVAILABLE OBJECTS:518- Shapes: Circle, Square, Rectangle, Triangle, Polygon, Star, RegularPolygon, Ellipse, Annulus, Arc519- Text: Text, MathTex, Tex, DecimalNumber, Integer520- Lines: Line, Arrow, Vector, DashedLine, Dot, Angle521- Grouping: VGroup, AnimationGroup522- Colors: RED, BLUE, GREEN, YELLOW, PURPLE, ORANGE, PINK, WHITE, BLACK, GRAY, etc.523- Color methods: .set_color(COLOR), .set_fill(COLOR, opacity=0.5), .set_stroke(COLOR, width=2)524- Gradient method: .set_color_by_gradient(COLOR1, COLOR2, COLOR3, ...)525 526CORRECT EXAMPLES:527```python528# Stars with gradient - VALID COLORS ONLY529stars = VGroup(*[Dot(np.array([np.random.uniform(-7, 7), np.random.uniform(-4, 4), 0])) for _ in range(50)])530# NO TRAILING COMMA before 'for'!531stars.set_color_by_gradient(BLUE, WHITE, YELLOW)532 533# Light beams - correct syntax534light_beams = VGroup(*[535 Line(ORIGIN, np.array([np.random.uniform(-5, 5), np.random.uniform(-3, 3), 0]))536 for _ in range(20)537]) # Each line is complete BEFORE the 'for'538 539# Simple geometric jet (no external files!)540jet = VGroup(541 # Body542 Rectangle(width=0.3, height=1, color=WHITE),543 # Wings544 Polygon(545 np.array([-0.5, 0, 0]),546 np.array([0.5, 0, 0]),547 np.array([0, 0.3, 0]),548 color=BLUE549 )550).move_to(LEFT * 3)551 552# Moving animation553self.play(jet.animate.shift(RIGHT * 5), run_time=3)554 555# Color with fill - VALID COLORS556rect = Rectangle(width=4, height=2, color=BLUE, fill_opacity=0.5)557 558# Shockwave using Arc (not Sector with invalid params)559shockwave = Arc(start_angle=0, angle=PI/4, radius=2, color=WHITE, stroke_width=5)560shockwave.move_to(jet.get_center())561 562# Gradient background - VALID COLORS563bg = Rectangle(width=14, height=8, fill_opacity=1)564bg.set_color_by_gradient(PURPLE, PINK, ORANGE, YELLOW)565 566# Camera zoom (MovingCameraScene only!)567class MathScene(MovingCameraScene):568 def construct(self):569 self.play(self.camera.frame.animate.scale(0.5), run_time=2)570 571# Matrix display572matrix = MathTex(573 r"\begin{bmatrix} \cos(\theta) & -\sin(\theta) \\ \sin(\theta) & \cos(\theta) \end{bmatrix}"574).scale(1.5)575 576# Circle - radius ONLY577circle = Circle(radius=2, color=RED, fill_opacity=0.3)578# NOT: Circle(height=4, width=4) - this causes errors!579 580# Ellipse for oval shapes581oval = Ellipse(width=4, height=2, color=GREEN, fill_opacity=0.5)582 583# Pulsating effect - use animation loop, NOT updaters with self.time584# WRONG: energy.add_updater(lambda m: m.shift(UP * np.sin(self.time)))585# RIGHT: Use a for loop with animations586energy = Circle(radius=2, color=YELLOW, fill_opacity=0.5)587for _ in range(3):588 self.play(energy.animate.scale(1.2), run_time=0.5)589 self.play(energy.animate.scale(0.83), run_time=0.5) # Back to original590```591 592AVOID:593- 3D objects (Cone, Cylinder, Sphere, Surface, Cube)594- ImageMobject, SVGMobject (files don't exist!)595- camera.frame with regular Scene class596- Matrix() object (use MathTex with bmatrix instead)597- Invalid color names (RED_BROWN, NEON_BLUE, SOFT_RED, SILVER, GOLD, etc.)598- Circle with height/width/depth parameters599- add_updater with self.time (self.time doesn't exist)600- Overly complex updaters - use simple animations instead601- Undefined variables or imports602- External file dependencies of any kind603- Overly complex animations - keep it simple and working604 605OUTPUT:606- Clean Python code ONLY607- No markdown, no explanations608- One complete working script609- All visuals created from Manim primitives"""610 611 code_prompt = f"""Create a SIMPLE working Manim script for this:612 613{enhanced_prompt}614 615Requirements:616- Class: MathScene(Scene)617- Background: self.camera.background_color = "#0a0a0a"618- Keep it SIMPLE: 10-15 lines of animation max619- Use basic shapes and Text/MathTex620- Just a few self.play() calls621- Valid colors only (RED, BLUE, GREEN, YELLOW, etc.)622 623Return ONLY Python code, no markdown."""624 625 try:626 response = ollama.chat(627 model=model,628 messages=[629 {"role": "system", "content": system_prompt},630 {"role": "user", "content": code_prompt}631 ],632 options={633 "temperature": 0.3,634 "top_p": 0.95635 }636 )637 raw_code = response['message']['content']638 return sanitize_code(raw_code)639 except Exception as e:640 st.error(f"โ LLM error during code generation: {str(e)}")641 return create_fallback_animation(enhanced_prompt[:100])642 643def refine_code_with_modifications(original_code: str, modifications: str, model: str = DEFAULT_MODEL) -> str:644 """645 Refine existing Manim code based on user modifications.646 647 Args:648 original_code: Current Manim code649 modifications: User's requested changes650 model: Ollama model to use651 652 Returns:653 Modified Manim code654 """655 system_prompt = """You are an expert Manim developer specializing in code refinement.656The user has an existing animation and wants to modify it.657 658Your task:6591. Understand the existing code structure6602. Apply the requested modifications precisely6613. Maintain all working parts of the original6624. Ensure the result is valid Manim v0.18.1 code6635. Keep the same class name: MathScene664 665Return ONLY the complete modified code, no explanations."""666 667 refine_prompt = f"""Here is the current Manim code:668 669```python670{original_code}671```672 673Apply these modifications:674{modifications}675 676Return the complete updated code."""677 678 try:679 response = ollama.chat(680 model=model,681 messages=[682 {"role": "system", "content": system_prompt},683 {"role": "user", "content": refine_prompt}684 ],685 options={686 "temperature": 0.2,687 "top_p": 0.9688 }689 )690 raw_code = response['message']['content']691 return sanitize_code(raw_code)692 except Exception as e:693 st.error(f"โ Error refining code: {str(e)}")694 return original_code695 696# ============================================================================697# RENDERING FUNCTION698# ============================================================================699 700def render_manim_animation(701 code: str,702 quality_preset: str,703 custom_resolution: Optional[str] = None,704 custom_fps: Optional[int] = None705) -> Tuple[bool, Optional[Path], str]:706 """707 Render Manim animation from code.708 709 Args:710 code: Python code containing Manim scene711 quality_preset: Quality preset name712 custom_resolution: Optional custom resolution (e.g., "1920x1080")713 custom_fps: Optional custom frame rate714 715 Returns:716 Tuple of (success, video_path, error_message)717 """718 temp_dir = TEMP_DIR / uuid.uuid4().hex[:12]719 temp_dir.mkdir(parents=True, exist_ok=True)720 script_path = temp_dir / "scene.py"721 722 try:723 with open(script_path, "w", encoding="utf-8") as f:724 f.write(code)725 except Exception as e:726 return False, None, f"Failed to write script: {str(e)}"727 728 quality_flag = QUALITY_PRESETS[quality_preset]["flag"]729 730 cmd = [731 "manim", "render",732 str(script_path), "MathScene",733 quality_flag,734 "--format", "mp4",735 "--media_dir", str(temp_dir)736 ]737 738 if custom_resolution:739 width, height = custom_resolution.split("x")740 cmd.extend(["-r", f"{width},{height}"])741 742 if custom_fps:743 cmd.extend(["--frame_rate", str(custom_fps)])744 745 try:746 # Run from the parent directory, not from temp_dir747 result = subprocess.run(748 cmd,749 capture_output=True,750 text=True,751 timeout=600752 )753 754 if result.returncode != 0:755 error_msg = result.stderr[-2000:] if result.stderr else "Unknown error"756 return False, None, error_msg757 758 video_files = list(temp_dir.rglob("*.mp4"))759 if not video_files:760 return False, None, "No video file was generated"761 762 return True, video_files[0], ""763 764 except subprocess.TimeoutExpired:765 return False, None, "Rendering timed out (>10 minutes)"766 except Exception as e:767 return False, None, f"Rendering failed: {str(e)}"768 769# ============================================================================770# STREAMLIT UI771# ============================================================================772 773def render_ui():774 """Main Streamlit UI rendering function."""775 776 st.markdown("""777 <style>778 .main-header {779 text-align: center;780 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);781 -webkit-background-clip: text;782 -webkit-text-fill-color: transparent;783 font-size: 3.5rem;784 font-weight: 800;785 margin-bottom: 0.5rem;786 }787 .sub-header {788 text-align: center;789 color: #a0a0a0;790 font-size: 1.2rem;791 margin-bottom: 2rem;792 }793 .stButton>button {794 border-radius: 8px;795 font-weight: 600;796 transition: all 0.3s;797 }798 </style>799 """, unsafe_allow_html=True)800 801 st.markdown("<h1 class='main-header'>๐ฌ Math to Manim</h1>", unsafe_allow_html=True)802 st.markdown("<p class='sub-header'>Professional Edition โข Transform ideas into cinematic mathematical animations</p>", unsafe_allow_html=True)803 804 initialize_session_state()805 806 with st.sidebar:807 st.header("โ๏ธ Configuration")808 809 st.subheader("๐ค LLM Model")810 model_name = st.text_input("Ollama Model", value=DEFAULT_MODEL, help="Ensure this model is installed")811 812 st.subheader("๐จ Render Quality")813 quality_preset = st.selectbox(814 "Preset",815 options=list(QUALITY_PRESETS.keys()),816 index=1,817 help="Higher quality = longer render time"818 )819 st.caption(f"๐ {QUALITY_PRESETS[quality_preset]['desc']}")820 821 with st.expander("๐ง Advanced Settings"):822 use_custom = st.checkbox("Custom Resolution & FPS")823 if use_custom:824 custom_res = st.text_input("Resolution (WxH)", value="1920x1080", placeholder="1920x1080")825 custom_fps = st.number_input("Frame Rate", min_value=15, max_value=120, value=60, step=15)826 else:827 custom_res = None828 custom_fps = None829 830 st.divider()831 832 if st.session_state.generation_history:833 st.subheader("๐ Recent Generations")834 for i, item in enumerate(reversed(st.session_state.generation_history[-5:])):835 if st.button(f"๐ฌ {item['prompt'][:30]}...", key=f"hist_{i}"):836 st.session_state.generated_code = item['code']837 st.session_state.enhanced_prompt = item['enhanced']838 839 st.divider()840 841 if st.button("๐๏ธ Cleanup Temp Files"):842 cleanup_old_files()843 st.success("Cleaned up old files!")844 845 col1, col2 = st.columns([2, 1])846 847 with col1:848 st.subheader("โ๏ธ Describe Your Animation")849 850 use_example = st.selectbox(851 "Or choose an example:",852 ["Custom..."] + EXAMPLE_PROMPTS,853 index=0854 )855 856 if use_example != "Custom...":857 user_request = st.text_area(858 "Your Vision",859 value=use_example,860 height=200,861 help="Describe what you want to see. Be as detailed or simple as you like!"862 )863 else:864 user_request = st.text_area(865 "Your Vision",866 height=200,867 placeholder="Examples:\nโข Fourier series forming a beating heart\nโข Quantum tunneling through a barrier\nโข Lorenz attractor butterfly effect\nโข Maxwell's equations in waves",868 help="Describe what you want to see!"869 )870 871 with col2:872 st.subheader("๐ฏ Quick Tips")873 st.info("""874 **For best results:**875 - Mention colors, speeds, angles876 - Specify formulas to show877 - Describe camera movement878 - Set the mood (dark, cosmic, vibrant)879 880 **Examples:**881 - "with glowing trails"882 - "show the formula E=mcยฒ"883 - "camera orbits slowly"884 - "dark space background"885 """)886 887 st.divider()888 889 if st.button("๐ Generate Animation", type="primary", use_container_width=True):890 if not user_request.strip():891 st.error("โ Please describe your animation first!")892 st.stop()893 894 with st.spinner("๐จ Crafting your cinematic vision..."):895 enhanced_prompt = generate_enhanced_prompt(user_request, model_name)896 st.session_state.enhanced_prompt = enhanced_prompt897 898 st.success("โ
Vision enhanced!")899 with st.expander("๐ View Enhanced Prompt", expanded=True):900 st.markdown(f"**{enhanced_prompt}**")901 902 with st.spinner("โก Generating Manim masterpiece code..."):903 generated_code = generate_manim_code(enhanced_prompt, model_name)904 st.session_state.generated_code = generated_code905 906 st.success("โ
Code generated!")907 908 with st.spinner("๐ฌ Rendering your animation (this may take a few minutes)..."):909 progress_bar = st.progress(0)910 status_text = st.empty()911 912 for i in range(100):913 time.sleep(0.1)914 progress_bar.progress(i + 1)915 if i < 30:916 status_text.text("Initializing Manim renderer...")917 elif i < 70:918 status_text.text("Rendering frames...")919 else:920 status_text.text("Finalizing video...")921 922 success, video_path, error_msg = render_manim_animation(923 generated_code,924 quality_preset,925 custom_res if use_custom else None,926 custom_fps if use_custom else None927 )928 929 progress_bar.empty()930 status_text.empty()931 932 if success:933 st.session_state.video_path = video_path934 935 st.session_state.generation_history.append({936 'prompt': user_request[:50],937 'enhanced': enhanced_prompt,938 'code': generated_code,939 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S")940 })941 942 st.balloons()943 st.success("๐ **YOUR MASTERPIECE IS READY!**")944 else:945 st.error(f"โ Rendering failed:\n```\n{error_msg}\n```")946 st.warning("๐ก Try simplifying your prompt or check the generated code below")947 948 if st.session_state.video_path and st.session_state.video_path.exists():949 st.divider()950 st.subheader("๐ฅ Your Animation")951 952 col_vid, col_dl = st.columns([3, 1])953 954 with col_vid:955 st.video(str(st.session_state.video_path))956 957 with col_dl:958 with open(st.session_state.video_path, "rb") as f:959 st.download_button(960 label="โฌ๏ธ Download Video",961 data=f,962 file_name=f"animation_{int(time.time())}.mp4",963 mime="video/mp4",964 use_container_width=True965 )966 967 if st.button("๐ Generate New", use_container_width=True):968 st.session_state.video_path = None969 st.rerun()970 971 if st.session_state.generated_code:972 st.divider()973 st.subheader("๐ป Generated Code")974 975 tab1, tab2 = st.tabs(["๐ View Code", "โ๏ธ Refine Animation"])976 977 with tab1:978 st.code(st.session_state.generated_code, language="python", line_numbers=True)979 980 col_copy, col_save = st.columns(2)981 with col_save:982 if st.button("๐พ Save Code to File"):983 save_path = TEMP_DIR / f"saved_scene_{int(time.time())}.py"984 save_path.parent.mkdir(exist_ok=True)985 with open(save_path, "w") as f:986 f.write(st.session_state.generated_code)987 st.success(f"Saved to: {save_path}")988 989 with tab2:990 st.markdown("**Modify your animation by describing changes:**")991 992 modifications = st.text_area(993 "What would you like to change?",994 height=150,995 placeholder="Examples:\nโข Make the colors more vibrant\nโข Add a title at the top\nโข Slow down the animation by 2x\nโข Add a formula showing the equation\nโข Change background to white",996 help="Describe modifications in natural language"997 )998 999 if st.button("๐ Apply Modifications", type="primary"):1000 if not modifications.strip():1001 st.warning("Please describe what you want to change")1002 else:1003 with st.spinner("Refining your animation..."):1004 refined_code = refine_code_with_modifications(1005 st.session_state.generated_code,1006 modifications,1007 model_name1008 )1009 st.session_state.generated_code = refined_code1010 1011 st.success("โ
Code refined! Scroll up and click 'Generate Animation' to render the updated version.")1012 st.rerun()1013 1014 st.divider()1015 st.caption("๐ฌ Math to Manim Professional Edition v2.0.0 โข Powered by Ollama & Manim โข Made with โค๏ธ")1016 1017# ============================================================================1018# MAIN ENTRY POINT1019# ============================================================================1020 1021if __name__ == "__main__":1022 TEMP_DIR.mkdir(exist_ok=True)1023 cleanup_old_files()1024 render_ui()