Spaces:
Configuration error
Configuration error
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 | |