import os import json import re from google import genai from google.genai import types COMPILER_SYSTEM_PROMPT = """ You are a deterministic Manim script generator for a text-to-educational-animation engine. Your job: Convert the user prompt into a single clean Manim Community Edition Python script. The script will be executed automatically by a backend system, so the structure must be strict. ──────────────────────────── ABSOLUTE REQUIREMENTS ──────────────────────────── Always output ONLY Python code. No markdown No explanations No comments No triple quotes NEVER include self.add_sound() calls NEVER include audio or sound methods Exactly 1 Scene class: class GenScene(Scene): Never use any other scene name. Imports: from manim import * No LaTeX Never use: MathTex Tex Matrix TexTemplate Use only: Text("expression or label") ──────────────────────────── ASCII RULES (CRITICAL) ──────────────────────────── 1. NEVER output Unicode mathematical characters or subscript/superscript glyphs. ❌ Do not use: ² ₁ ₓ α β γ θ π σ → ∞ × ÷ ✔️ Instead use ONLY plain ASCII text: "x^2" instead of "x²" "x_1" instead of "x₁" "pi" instead of "π" "velocity" instead of "→v" 2. The script must be compatible with Windows console and UTF-8 only. - No special glyphs, emoji, arrows, smart quotes, curly quotes, or accents. ──────────────────────────── VISUAL RULES (CRITICAL) ──────────────────────────── 1. NEVER allow visuals to go outside the video frame. - Keep all objects centered or inside safe boundaries. - Do not let squares, shapes, or arrows clip off-screen. - Standard frame is [-7, 7] horizontally and [-4, 4] vertically. Keep well within this. 2. MANDATORY SPACING - No overlapping elements. - Use FadeOut() to CLEAR previous content before showing new content - Minimum buffer between objects: buff=0.5 (NEVER less than 0.4) - Use screen zones: * TOP zone (y=3 to y=2): For titles/headers * MIDDLE zone (y=1 to y=-1): For main content * BOTTOM zone (y=-2 to y=-3): For explanations/labels - ALWAYS clear screen between major steps using self.play(FadeOut(VGroup(*self.mobjects))) - Position text with .to_edge(UP/DOWN) or .shift(UP*2 / DOWN*2) to avoid center crowding 3. Visual accuracy FIRST. - Show geometry clearly. - Avoid rotating or stretching objects unnecessarily. - Avoid random effects. - Use scale(0.7) for text if needed to prevent overflow ──────────────────────────── ANIMATION RULES ──────────────────────────── 1. Timing for audio synchronization: - Each narration step gets approximately 3-4 seconds of animation - Use run_time=1.0 for Write() and Create() - Use run_time=0.8 for FadeIn/FadeOut - Add self.wait(1.5) between major steps for narration - TOTAL scene duration should be 12-20 seconds 2. Only use these animations: Create FadeIn FadeOut Write Transform MoveTo Scale Rotate 3. No 3D, no camera zoom, no cinematic effects, no physics. 4. STRUCTURE each step clearly: - Clear previous content with FadeOut - Show new title/concept - Display visual elements one by one - Add wait time for narration - Transition to next step ──────────────────────────── STRUCTURE & PACING ──────────────────────────── 1. Follow step-by-step logic: - Introduce main idea - Draw objects (one-by-one, not overlapping) - Highlight key components - Explain or show the formula visually - Conclude cleanly 2. Keep total runtime 12–18 seconds. - Use self.wait(1) or self.wait(2) to pace the video. ──────────────────────────── OUTPUT FORMAT EXAMPLE ──────────────────────────── from manim import * class GenScene(Scene): def construct(self): # 1. Introduce title = Text("Concept Name").scale(0.8).to_edge(UP) self.play(Write(title), run_time=1) self.wait(0.5) # 2. Draw Objects box = Square(side_length=2, color=BLUE) self.play(Create(box), run_time=1) self.wait(0.5) # 3. Label (No overlap) label = Text("Side = 2").next_to(box, DOWN, buff=0.5) self.play(Write(label), run_time=1) self.wait(1) # 4. Conclude self.play(FadeOut(box), FadeOut(label), run_time=1) self.wait(1) ──────────────────────────── FINAL OUTPUT RULE ──────────────────────────── ➡️ Return ONLY Python code. ➡️ No formatting, no text, no explanations. ➡️ Only 1 Scene class named GenScene. """ async def generate_manim_code(outline: dict, step_audio_paths=None): outline_str = json.dumps(outline, indent=2) api_key = os.environ.get("GEMINI_API_KEY") if not api_key: raise ValueError("GEMINI_API_KEY not found in environment variables.") client = genai.Client(api_key=api_key) prompt = f"{COMPILER_SYSTEM_PROMPT}\n\nINPUT OUTLINE:\n{outline_str}\n\nPYTHON CODE:" try: response = client.models.generate_content( model='gemini-2.5-flash', contents=prompt ) code = response.text.strip() # Cleanup markdown if present if code.startswith("```python"): code = code[9:] elif code.startswith("```"): code = code[3:] if code.endswith("```"): code = code[:-3] # --- POST-PROCESSING SANITIZATION --- if "MathTex" in code or "Tex(" in code: print("WARNING: Model used LaTeX despite instructions. Sanitizing code...") code = code.replace("MathTex", "Text") code = code.replace("Tex(", "Text(") replacements = { r"^\\circ": " degrees", r"\\circ": " degrees", "°": " degrees", r"\\theta": "theta", "θ": "theta", r"\\pi": "pi", "π": "pi", r"\\alpha": "alpha", "α": "alpha", r"\\beta": "beta", "β": "beta", r"\\gamma": "gamma", "γ": "gamma", r"\\sigma": "sigma", "σ": "sigma", r"\\Delta": "Delta", "Δ": "Delta", r"\\times": "x", "×": "x", r"\\cdot": "*", "·": "*", r"\\div": "/", "÷": "/", r"\\pm": "+/-", "±": "+/-", r"\\approx": "~", "≈": "~", r"\\neq": "!=", "≠": "!=", r"\\le": "<=", "≤": "<=", r"\\ge": ">=", "≥": ">=", r"\\infty": "infinity", "∞": "infinity", r"\\Rightarrow": "->", "⇒": "->", r"\\rightarrow": "->", "→": "->", r"\\leftarrow": "<-", "←": "<-", "²": "^2", "³": "^3", "₁": "_1", "₂": "_2", "ₓ": "_x", r"\\\\": "\n", # Double backslash to newline "–": "-", # En dash to hyphen "—": "-", # Em dash to hyphen "’": "'", # Smart quotes "“": '"', "”": '"', } for pattern, replacement in replacements.items(): code = code.replace(pattern, replacement) # Remove any model-generated self.add_sound calls and self.wait calls lines = code.split('\n') code = '\n'.join([line for line in lines if "self.add_sound" not in line and not line.strip().startswith("self.wait(")]) # Fix .center usage (replace .center with .get_center() only when used as an argument) code = re.sub(r'([\w\)\]]+)\.center(\s*\))', r'\1.get_center()\2', code) code = re.sub(r'class\s+\w+\(Scene\):', 'class GenScene(Scene):', code) # Fix VGroup(*self.mobjects) -> Group(*self.mobjects) # VGroup can only contain VMobjects, but self.mobjects may contain Groups code = re.sub(r'VGroup\(\*self\.mobjects\)', r'Group(*self.mobjects)', code) # DISABLE audio insertion - it causes syntax errors # Audio feature is disabled to prevent malformed code # Validate Python syntax try: compile(code, '', 'exec') print("✓ Generated code validated successfully") except SyntaxError as se: print(f"⚠ Syntax error in generated code: {se}") print(f" Line {se.lineno}: {se.text}") # Try to fix by removing problematic lines lines = code.split('\n') fixed_lines = [] error_line = se.lineno - 1 if se.lineno else -1 for i, line in enumerate(lines): skip = False if i == error_line or "self.add_sound" in line or (line.strip().startswith("self.wait(") and "self.play" not in lines[i-1] if i > 0 else False): print(f" ✗ Removing problematic line {i+1}: {line.strip()}") skip = True if not skip: fixed_lines.append(line) code = '\n'.join(fixed_lines) # Try compiling again try: compile(code, '', 'exec') print("✓ Fixed syntax errors") except SyntaxError as se2: print(f"⚠ Still has syntax errors: {se2}") return code except Exception as e: print(f"Error generating code with Gemini: {e}") raise e