Animetrix_AI / backend /compiler.py
SayedZahur786's picture
Fix visual clutter and audio sync: enhanced layout zones, audio concatenation, proper timing
ee6d55e
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, '<string>', '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, '<string>', '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