CoolFace
Apppublic

Bugsbunny2000/Math-Animation-Generator

sourceHugging Facemitupdated 9mo agoView on Hugging Face
0likes
app-3.py1024 linesDownload Raw Back to root
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()