File size: 10,141 Bytes
a5e880f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd8d357
 
a5e880f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee6d55e
 
 
 
 
 
 
 
 
a5e880f
 
 
 
 
ee6d55e
a5e880f
 
 
 
 
 
 
ee6d55e
 
 
 
 
 
a5e880f
 
 
 
 
 
 
 
 
 
 
 
 
ee6d55e
 
 
 
 
 
 
a5e880f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
082c2a7
a5e880f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd8d357
a5e880f
bd8d357
 
a5e880f
 
 
83cbaf8
 
 
 
a5e880f
bd8d357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a5e880f
bd8d357
 
 
 
a5e880f
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291

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