thanhkt commited on
Commit
1645305
·
verified ·
1 Parent(s): ad53396

Upload 13 files

Browse files
Files changed (13) hide show
  1. .env +1 -0
  2. .gitattributes +35 -35
  3. .gitignore +51 -0
  4. Dockerfile +56 -0
  5. README.md +11 -11
  6. ai_agent.py +801 -0
  7. architecture_plan.md +63 -0
  8. code_cleaner.py +184 -0
  9. config.py +129 -0
  10. main.py +307 -0
  11. manim_prompts.py +85 -0
  12. requirements.txt +6 -0
  13. simple_manim_agent.py +329 -0
.env ADDED
@@ -0,0 +1 @@
 
 
1
+ TOGETHER_API_KEY = cee1393e4d4e7a94121882052a03f30a1d51f5dbd251140844ec616e17f60e9b
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment variables
2
+ .env
3
+ .env.*
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+ # Virtual Environment
28
+ venv/
29
+ env/
30
+ ENV/
31
+ .venv/
32
+
33
+ # IDE
34
+ .idea/
35
+ .vscode/
36
+ *.swp
37
+ *.swo
38
+
39
+ # OS
40
+ .DS_Store
41
+ Thumbs.db
42
+
43
+ # Manim output
44
+ media/
45
+ media/videos/
46
+ *.mp4
47
+
48
+ # Ignore GIFs except in SpatialReasoningTest
49
+ *.gif
50
+ !SpatialReasoningTest/*.gif
51
+
Dockerfile ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ # Install all dependencies in one layer
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ gcc \
6
+ libffi-dev \
7
+ curl \
8
+ ca-certificates \
9
+ python3-dev \
10
+ pkg-config \
11
+ libcairo2-dev \
12
+ libpango1.0-dev \
13
+ ffmpeg \
14
+ texlive-full \
15
+ dvisvgm \
16
+ fonts-dejavu \
17
+ && apt-get clean \
18
+ && rm -rf /var/lib/apt/lists/* \
19
+ && which dvisvgm
20
+
21
+ WORKDIR /app
22
+
23
+ # Install uv for faster pip operations
24
+ ADD https://astral.sh/uv/install.sh /uv-installer.sh
25
+ RUN sh /uv-installer.sh && rm /uv-installer.sh
26
+ ENV PATH="/root/.local/bin:${PATH}"
27
+
28
+ # Copy virtual environment and project files
29
+ COPY requirements.txt .
30
+
31
+
32
+ # Activate the existing venv and install any missing packages
33
+ RUN uv venv /app/manimations \
34
+ && . /app/manimations/bin/activate \
35
+ && uv pip install --no-cache -r requirements.txt \
36
+ && uv pip install --no-cache pycairo pangocffi manim
37
+
38
+ ENV PATH="/app/manimations/bin:${PATH}"
39
+ COPY *.py /app/
40
+ # Set environment variables
41
+ ENV PYTHONPATH=/app
42
+ ENV MPLBACKEND=Agg
43
+ ENV GRADIO_SERVER_NAME=0.0.0.0
44
+ ENV GRADIO_SERVER_PORT=7860
45
+
46
+ # Create directory for generated videos
47
+ RUN mkdir -p /app/generated_videos
48
+
49
+ # Copy .env file
50
+ COPY .env /app/.env
51
+
52
+ # Expose the port for the Gradio interface
53
+ EXPOSE 7860
54
+
55
+ # Command to run the application with the virtual environment
56
+ CMD ["/app/manimations/bin/python", "ai_agent.py"]
README.md CHANGED
@@ -1,11 +1,11 @@
1
- ---
2
- title: Text2manim
3
- emoji: 📊
4
- colorFrom: red
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- short_description: Text to Math, Physic video using Manim and AI agent
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---
2
+ title: Text2manim
3
+ emoji: 📊
4
+ colorFrom: red
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ short_description: Text to Math, Physic video using Manim and AI agent
9
+ ---
10
+
11
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
ai_agent.py ADDED
@@ -0,0 +1,801 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AI Agent for generating Manim animations from text prompts using pydantic-ai.
3
+ """
4
+
5
+ import os
6
+ from typing import List, Optional
7
+ from dotenv import load_dotenv
8
+ import gradio as gr
9
+ from pydantic_ai.models.openai import OpenAIModel
10
+ from pydantic_ai.providers.openai import OpenAIProvider
11
+ from pydantic_ai import Agent, RunContext
12
+ from pydantic import BaseModel, Field
13
+ import openai
14
+ import tempfile
15
+ import subprocess
16
+ import logging
17
+ from datetime import datetime
18
+ import shutil
19
+ import time
20
+ from io import StringIO
21
+ import re
22
+ import json
23
+ import logging
24
+
25
+
26
+ # Configure logging
27
+ logging.basicConfig(level=logging.INFO)
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Load environment variables
31
+ load_dotenv()
32
+ llm = "deepseek-ai/DeepSeek-V3"
33
+
34
+ # Define Pydantic models for structured data
35
+ class AnimationPrompt(BaseModel):
36
+ """User input for animation generation."""
37
+ description: str = Field(..., description="Text description of the mathematical or physics concept to animate")
38
+ complexity: str = Field("medium", description="Desired complexity of the animation")
39
+
40
+ class AnimationScenario(BaseModel):
41
+ """Structured scenario for animation generation."""
42
+ title: str = Field(..., description="Title of the animation")
43
+ objects: List[str] = Field(..., description="Mathematical objects to include in the animation")
44
+ transformations: List[str] = Field(..., description="Transformations to apply to the objects")
45
+ equations: Optional[List[str]] = Field(None, description="Mathematical equations to visualize")
46
+
47
+ class AnimationResult(BaseModel):
48
+ """Result of animation generation."""
49
+ code: str = Field(..., description="Generated Manim code")
50
+ video_path: str = Field(..., description="Path to the generated video file")
51
+
52
+ model = OpenAIModel(
53
+ 'deepseek-ai/DeepSeek-V3',
54
+ provider=OpenAIProvider(
55
+ base_url='https://api.together.xyz/v1', api_key=os.environ.get('TOGETHER_API_KEY')
56
+ ),
57
+ )
58
+ # Create the agent with a static system prompt
59
+ manim_agent = Agent(
60
+ model, # or use Together API as needed
61
+ deps_type=AnimationPrompt, # Use AnimationPrompt as dependency type
62
+ system_prompt=(
63
+ "You are a specialized AI agent for creating mathematical animations. "
64
+ "Your goal is to convert user descriptions into precise Manim code "
65
+ "that visualizes mathematical and physics concepts clearly and elegantly."
66
+ ),
67
+ )
68
+
69
+ # Configure OpenAI client to use Together API
70
+ client = openai.OpenAI(
71
+ api_key=os.environ.get("TOGETHER_API_KEY"),
72
+ base_url="https://api.together.xyz/v1",
73
+ )
74
+
75
+ # Add dynamic system prompts
76
+ @manim_agent.system_prompt
77
+ def add_complexity_guidance(ctx: RunContext[AnimationPrompt]) -> str:
78
+ """Add guidance based on requested complexity."""
79
+ complexity = ctx.deps.complexity
80
+ if complexity == "simple":
81
+ return (
82
+ "Create simple, easy-to-understand animations with minimal elements. "
83
+ "Focus on clarity over sophistication."
84
+ )
85
+ elif complexity == "complex":
86
+ return (
87
+ "Create sophisticated animations with multiple mathematical elements and transformations. "
88
+ "You can use advanced Manim features and complex mathematical concepts."
89
+ )
90
+ else: # medium
91
+ return (
92
+ "Balance clarity and sophistication in your animations. "
93
+ "Include enough detail to illustrate the concept clearly without overwhelming the viewer."
94
+ )
95
+
96
+ @manim_agent.system_prompt
97
+ def add_timestamp() -> str:
98
+ """Add a timestamp to the system prompt."""
99
+ return f"Current date and time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
100
+
101
+ @manim_agent.tool
102
+ def extract_scenario(ctx: RunContext[AnimationPrompt]) -> AnimationScenario:
103
+ """Extract a structured animation scenario from a text prompt."""
104
+ prompt = ctx.deps # Get the AnimationPrompt from context
105
+
106
+ # Use Together API with OpenAI client
107
+ response = client.chat.completions.create(
108
+ model=llm,
109
+ messages=[
110
+ {"role": "system", "content": """
111
+ Create a storyboard for a math/physics educational animation. Focus on making concepts clear for beginners.
112
+
113
+ Respond with a JSON object containing:
114
+ - title: A clear, engaging title
115
+ - objects: Mathematical objects to include (e.g., "coordinate_plane", "function_graph")
116
+ - transformations: Animation types to use (e.g., "fade_in", "transform")
117
+ - equations: Mathematical equations to feature (can be null)
118
+ - storyboard: 5-7 sections, each with:
119
+ * section_name: Section name (e.g., "Introduction")
120
+ * time_range: Timestamp range (e.g., "0:00-2:00")
121
+ * narration: What the narrator says
122
+ * visuals: What appears on screen
123
+ * animations: Specific animations
124
+ * key_points: 1-2 main takeaways
125
+
126
+ Include: introduction, simple explanation, detailed walkthrough, examples, and conclusion.
127
+
128
+ Use everyday analogies, define technical terms, and focus on visualization.
129
+
130
+ Only respond with the JSON object, nothing else.
131
+ """},
132
+ {"role": "user", "content": f"Create an animation storyboard for: '{prompt.description}'. "
133
+ f"Complexity level: {prompt.complexity}. Make it beginner-friendly "
134
+ f"with clear explanations and visual examples."}
135
+ ]
136
+ )
137
+ content = response.choices[0].message.content
138
+
139
+ try:
140
+ # Extract JSON from response
141
+ json_match = re.search(r'\{.*\}', content, re.DOTALL)
142
+ if json_match:
143
+ json_str = json_match.group(0)
144
+ scenario_dict = json.loads(json_str)
145
+
146
+ # Get basic scenario info
147
+ title = scenario_dict.get('title', f"{prompt.description.capitalize()} Visualization")
148
+ objects = scenario_dict.get('objects', [])
149
+ transformations = scenario_dict.get('transformations', [])
150
+ equations = scenario_dict.get('equations', None)
151
+
152
+ # Store the storyboard in logger
153
+ if 'storyboard' in scenario_dict:
154
+ logger.info(f"Generated storyboard: {json.dumps(scenario_dict['storyboard'], indent=2)}")
155
+
156
+ return AnimationScenario(
157
+ title=title,
158
+ objects=objects,
159
+ transformations=transformations,
160
+ equations=equations
161
+ )
162
+ except Exception as e:
163
+ logger.error(f"Error parsing scenario JSON: {e}")
164
+
165
+ # Fallback with default values
166
+ return AnimationScenario(
167
+ title=f"{prompt.description.capitalize()} Visualization",
168
+ objects=["circle", "text", "coordinate_system"],
169
+ transformations=["creation", "transformation", "highlight"],
170
+ equations=None
171
+ )
172
+
173
+ # Also simplify extract_scenario_direct with the same approach
174
+ def extract_scenario_direct(prompt: str, complexity: str = "medium") -> AnimationScenario:
175
+ """Direct implementation of scenario extraction without using RunContext."""
176
+ # Use Together API with OpenAI client
177
+ response = client.chat.completions.create(
178
+ model=llm,
179
+ messages=[
180
+ {"role": "system", "content": """
181
+ Create a storyboard for a math/physics educational animation. Focus on making concepts clear for beginners.
182
+
183
+ Respond with a JSON object containing:
184
+ - title: A clear, engaging title
185
+ - objects: Mathematical objects to include (e.g., "coordinate_plane", "function_graph")
186
+ - transformations: Animation types to use (e.g., "fade_in", "transform")
187
+ - equations: Mathematical equations to feature (can be null)
188
+ - storyboard: 5-7 sections, each with:
189
+ * section_name: Section name (e.g., "Introduction")
190
+ * time_range: Timestamp range (e.g., "0:00-2:00")
191
+ * narration: What the narrator says
192
+ * visuals: What appears on screen
193
+ * animations: Specific animations
194
+ * key_points: 1-2 main takeaways
195
+
196
+ Include: introduction, simple explanation, detailed walkthrough, examples, and conclusion.
197
+
198
+ Use everyday analogies, define technical terms, and focus on visualization.
199
+
200
+ Only respond with the JSON object, nothing else.
201
+ """},
202
+ {"role": "user", "content": f"Create an animation storyboard for: '{prompt}'. "
203
+ f"Complexity level: {complexity}. Make it beginner-friendly "
204
+ f"with clear explanations and visual examples."}
205
+ ]
206
+ )
207
+ content = response.choices[0].message.content
208
+
209
+ try:
210
+ # Extract JSON from response
211
+ json_match = re.search(r'\{.*\}', content, re.DOTALL)
212
+ if json_match:
213
+ json_str = json_match.group(0)
214
+ scenario_dict = json.loads(json_str)
215
+
216
+ # Get basic scenario info
217
+ title = scenario_dict.get('title', f"{prompt.capitalize()} Visualization")
218
+ objects = scenario_dict.get('objects', [])
219
+ transformations = scenario_dict.get('transformations', [])
220
+ equations = scenario_dict.get('equations', None)
221
+
222
+ # Store the storyboard in logger
223
+ if 'storyboard' in scenario_dict:
224
+ logger.info(f"Generated storyboard: {json.dumps(scenario_dict['storyboard'], indent=2)}")
225
+
226
+ return AnimationScenario(
227
+ title=title,
228
+ objects=objects,
229
+ transformations=transformations,
230
+ equations=equations
231
+ )
232
+ except Exception as e:
233
+ logger.error(f"Error parsing scenario JSON: {e}")
234
+
235
+ # Fallback based on keywords in prompt
236
+ objects = ["circle", "text", "coordinate_system"]
237
+ transformations = ["creation", "transformation", "highlight"]
238
+ equations = None
239
+
240
+ if any(kw in prompt.lower() for kw in ["triangle", "pythagorean"]):
241
+ objects = ["triangle", "square", "text"]
242
+ transformations = ["creation", "area_calculation"]
243
+ equations = ["a^2 + b^2 = c^2"]
244
+ elif any(kw in prompt.lower() for kw in ["calculus", "derivative", "integral"]):
245
+ objects = ["function_graph", "tangent_line", "area"]
246
+ transformations = ["drawing", "zoom", "fill"]
247
+ equations = ["f'(x) = \\lim_{h \\to 0}\\frac{f(x+h) - f(x)}{h}"]
248
+
249
+ return AnimationScenario(
250
+ title=f"{prompt.capitalize()} Visualization",
251
+ objects=objects,
252
+ transformations=transformations,
253
+ equations=equations
254
+ )
255
+
256
+ @manim_agent.tool
257
+ def generate_code(ctx: RunContext[AnimationPrompt], scenario: AnimationScenario) -> str:
258
+ """Generate Manim code from a structured scenario."""
259
+ # Use OpenAI to generate Manim code
260
+ objects_str = ", ".join(scenario.objects)
261
+ transformations_str = ", ".join(scenario.transformations)
262
+ equations_str = ", ".join(scenario.equations) if scenario.equations else "No equations"
263
+
264
+ prompt_description = ctx.deps.description # Access the original prompt
265
+ response = client.chat.completions.create(
266
+ model=llm,
267
+ messages=[
268
+ {"role": "system", "content": "Generate Manim code for mathematical animations."},
269
+ {"role": "user", "content": f"Create Manim code for an animation titled '{scenario.title}' "
270
+ f"with objects: {objects_str}, transformations: {transformations_str}, "
271
+ f"and equations: {equations_str}. Original request: '{prompt_description}'"}
272
+ ]
273
+ )
274
+ return response.choices[0].message.content
275
+
276
+ @manim_agent.tool_plain
277
+ def render_animation(code: str, quality="medium_quality") -> str:
278
+ """Render Manim code into a video. This doesn't need the context."""
279
+ return render_manim_video(code, quality)
280
+
281
+ def render_manim_video(code, quality="medium_quality"):
282
+ try:
283
+ temp_dir = tempfile.mkdtemp()
284
+ script_path = os.path.join(temp_dir, "manim_script.py")
285
+
286
+ with open(script_path, "w") as f:
287
+ f.write(code)
288
+
289
+ class_name = None
290
+ for line in code.split("\n"):
291
+ if line.startswith("class ") and "Scene" in line:
292
+ class_name = line.split("class ")[1].split("(")[0].strip()
293
+ break
294
+
295
+ if not class_name:
296
+ return "Error: Could not identify the Scene class in the generated code."
297
+
298
+ if quality == "high_quality":
299
+ command = ["manim", "-qh", script_path, class_name]
300
+ quality_dir = "1080p60"
301
+ elif quality == "low_quality":
302
+ command = ["manim", "-ql", script_path, class_name]
303
+ quality_dir = "480p15"
304
+ else:
305
+ command = ["manim", "-qm", script_path, class_name]
306
+ quality_dir = "720p30"
307
+
308
+ logger.info(f"Executing command: {' '.join(command)}")
309
+
310
+ result = subprocess.run(command, cwd=temp_dir, capture_output=True, text=True)
311
+
312
+ logger.info(f"Manim stdout: {result.stdout}")
313
+ logger.error(f"Manim stderr: {result.stderr}")
314
+
315
+ if result.returncode != 0:
316
+ logger.error(f"Manim execution failed: {result.stderr}")
317
+ return f"Error rendering video: {result.stderr}"
318
+
319
+ media_dir = os.path.join(temp_dir, "media")
320
+ videos_dir = os.path.join(media_dir, "videos")
321
+
322
+ if not os.path.exists(videos_dir):
323
+ return "Error: No video was generated. Check if Manim is installed correctly."
324
+
325
+ scene_dirs = [d for d in os.listdir(videos_dir) if os.path.isdir(os.path.join(videos_dir, d))]
326
+
327
+ if not scene_dirs:
328
+ return "Error: No scene directory found in the output."
329
+
330
+ scene_dir = max([os.path.join(videos_dir, d) for d in scene_dirs], key=os.path.getctime)
331
+
332
+ mp4_files = [f for f in os.listdir(os.path.join(scene_dir, quality_dir)) if f.endswith(".mp4")]
333
+
334
+ if not mp4_files:
335
+ return "Error: No MP4 file was generated."
336
+
337
+ video_file = max([os.path.join(scene_dir, quality_dir, f) for f in mp4_files], key=os.path.getctime)
338
+
339
+ output_dir = os.path.join(os.getcwd(), "generated_videos")
340
+ os.makedirs(output_dir, exist_ok=True)
341
+
342
+ timestamp = int(time.time())
343
+ output_file = os.path.join(output_dir, f"manim_video_{timestamp}.mp4")
344
+
345
+ shutil.copy2(video_file, output_file)
346
+
347
+ logger.info(f"Video generated: {output_file}")
348
+
349
+ return output_file
350
+
351
+ except Exception as e:
352
+ logger.error(f"Error rendering video: {e}")
353
+ return f"Error: {str(e)}"
354
+ finally:
355
+ try:
356
+ shutil.rmtree(temp_dir)
357
+ except Exception as e:
358
+ logger.error(f"Error cleaning up temporary directory: {e}")
359
+
360
+ def format_log_output(scenario: AnimationScenario, code: str) -> str:
361
+ """Format scenario and code for display in UI."""
362
+ log_output = f"## Animation Scenario\n\n"
363
+ log_output += f"**Title:** {scenario.title}\n\n"
364
+
365
+ # Check if we have a storyboard in the logger
366
+ import json
367
+ import re
368
+ from io import StringIO
369
+ import logging
370
+
371
+ # Create a string buffer to capture log output
372
+ log_buffer = StringIO()
373
+ log_handler = logging.StreamHandler(log_buffer)
374
+ logger.addHandler(log_handler)
375
+
376
+ # Extract storyboard from logs if possible
377
+ storyboard = None
378
+ log_handler.flush()
379
+ logs = log_buffer.getvalue()
380
+ logger.removeHandler(log_handler)
381
+
382
+ json_match = re.search(r'Generated storyboard: (\[.*\])', logs)
383
+ if json_match:
384
+ try:
385
+ storyboard_str = json_match.group(1)
386
+ storyboard = json.loads(storyboard_str)
387
+ except:
388
+ storyboard = None
389
+
390
+ # If storyboard exists, display it
391
+ if storyboard:
392
+ log_output += f"## Animation Storyboard\n\n"
393
+ for i, section in enumerate(storyboard):
394
+ log_output += f"### {i+1}. {section.get('section_name', 'Section')}\n"
395
+ log_output += f"**Time:** {section.get('time_range', 'N/A')}\n\n"
396
+ log_output += f"**Narration:** {section.get('narration', '')}\n\n"
397
+ log_output += f"**Visuals:** {section.get('visuals', '')}\n\n"
398
+ log_output += f"**Animations:** {', '.join(section.get('animations', []))}\n\n"
399
+
400
+ if 'key_points' in section and section['key_points']:
401
+ log_output += f"**Key Points:**\n"
402
+ if isinstance(section['key_points'], list):
403
+ for point in section['key_points']:
404
+ log_output += f"- {point}\n"
405
+ else:
406
+ log_output += f"{section['key_points']}\n"
407
+
408
+ log_output += "---\n\n"
409
+
410
+ # Continue with regular output
411
+ log_output += f"**Mathematical Objects:**\n"
412
+ for obj in scenario.objects:
413
+ log_output += f"- {obj}\n"
414
+
415
+ log_output += f"\n**Transformations:**\n"
416
+ for transform in scenario.transformations:
417
+ log_output += f"- {transform}\n"
418
+
419
+ if scenario.equations:
420
+ log_output += f"\n**Equations:**\n"
421
+ for eq in scenario.equations:
422
+ log_output += f"- {eq}\n"
423
+
424
+ log_output += f"\n## Generated Manim Code\n\n```python\n{code}\n```"
425
+
426
+ return log_output
427
+
428
+ # Add a memory class to store conversation history
429
+ class ConversationMemory:
430
+ def __init__(self):
431
+ self.history = []
432
+ self.current_scenario = None
433
+ self.current_code = None
434
+
435
+ def add_interaction(self, prompt, scenario, code, video_path):
436
+ self.history.append({
437
+ "prompt": prompt,
438
+ "scenario": scenario,
439
+ "code": code,
440
+ "video_path": video_path,
441
+ "timestamp": datetime.now().isoformat()
442
+ })
443
+ self.current_scenario = scenario
444
+ self.current_code = code
445
+
446
+ def get_context_for_refinement(self):
447
+ if not self.history:
448
+ return ""
449
+
450
+ # Construct context from the last interaction
451
+ last = self.history[-1]
452
+ context = f"Previous prompt: {last['prompt']}\n"
453
+ if self.current_scenario and hasattr(self.current_scenario, 'title'):
454
+ context += f"Current animation title: {self.current_scenario.title}\n"
455
+ return context
456
+
457
+ # Initialize the memory
458
+ memory = ConversationMemory()
459
+
460
+ # Function to refine animation based on feedback
461
+ def refine_animation(code: str, feedback: str, quality: str = "medium_quality") -> tuple:
462
+ """Refine animation based on user feedback."""
463
+ try:
464
+ # Get context from memory
465
+ context = memory.get_context_for_refinement()
466
+
467
+ # Use LLM to refine the code based on feedback
468
+ response = client.chat.completions.create(
469
+ model=llm,
470
+ messages=[
471
+ {"role": "system", "content": """
472
+ You are a Manim code expert. Your task is to refine animation code based on user feedback.
473
+ Keep the overall structure and purpose of the animation, but implement the changes requested.
474
+ Make sure the code remains valid and follows Manim best practices.
475
+
476
+ IMPORTANT REQUIREMENTS:
477
+ 1. Only return the complete, corrected Manim code
478
+ 2. Ensure class name and structure remains consistent
479
+ 3. All changes must be compatible with Manim Community edition
480
+ 4. Do not explain your changes in comments outside of helpful inline comments
481
+ """},
482
+ {"role": "user", "content": f"Here is the current Manim animation code:\n\n```python\n{code}\n```\n\n{context}\nPlease refine this code based on this feedback: \"{feedback}\"\n\nReturn only the improved code."}
483
+ ]
484
+ )
485
+
486
+ refined_code = response.choices[0].message.content.strip()
487
+
488
+ # Remove any markdown code formatting if present
489
+ if refined_code.startswith("```python"):
490
+ refined_code = refined_code.split("```python", 1)[1]
491
+ if refined_code.endswith("```"):
492
+ refined_code = refined_code.rsplit("```", 1)[0]
493
+
494
+ refined_code = refined_code.strip()
495
+
496
+ # Render the refined code
497
+ video_path = render_manim_video(refined_code, quality)
498
+
499
+ if video_path and not video_path.startswith("Error"):
500
+ # Update memory with refined code
501
+ if memory.current_scenario:
502
+ memory.current_code = refined_code
503
+
504
+ return refined_code, video_path, f"## Refined Animation\n\nFeedback incorporated: \"{feedback}\"\n\nAnimation successfully rendered."
505
+ else:
506
+ return refined_code, None, f"## Error in Rendering\n\n```\n{video_path}\n```\n\nPlease check your code for errors."
507
+
508
+ except Exception as e:
509
+ logger.error(f"Error refining animation: {e}")
510
+ return code, None, f"## Error in Refinement\n\n```\n{str(e)}\n```\n\nPlease try again with different feedback."
511
+
512
+ # Function to process user request
513
+ def generate_animation(prompt: str, complexity: str = "medium", quality: str = "medium_quality") -> tuple:
514
+ """Generate an animation from a text prompt."""
515
+ try:
516
+ # Create prompt object with complexity
517
+ prompt_obj = AnimationPrompt(description=prompt, complexity=complexity)
518
+
519
+ # Run the agent in a way that it will use all necessary tools
520
+ result = manim_agent.run_sync(
521
+ f"Generate an animation from this description: {prompt}. "
522
+ f"First, extract the key elements of the scenario. Then, generate "
523
+ f"the Manim code for the animation. Finally, render the animation.",
524
+ deps=prompt_obj
525
+ )
526
+
527
+ # As a fallback, we'll use the direct methods
528
+ scenario = extract_scenario_direct(prompt, complexity)
529
+
530
+ # Fix: Use generate_code_direct instead of generate_code
531
+ # generate_code is an agent tool that requires a RunContext
532
+ code = generate_code_direct(prompt, scenario, complexity)
533
+
534
+ video_path = render_manim_video(code, quality) # Use the new render function
535
+
536
+ log_output = format_log_output(scenario, code)
537
+
538
+ # Store in memory
539
+ memory.add_interaction(prompt, scenario, code, video_path)
540
+
541
+ return code, video_path, log_output
542
+ except Exception as e:
543
+ logger.error(f"Error generating animation: {e}")
544
+ return f"Error: {str(e)}", None, f"Error occurred: {str(e)}"
545
+
546
+ # Add the missing generate_code_direct function if it doesn't exist
547
+ def generate_code_direct(prompt: str, scenario: AnimationScenario, complexity: str = "medium") -> str:
548
+ """Direct implementation of code generation without using RunContext."""
549
+ # Use Together API with OpenAI client
550
+ objects_str = ", ".join(scenario.objects)
551
+ transformations_str = ", ".join(scenario.transformations)
552
+ equations_str = ", ".join(scenario.equations) if scenario.equations else "No equations"
553
+
554
+ # Try to get storyboard from logger if it exists
555
+ storyboard_info = ""
556
+ from io import StringIO
557
+ import re
558
+ import json
559
+ import logging
560
+
561
+ # Create a string buffer to capture log output
562
+ log_buffer = StringIO()
563
+ log_handler = logging.StreamHandler(log_buffer)
564
+ logger.addHandler(log_handler)
565
+ log_handler.flush()
566
+ logs = log_buffer.getvalue()
567
+ logger.removeHandler(log_handler)
568
+
569
+ # Extract storyboard from logs if possible
570
+ json_match = re.search(r'Generated storyboard: (\[.*\])', logs)
571
+ if json_match:
572
+ try:
573
+ storyboard_str = json_match.group(1)
574
+ storyboard = json.loads(storyboard_str)
575
+ storyboard_info = "Follow this narrative structure in your animation:\n"
576
+ for i, section in enumerate(storyboard):
577
+ storyboard_info += f"Section {i+1}: {section.get('section_name', 'Section')} - {section.get('time_range', 'N/A')}\n"
578
+ storyboard_info += f"Narration: {section.get('narration', '')}\n"
579
+ storyboard_info += f"Visuals: {section.get('visuals', '')}\n"
580
+ storyboard_info += f"Animations: {', '.join(section.get('animations', []))}\n\n"
581
+ except:
582
+ storyboard_info = ""
583
+
584
+ response = client.chat.completions.create(
585
+ model=llm,
586
+ messages=[
587
+ {"role": "system", "content": f"""
588
+ Create professional Manim animation code that explains mathematical concepts clearly and elegantly. Your code MUST:
589
+
590
+ TECHNICAL REQUIREMENTS:
591
+ 1. Use 'from manim import *' at the top
592
+ 2. Create a Scene class named 'ManimScene' that extends Scene
593
+ 3. Implement the construct method correctly
594
+ 4. Use only standard Manim Community edition objects and methods
595
+ 5. Include proper self.play() and self.wait() calls with appropriate durations
596
+ 6. Use valid LaTeX syntax for all mathematical expressions
597
+ 7. Be fully compilable without errors
598
+ 8. Include helpful comments explaining each section
599
+ 9. Just return python code, do not include apostrophe in front and back of code
600
+
601
+ VISUAL STRUCTURE AND LAYOUT:
602
+ 1. Structure the animation as a narrative with clear sections (introduction, explanation, conclusion)
603
+ 2. Create title screens with engaging typography and animations
604
+ 3. Position ALL elements with EXPLICIT coordinates using shift() or move_to() methods
605
+ 4. Ensure AT LEAST 1.5 units of space between separate visual elements
606
+ 5. For equations, use MathTex with proper scaling (scale(0.8) for complex equations)
607
+ 6. Group related objects using VGroup and arrange them with arrange() method
608
+ 7. When showing multiple equations, use arrange_in_grid() or arrange() with DOWN/RIGHT
609
+ 8. For graphs, set explicit x_range and y_range with generous padding around functions
610
+ 9. Scale ALL text elements appropriately (Title: 1.2, Headers: 1.0, Body: 0.8)
611
+ 10. Use colors consistently and meaningfully (BLUE for emphasis, RED for important points)
612
+ 11. Preventing overlaps of element, choose position for each element carefully, display element and text then move to next element
613
+
614
+ ANIMATION TECHNIQUES:
615
+ 1. Use FadeIn for introductions of new elements
616
+ 2. Apply TransformMatchingTex when evolving equations
617
+ 3. Use Create for drawing geometric objects
618
+ 4. Implement smooth transitions between different concepts with ReplacementTransform
619
+ 5. Highlight important parts with Indicate or Circumscribe
620
+ 6. Add pauses (self.wait()) after important points for comprehension
621
+ 7. For complex animations, break them into smaller steps with appropriate timing
622
+ 8. Use MoveAlongPath for demonstrating motion or change over time
623
+ 9. Create emphasis with scale_about_point or succession of animations
624
+ 10. Use camera movements sparingly and smoothly
625
+
626
+ EDUCATIONAL CLARITY:
627
+ 1. Begin with simple concepts and build to more complex ones
628
+ 2. Reveal information progressively, not all at once
629
+ 3. Use visual metaphors to represent abstract concepts
630
+ 4. Include clear labels for all important elements
631
+ 5. When showing equations, animate their components step by step
632
+ 6. Provide visual explanations alongside mathematical notation
633
+ 7. Use consistent notation throughout the animation
634
+ 8. Show practical applications or examples of the concept
635
+ 9. Summarize key points at the end of the animation
636
+
637
+ {storyboard_info}
638
+
639
+ RESPOND WITH CLEAN, WELL-STRUCTURED CODE ONLY. DO NOT INCLUDE EXPLANATIONS OUTSIDE OF CODE COMMENTS.
640
+ """
641
+ },
642
+ {"role": "user", "content": f"Create a comprehensive Manim animation for '{scenario.title}' that teaches this concept: '{prompt}'. \n\nUse these mathematical objects: {objects_str}. \nImplement these transformations/animations: {transformations_str}. \nFeature these equations: {equations_str}. \n\nComplexity level: {complexity}. \n\nEnsure all elements are properly spaced and positioned to prevent overlap. Structure the animation with a clear introduction, step-by-step explanation, and conclusion."}
643
+ ]
644
+ )
645
+ return response.choices[0].message.content
646
+
647
+ # Function to re-render animation with edited code
648
+ def rerender_animation(edited_code: str, quality: str = "medium_quality") -> tuple:
649
+ """Re-render animation with user-edited code."""
650
+ try:
651
+ video_path = render_manim_video(edited_code, quality)
652
+ if video_path and not video_path.startswith("Error"):
653
+ return video_path, f"## Re-rendered Animation\n\nCode successfully rendered to video.\n\nCheck the video player for results."
654
+ else:
655
+ return None, f"## Error in Rendering\n\n```\n{video_path}\n```\n\nPlease check your code for errors."
656
+ except Exception as e:
657
+ logger.error(f"Error re-rendering animation: {e}")
658
+ return None, f"## Error in Rendering\n\n```\n{str(e)}\n```\n\nPlease check your code for errors."
659
+
660
+ # Setup Gradio interface
661
+ def gradio_interface(prompt: str, complexity: str = "medium", quality: str = "medium_quality"):
662
+ code, video_path, log_output = generate_animation(prompt, complexity, quality)
663
+ if video_path and not video_path.startswith("Error"):
664
+ return code, video_path, log_output
665
+ else:
666
+ return code, None, log_output
667
+
668
+ # Replace the Gradio interface creation with a Blocks interface for better layout control
669
+ if __name__ == "__main__":
670
+ with gr.Blocks(title="Manimation Generator", theme=gr.themes.Base()) as demo:
671
+ gr.Markdown("# Manimation Generator")
672
+ gr.Markdown("Generate mathematical animations from text descriptions using AI")
673
+
674
+ # Add chat history component
675
+ chat_history = gr.Chatbot(label="Conversation History", height=300)
676
+
677
+ with gr.Row():
678
+ # Left column: User inputs
679
+ with gr.Column(scale=1):
680
+ # Replace single prompt with tabs for initial creation and feedback
681
+ with gr.Tabs():
682
+ with gr.TabItem("Create New Animation"):
683
+ new_prompt = gr.Textbox(
684
+ lines=5,
685
+ placeholder="Describe a mathematical concept to animate...",
686
+ label="Concept Description"
687
+ )
688
+
689
+ with gr.Row():
690
+ complexity = gr.Radio(
691
+ ["simple", "medium", "complex"],
692
+ value="medium",
693
+ label="Complexity Level"
694
+ )
695
+ quality = gr.Radio(
696
+ ["low_quality", "medium_quality", "high_quality"],
697
+ value="medium_quality",
698
+ label="Video Quality"
699
+ )
700
+
701
+ generate_btn = gr.Button("Generate Animation", variant="primary")
702
+
703
+ with gr.TabItem("Refine Animation"):
704
+ feedback = gr.Textbox(
705
+ lines=3,
706
+ placeholder="Provide feedback or suggestions for the current animation...",
707
+ label="Your Feedback"
708
+ )
709
+ refine_btn = gr.Button("Apply Feedback", variant="secondary")
710
+
711
+ # Code editor (common to both tabs)
712
+ code_output = gr.Code(
713
+ language="python",
714
+ label="Manim Code (Editable)",
715
+ lines=20,
716
+ interactive=True
717
+ )
718
+
719
+ # Add manual rerender button
720
+ rerender_btn = gr.Button("Re-render Current Code", variant="secondary")
721
+
722
+ # Right column: Video and details
723
+ with gr.Column(scale=1):
724
+ video_output = gr.Video(label="Animation")
725
+ # Uncomment the log_output component to fix the error
726
+ log_output = gr.Markdown(label="Details")
727
+
728
+ # Function to update chat history
729
+ def update_chat_history(history, user_message, bot_message, video_path):
730
+ history = history or []
731
+ history.append((user_message, None)) # User message
732
+ if video_path and not isinstance(video_path, str):
733
+ # If we have a valid video, include it in the message
734
+ bot_message = f"{bot_message}\n\n![Animation]({video_path})"
735
+ history.append((None, bot_message)) # Bot message
736
+ return history
737
+
738
+ # Function wrappers for UI updates with chat history
739
+ def generate_and_update_chat(prompt, complexity, quality, history):
740
+ code, video_path, log = generate_animation(prompt, complexity, quality)
741
+ new_history = update_chat_history(
742
+ history,
743
+ f"**Create animation:** {prompt}",
744
+ f"**Generated animation:** {memory.current_scenario.title if memory.current_scenario else 'Animation'}",
745
+ video_path
746
+ )
747
+ return code, video_path, log, new_history
748
+
749
+ def refine_and_update_chat(code, feedback_text, quality, history):
750
+ refined_code, video_path, log = refine_animation(code, feedback_text, quality)
751
+ new_history = update_chat_history(
752
+ history,
753
+ f"**Feedback:** {feedback_text}",
754
+ f"**Refined animation based on feedback**",
755
+ video_path
756
+ )
757
+ return refined_code, video_path, log, new_history
758
+
759
+ def rerender_and_update_chat(code, quality, history):
760
+ video_path, log = rerender_animation(code, quality)
761
+ new_history = update_chat_history(
762
+ history,
763
+ "**Re-rendered current code**",
764
+ "**Re-rendering complete**",
765
+ video_path
766
+ )
767
+ return video_path, log, new_history
768
+
769
+ # Connect the components to the function
770
+ generate_btn.click(
771
+ fn=generate_and_update_chat,
772
+ inputs=[new_prompt, complexity, quality, chat_history],
773
+ outputs=[code_output, video_output, log_output, chat_history]
774
+ )
775
+
776
+ refine_btn.click(
777
+ fn=refine_and_update_chat,
778
+ inputs=[code_output, feedback, quality, chat_history],
779
+ outputs=[code_output, video_output, log_output, chat_history]
780
+ )
781
+
782
+ rerender_btn.click(
783
+ fn=rerender_and_update_chat,
784
+ inputs=[code_output, quality, chat_history],
785
+ outputs=[video_output, log_output, chat_history]
786
+ )
787
+
788
+ # Add footer with social media links
789
+ with gr.Row(equal_height=True):
790
+ gr.Markdown("""
791
+ ### Connect With Us
792
+
793
+ [<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" width="30"/> GitHub](https://github.com/khanhthanhdev/Text2Video) |
794
+ [<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/05/Facebook_Logo_%282019%29.png/600px-Facebook_Logo_%282019%29.png" width="30"/> Facebook](https://facebook.com/khanhthanhdev)
795
+
796
+ ---
797
+ *Created with Manim and AI - Share your mathematical animations with the world!*
798
+ """)
799
+
800
+ demo.launch(server_name="0.0.0.0", server_port=7860)
801
+
architecture_plan.md ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AI Agent Architecture Plan
2
+
3
+ ## 1. Frameworks and Tools
4
+ - **Python**: The primary programming language.
5
+ - **Manim**: For creating mathematical animations.
6
+ - **OpenAI API**: For generating Manim code from text prompts.
7
+ - **pydantic-ai**: For structured AI agent creation, function calling, and workflow management.
8
+ - **Gradio**: For creating a user interface to input prompts and display generated videos.
9
+ - **dotenv**: For managing environment variables.
10
+ - **Logging**: For logging information and errors.
11
+
12
+ ## 2. Agent Architecture
13
+ - **Input Handling**: Use Gradio to create a user interface where users can input text prompts.
14
+ - **Agent Structure**: Leverage pydantic-ai to define the agent's schema, capabilities, and functions.
15
+ - **System Prompts**: Use both static and dynamic system prompts to guide the agent's behavior.
16
+ - Static prompts: Define the agent's role and general capabilities
17
+ - Dynamic prompts: Adjust behavior based on complexity settings and current context
18
+ - **Keyword Identification**: Use pydantic-ai with OpenAI API to identify keywords and generate Manim code.
19
+ - **Scenario Creation**: Define structured schemas in pydantic-ai to guide the generation of scenarios.
20
+ - **Function Search**: Use pydantic-ai's function calling capabilities to organize and call Manim functions.
21
+ - **Code Generation and Testing**: The generated code will be tested by rendering the video using Manim.
22
+
23
+ ## 3. Workflow
24
+ 1. **User Input**: The user inputs a text prompt describing a mathematical or physics concept.
25
+ 2. **Agent Processing**: The pydantic-ai agent processes the input through defined schemas and tools.
26
+ - System prompts dynamically adjust based on user requirements
27
+ - Tools are applied in sequence using the agent's capabilities
28
+ 3. **Keyword Identification and Scenario Creation**: The agent uses OpenAI API to analyze the prompt and generate a structured scenario.
29
+ 4. **Code Generation**: The agent transforms the structured scenario into Manim code using defined tools.
30
+ 5. **Video Rendering**: The code is executed using Manim to render the video.
31
+ 6. **Output**: The generated video is displayed to the user.
32
+
33
+ ## 4. Detailed Steps
34
+ 1. **Setup Environment**:
35
+ - Ensure all required packages are installed (`gradio`, `openai`, `pydantic-ai`, `dotenv`, `manim`, etc.).
36
+ - Set up environment variables in `.env` file (e.g., `TOGETHER_API_KEY`).
37
+ - Configure pydantic-ai with appropriate model settings.
38
+
39
+ 2. **Create Agent Structure**:
40
+ - Define pydantic models for input prompts, scenario descriptions, and animation parameters.
41
+ - Create static and dynamic system prompts to guide agent behavior:
42
+ - Static: Define the agent's role and general capabilities
43
+ - Dynamic: Adjust behavior based on request complexity and context
44
+ - Create tool functions for scenario extraction, code generation, and rendering.
45
+ - Configure the agent with appropriate tools and models.
46
+
47
+ 3. **Create User Interface**:
48
+ - Use Gradio to create a web interface for inputting prompts and displaying results.
49
+ - Add complexity selection controls to customize animation generation.
50
+ - Connect the UI to the pydantic-ai agent.
51
+
52
+ 4. **Generate Manim Code**:
53
+ - Implement functions using pydantic-ai tools to transform user prompts into structured scenarios.
54
+ - Convert structured scenarios into Manim code templates.
55
+ - Fill templates with specifics from the scenario.
56
+
57
+ 5. **Render Video**:
58
+ - Implement a function to render the generated Manim code into a video.
59
+ - Add error handling and validation using pydantic models.
60
+
61
+ 6. **Display Results**:
62
+ - Display the generated video and code in the Gradio interface.
63
+ - Provide feedback and explanations based on the agent's processing steps.
code_cleaner.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utilities for cleaning and validating Manim code generated by LLMs.
3
+ """
4
+
5
+ import re
6
+ import logging
7
+ import json
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ def clean_manim_code(raw_code):
12
+ """
13
+ Clean Manim code from LLM responses by removing markdown formatting
14
+ and ensuring proper structure.
15
+
16
+ Args:
17
+ raw_code (str): The raw code from the LLM response
18
+
19
+ Returns:
20
+ str: Cleaned, executable Python code
21
+ """
22
+ # Start with the raw code
23
+ code = raw_code
24
+
25
+ # Extract code from markdown code blocks if present
26
+ if "```python" in code:
27
+ parts = code.split("```python")
28
+ if len(parts) > 1:
29
+ code = parts[1]
30
+ if "```" in code:
31
+ code = code.split("```")[0]
32
+ elif "```" in code:
33
+ parts = code.split("```")
34
+ if len(parts) > 1:
35
+ code = parts[1]
36
+ if "```" in parts[1]:
37
+ code = code.split("```")[0]
38
+
39
+ # Remove any remaining backticks
40
+ code = code.replace('```', '')
41
+
42
+ # Ensure code begins with the necessary import
43
+ if not code.strip().startswith('from manim import'):
44
+ code = 'from manim import *\n\n' + code
45
+
46
+ # Verify the code contains a Scene class
47
+ if 'class' not in code or 'Scene' not in code:
48
+ logger.warning("Generated code does not contain a proper Scene class")
49
+ # Add a basic scene structure if missing
50
+ if 'class ManimScene(Scene):' not in code:
51
+ code = 'from manim import *\n\nclass ManimScene(Scene):\n def construct(self):\n ' + code
52
+
53
+ # Verify the code has a construct method
54
+ if 'def construct(self)' not in code:
55
+ logger.warning("Generated code does not contain a construct method")
56
+ # Try to find where the class is defined and add construct method
57
+ class_match = re.search(r'class\s+\w+\s*\(\s*Scene\s*\)\s*:', code)
58
+ if class_match:
59
+ insert_pos = class_match.end()
60
+ code = code[:insert_pos] + '\n def construct(self):\n pass\n' + code[insert_pos:]
61
+
62
+ # Ensure there's a wait at the end if not present
63
+ if 'self.wait(' not in code.split('def construct')[-1]:
64
+ # Find the end of the construct method to add wait
65
+ construct_body_match = re.search(r'def\s+construct\s*\(\s*self\s*\)\s*:', code)
66
+ if construct_body_match:
67
+ # Check if the method has content
68
+ method_content = code[construct_body_match.end():]
69
+ indentation = ' ' # Default indentation
70
+
71
+ # Try to determine indentation from code
72
+ indent_match = re.search(r'\n(\s+)', method_content)
73
+ if indent_match:
74
+ indentation = indent_match.group(1)
75
+
76
+ # Find a good place to insert the wait
77
+ if '}' in method_content.splitlines()[-1]: # If last line closes something
78
+ code = code.rstrip() + f'\n{indentation}self.wait(1)\n'
79
+ else:
80
+ code = code.rstrip() + f'\n{indentation}self.wait(1)\n'
81
+
82
+ return code.strip()
83
+
84
+ def parse_scenario_from_llm_response(content):
85
+ """
86
+ Extract structured scenario information from an LLM response.
87
+
88
+ Args:
89
+ content (str): The LLM response text
90
+
91
+ Returns:
92
+ dict: Extracted scenario dictionary
93
+ """
94
+ try:
95
+ # Try to find and extract a JSON object
96
+ json_match = re.search(r'\{.*\}', content, re.DOTALL)
97
+ if json_match:
98
+ json_str = json_match.group(0)
99
+ scenario_dict = json.loads(json_str)
100
+ return scenario_dict
101
+ except Exception as e:
102
+ logger.error(f"Error parsing scenario JSON: {e}")
103
+
104
+ # Manual parsing fallback
105
+ scenario = {
106
+ "title": "",
107
+ "objects": [],
108
+ "transformations": [],
109
+ "equations": []
110
+ }
111
+
112
+ # Simple pattern matching to extract information
113
+ title_match = re.search(r'title["\s:]+([^"]+)', content, re.IGNORECASE)
114
+ if title_match:
115
+ scenario["title"] = title_match.group(1).strip()
116
+
117
+ # Extract lists with various possible formats
118
+ objects_pattern = r'objects[":\s\[]+([^\]]+)'
119
+ objects_match = re.search(objects_pattern, content, re.IGNORECASE | re.DOTALL)
120
+ if objects_match:
121
+ objects_text = objects_match.group(1)
122
+ # Handle both comma-separated and quote-wrapped items
123
+ objects = re.findall(r'"([^"]+)"', objects_text)
124
+ if not objects:
125
+ objects = [item.strip() for item in objects_text.split(',')]
126
+ scenario["objects"] = objects
127
+
128
+ # Similar extraction for transformations
129
+ trans_pattern = r'transformations[":\s\[]+([^\]]+)'
130
+ trans_match = re.search(trans_pattern, content, re.IGNORECASE | re.DOTALL)
131
+ if trans_match:
132
+ trans_text = trans_match.group(1)
133
+ transformations = re.findall(r'"([^"]+)"', trans_text)
134
+ if not transformations:
135
+ transformations = [item.strip() for item in trans_text.split(',')]
136
+ scenario["transformations"] = transformations
137
+
138
+ # Extract equations if present
139
+ equations_pattern = r'equations[":\s\[]+([^\]]+)'
140
+ equations_match = re.search(equations_pattern, content, re.IGNORECASE | re.DOTALL)
141
+ if equations_match:
142
+ equations_text = equations_match.group(1)
143
+ if equations_text.lower().strip() in ['null', 'none']:
144
+ scenario["equations"] = None
145
+ else:
146
+ equations = re.findall(r'"([^"]+)"', equations_text)
147
+ if not equations:
148
+ equations = [item.strip() for item in equations_text.split(',')]
149
+ scenario["equations"] = equations
150
+
151
+ return scenario
152
+
153
+ def validate_manim_code(code):
154
+ """
155
+ Perform basic validation on Manim code to catch common issues.
156
+
157
+ Args:
158
+ code (str): The Manim code to validate
159
+
160
+ Returns:
161
+ tuple: (is_valid, error_message)
162
+ """
163
+ # Check for basic Python syntax errors
164
+ try:
165
+ compile(code, '<string>', 'exec')
166
+ except SyntaxError as e:
167
+ return False, f"Syntax error: {str(e)}"
168
+
169
+ # Check for necessary components
170
+ if 'from manim import' not in code:
171
+ return False, "Missing Manim import"
172
+
173
+ if 'class' not in code or 'Scene' not in code:
174
+ return False, "No Scene class defined"
175
+
176
+ if 'def construct(self)' not in code:
177
+ return False, "No construct method defined"
178
+
179
+ # Check for common Manim issues
180
+ if 'self.play(' not in code and 'self.add(' not in code:
181
+ return False, "No objects added to scene (missing self.play or self.add calls)"
182
+
183
+ # All checks passed
184
+ return True, "Code appears valid"
config.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration settings and shared utilities for the Manimation project.
3
+ """
4
+
5
+ import os
6
+ import openai
7
+ import tempfile
8
+ import subprocess
9
+ import shutil
10
+ import time
11
+ import logging
12
+ from dotenv import load_dotenv
13
+
14
+ # Configure logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Load environment variables
19
+ load_dotenv()
20
+
21
+ # Configure OpenAI client to use Together API
22
+ def get_openai_client():
23
+ """Get configured OpenAI client using Together API."""
24
+ client = openai.OpenAI(
25
+ api_key=os.environ.get("TOGETHER_API_KEY"),
26
+ base_url="https://api.together.xyz/v1",
27
+ )
28
+ return client
29
+
30
+ # Define available models
31
+ AVAILABLE_MODELS = {
32
+ "llama3": "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
33
+ "deepseek": "deepseek-ai/DeepSeek-V3",
34
+ "mixtral": "mistralai/Mixtral-8x7B-Instruct-v0.1",
35
+ }
36
+
37
+ # Default model to use
38
+ DEFAULT_MODEL = AVAILABLE_MODELS["deepseek"]
39
+
40
+ # Shared utility for rendering manim videos
41
+ def render_manim_video(code, quality="medium_quality"):
42
+ """
43
+ Render Manim code into a video file.
44
+
45
+ Args:
46
+ code (str): Manim Python code to render
47
+ quality (str): Quality level - "low_quality", "medium_quality", or "high_quality"
48
+
49
+ Returns:
50
+ str: Path to the rendered video file or error message
51
+ """
52
+ try:
53
+ temp_dir = tempfile.mkdtemp()
54
+ script_path = os.path.join(temp_dir, "manim_script.py")
55
+
56
+ with open(script_path, "w") as f:
57
+ f.write(code)
58
+
59
+ class_name = None
60
+ for line in code.split("\n"):
61
+ if line.startswith("class ") and "Scene" in line:
62
+ class_name = line.split("class ")[1].split("(")[0].strip()
63
+ break
64
+
65
+ if not class_name:
66
+ return "Error: Could not identify the Scene class in the generated code."
67
+
68
+ if quality == "high_quality":
69
+ command = ["manim", "-qh", script_path, class_name]
70
+ quality_dir = "1080p60"
71
+ elif quality == "low_quality":
72
+ command = ["manim", "-ql", script_path, class_name]
73
+ quality_dir = "480p15"
74
+ else:
75
+ command = ["manim", "-qm", script_path, class_name]
76
+ quality_dir = "720p30"
77
+
78
+ logger.info(f"Executing command: {' '.join(command)}")
79
+
80
+ result = subprocess.run(command, cwd=temp_dir, capture_output=True, text=True)
81
+
82
+ logger.info(f"Manim stdout: {result.stdout}")
83
+ logger.error(f"Manim stderr: {result.stderr}")
84
+
85
+ if result.returncode != 0:
86
+ logger.error(f"Manim execution failed: {result.stderr}")
87
+ return f"Error rendering video: {result.stderr}"
88
+
89
+ media_dir = os.path.join(temp_dir, "media")
90
+ videos_dir = os.path.join(media_dir, "videos")
91
+
92
+ if not os.path.exists(videos_dir):
93
+ return "Error: No video was generated. Check if Manim is installed correctly."
94
+
95
+ scene_dirs = [d for d in os.listdir(videos_dir) if os.path.isdir(os.path.join(videos_dir, d))]
96
+
97
+ if not scene_dirs:
98
+ return "Error: No scene directory found in the output."
99
+
100
+ scene_dir = max([os.path.join(videos_dir, d) for d in scene_dirs], key=os.path.getctime)
101
+
102
+ mp4_files = [f for f in os.listdir(os.path.join(scene_dir, quality_dir)) if f.endswith(".mp4")]
103
+
104
+ if not mp4_files:
105
+ return "Error: No MP4 file was generated."
106
+
107
+ video_file = max([os.path.join(scene_dir, quality_dir, f) for f in mp4_files], key=os.path.getctime)
108
+
109
+ output_dir = os.path.join(os.getcwd(), "generated_videos")
110
+ os.makedirs(output_dir, exist_ok=True)
111
+
112
+ timestamp = int(time.time())
113
+ output_file = os.path.join(output_dir, f"manim_video_{timestamp}.mp4")
114
+
115
+ shutil.copy2(video_file, output_file)
116
+
117
+ logger.info(f"Video generated: {output_file}")
118
+
119
+ return output_file
120
+
121
+ except Exception as e:
122
+ logger.error(f"Error rendering video: {e}")
123
+ return f"Error rendering video: {str(e)}"
124
+ finally:
125
+ if 'temp_dir' in locals():
126
+ try:
127
+ shutil.rmtree(temp_dir)
128
+ except Exception as e:
129
+ logger.error(f"Error cleaning up temporary directory: {e}")
main.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import tempfile
4
+ import subprocess
5
+ import shutil
6
+ import logging
7
+ import time
8
+ from openai import OpenAI
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ def get_client():
17
+ return OpenAI(
18
+ api_key=os.environ.get("TOGETHER_API_KEY"),
19
+ base_url="https://api.together.xyz/v1"
20
+ )
21
+
22
+ AVAILABLE_MODELS = [
23
+ "meta-llama/Llama-3.3-70B-Instruct-Turbo",
24
+ "deepseek-ai/DeepSeek-V3",
25
+ "deepseek-ai/DeepSeek-R1",
26
+ "Qwen/QwQ-32B-Preview",
27
+ "meta-llama/Llama-3.3-70B-Instruct-Turbo-Free",
28
+ "Qwen/Qwen2.5-Coder-32B-Instruct"
29
+ ]
30
+
31
+ def generate_manim_code(prompt, model_name, temperature=0.7, max_tokens=8192):
32
+ try:
33
+ client = get_client()
34
+ system_prompt = """
35
+ You are an expert in creating mathematical and physics visualizations using Manim (Mathematical Animation Engine).
36
+ Your task is to convert a text prompt into valid, executable Manim Python code.
37
+
38
+ IMPORTANT RULES FOR COMPILATION SUCCESS:
39
+ 1. Only return valid Python code that works with the latest version of Manim Community edition
40
+ 2. Do NOT include any explanations outside of code comments
41
+ 3. Use ONLY the Scene class as the base class
42
+ 4. Include ALL necessary imports at the top (from manim import *)
43
+ 5. Use descriptive variable names that follow Python conventions
44
+ 6. Include helpful comments for complex parts of the visualization
45
+ 7. The class name MUST be "Screen" - always use this exact name
46
+ 8. Always implement the construct method correctly
47
+ 9. Ensure all objects are properly added to the scene with self.play() or self.add()
48
+ 10. Do not create custom classes other than the main Scene class
49
+ 11. Include proper self.wait() calls after animations for better viewing
50
+ 12. Check all mathematical expressions are valid LaTeX syntax
51
+ 13. Avoid advanced or experimental Manim features that might not be widely available
52
+ 14. Keep animations under 20 seconds total for better performance
53
+ 15. Ensure all coordinates and dimensions are appropriate for the default canvas size
54
+ 16. DO NOT include any backticks (```) or markdown formatting in your response
55
+
56
+ RESPOND WITH ONLY THE EXECUTABLE PYTHON CODE, NO INTRODUCTION OR EXPLANATION, NO MARKDOWN FORMATTING.
57
+ """
58
+
59
+ final_prompt = f"Create a Manim visualization that explains: {prompt}"
60
+
61
+ logger.info(f"Generating code with model: {model_name}")
62
+
63
+ response = client.chat.completions.create(
64
+ model=model_name,
65
+ temperature=temperature,
66
+ max_tokens=max_tokens,
67
+ messages=[
68
+ {"role": "system", "content": system_prompt},
69
+ {"role": "user", "content": final_prompt}
70
+ ]
71
+ )
72
+
73
+ generated_code = response.choices[0].message.content
74
+
75
+ # Strip markdown formatting if it appears in the response
76
+ if "```python" in generated_code:
77
+ generated_code = generated_code.split("```python")[1]
78
+ if "```" in generated_code:
79
+ generated_code = generated_code.split("```")[0]
80
+ elif "```" in generated_code:
81
+ generated_code = generated_code.split("```")[1]
82
+ if "```" in generated_code:
83
+ generated_code = generated_code.split("```")[0]
84
+
85
+ # Remove any additional backticks that might cause syntax errors
86
+ generated_code = generated_code.replace('```', '')
87
+
88
+ # Ensure code starts with proper import
89
+ if not generated_code.strip().startswith('from manim import'):
90
+ generated_code = 'from manim import *\n\n' + generated_code
91
+
92
+ return generated_code.strip()
93
+
94
+ except Exception as e:
95
+ logger.error(f"Error generating code: {e}")
96
+ return f"Error generating code: {str(e)}"
97
+
98
+ def render_manim_video(code, quality="medium_quality"):
99
+ try:
100
+ temp_dir = tempfile.mkdtemp()
101
+ script_path = os.path.join(temp_dir, "manim_script.py")
102
+
103
+ with open(script_path, "w") as f:
104
+ f.write(code)
105
+
106
+ class_name = None
107
+ for line in code.split("\n"):
108
+ if line.startswith("class ") and "Scene" in line:
109
+ class_name = line.split("class ")[1].split("(")[0].strip()
110
+ break
111
+
112
+ if not class_name:
113
+ return "Error: Could not identify the Scene class in the generated code."
114
+
115
+ if quality == "high_quality":
116
+ command = ["manim", "-qh", script_path, class_name]
117
+ quality_dir = "1080p60"
118
+ elif quality == "low_quality":
119
+ command = ["manim", "-ql", script_path, class_name]
120
+ quality_dir = "480p15"
121
+ else:
122
+ command = ["manim", "-qm", script_path, class_name]
123
+ quality_dir = "720p30"
124
+
125
+ logger.info(f"Executing command: {' '.join(command)}")
126
+
127
+ result = subprocess.run(command, cwd=temp_dir, capture_output=True, text=True)
128
+
129
+ logger.info(f"Manim stdout: {result.stdout}")
130
+ logger.error(f"Manim stderr: {result.stderr}")
131
+
132
+ if result.returncode != 0:
133
+ logger.error(f"Manim execution failed: {result.stderr}")
134
+ return f"Error rendering video: {result.stderr}"
135
+
136
+ media_dir = os.path.join(temp_dir, "media")
137
+ videos_dir = os.path.join(media_dir, "videos")
138
+
139
+ if not os.path.exists(videos_dir):
140
+ return "Error: No video was generated. Check if Manim is installed correctly."
141
+
142
+ scene_dirs = [d for d in os.listdir(videos_dir) if os.path.isdir(os.path.join(videos_dir, d))]
143
+
144
+ if not scene_dirs:
145
+ return "Error: No scene directory found in the output."
146
+
147
+ scene_dir = max([os.path.join(videos_dir, d) for d in scene_dirs], key=os.path.getctime)
148
+
149
+ mp4_files = [f for f in os.listdir(os.path.join(scene_dir, quality_dir)) if f.endswith(".mp4")]
150
+
151
+ if not mp4_files:
152
+ return "Error: No MP4 file was generated."
153
+
154
+ video_file = max([os.path.join(scene_dir, quality_dir, f) for f in mp4_files], key=os.path.getctime)
155
+
156
+ output_dir = os.path.join(os.getcwd(), "generated_videos")
157
+ os.makedirs(output_dir, exist_ok=True)
158
+
159
+ timestamp = int(time.time())
160
+ output_file = os.path.join(output_dir, f"manim_video_{timestamp}.mp4")
161
+
162
+ shutil.copy2(video_file, output_file)
163
+
164
+ logger.info(f"Video generated: {output_file}")
165
+
166
+ return output_file
167
+
168
+ except Exception as e:
169
+ logger.error(f"Error rendering video: {e}")
170
+ return f"Error rendering video: {str(e)}"
171
+ finally:
172
+ if 'temp_dir' in locals():
173
+ try:
174
+ shutil.rmtree(temp_dir)
175
+ except Exception as e:
176
+ logger.error(f"Error cleaning up temporary directory: {e}")
177
+
178
+ def placeholder_for_examples(prompt, model, quality):
179
+ code = """
180
+ from manim import *
181
+
182
+ class PythagoreanTheorem(Scene):
183
+ def construct(self):
184
+ # This is placeholder code for examples
185
+ # Creating a right triangle
186
+ triangle = Polygon(
187
+ ORIGIN,
188
+ RIGHT * 3,
189
+ UP * 4,
190
+ color=WHITE
191
+ )
192
+
193
+ # Adding labels
194
+ a = Text("a", font_size=30).next_to(triangle, DOWN)
195
+ b = Text("b", font_size=30).next_to(triangle, RIGHT)
196
+ c = Text("c", font_size=30).next_to(
197
+ triangle.get_center(),
198
+ UP + LEFT
199
+ )
200
+
201
+ # Add to scene
202
+ self.play(Create(triangle))
203
+ self.play(Write(a), Write(b), Write(c))
204
+
205
+ # Wait at the end
206
+ self.wait(2)
207
+ """
208
+ return code, None, "Example mode: Click 'Generate Video' to actually process this example"
209
+
210
+ def process_prompt(prompt, model_name, quality="medium_quality"):
211
+ try:
212
+ code = generate_manim_code(prompt, model_name)
213
+ video_path = render_manim_video(code, quality)
214
+ return code, video_path
215
+ except Exception as e:
216
+ logger.error(f"Error processing prompt: {e}")
217
+ return f"Error: {str(e)}", None
218
+
219
+ def process_prompt_with_status(prompt, model, quality, progress=gr.Progress()):
220
+ try:
221
+ progress(0, desc="Starting...")
222
+
223
+ progress(0.3, desc="Generating Manim code using AI...")
224
+ code = generate_manim_code(prompt, model)
225
+
226
+ progress(0.6, desc="Rendering video with Manim (this may take a few minutes)...")
227
+ video_path = render_manim_video(code, quality)
228
+
229
+ progress(1.0, desc="Complete")
230
+
231
+ if not video_path or video_path.startswith("Error"):
232
+ status = video_path if video_path else "Error: Failed to generate video."
233
+ return code, None, status
234
+ else:
235
+ status = "Video generated successfully!"
236
+ return code, video_path, status
237
+
238
+ except Exception as e:
239
+ logger.error(f"Error in processing: {e}")
240
+ return (code if 'code' in locals() else "Error generating code"), None, f"Error: {str(e)}"
241
+
242
+ def create_interface():
243
+ with gr.Blocks(title="Math & Physics Video Generator") as app:
244
+ gr.Markdown("# Interactive Math & Physics Video Generator")
245
+ gr.Markdown("Generate educational videos from text prompts using AI and Manim")
246
+
247
+ with gr.Row():
248
+ with gr.Column():
249
+ model_dropdown = gr.Dropdown(
250
+ choices=AVAILABLE_MODELS,
251
+ value=AVAILABLE_MODELS[1],
252
+ label="Select AI Model"
253
+ )
254
+ quality_radio = gr.Radio(
255
+ choices=["low_quality", "medium_quality", "high_quality"],
256
+ value="medium_quality",
257
+ label="Output Quality (affects rendering time)"
258
+ )
259
+ prompt_input = gr.Textbox(
260
+ placeholder="Enter a mathematical or physics concept to visualize...",
261
+ label="Prompt",
262
+ lines=3
263
+ )
264
+ submit_btn = gr.Button("Generate Video", variant="primary")
265
+
266
+ with gr.Accordion("Generated Manim Code", open=False):
267
+ code_output = gr.Code(
268
+ language="python",
269
+ label="Generated Manim Code",
270
+ lines=20
271
+ )
272
+
273
+ with gr.Column():
274
+ video_output = gr.Video(
275
+ label="Generated Animation",
276
+ width="100%",
277
+ height=500
278
+ )
279
+ status_output = gr.Textbox(
280
+ label="Status",
281
+ value="Ready. Enter a prompt and click 'Generate Video'.",
282
+ interactive=False
283
+ )
284
+
285
+ submit_btn.click(
286
+ fn=process_prompt_with_status,
287
+ inputs=[prompt_input, model_dropdown, quality_radio],
288
+ outputs=[code_output, video_output, status_output]
289
+ )
290
+
291
+ gr.Examples(
292
+ examples=[
293
+ ["Explain the Pythagorean theorem", AVAILABLE_MODELS[1], "medium_quality"],
294
+ ["Show how a pendulum works with damping", AVAILABLE_MODELS[1], "medium_quality"],
295
+ ["Demonstrate the concept of derivatives in calculus", AVAILABLE_MODELS[1], "medium_quality"],
296
+ ["Visualize the wave function of a particle in a box", AVAILABLE_MODELS[1], "medium_quality"],
297
+ ["Explain how a capacitor charges and discharges", AVAILABLE_MODELS[1], "medium_quality"]
298
+ ],
299
+ inputs=[prompt_input, model_dropdown, quality_radio],
300
+ fn=placeholder_for_examples
301
+ )
302
+
303
+ return app
304
+
305
+ if __name__ == "__main__":
306
+ app = create_interface()
307
+ app.launch(share=True)
manim_prompts.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Optimized prompts for generating Manim code using LLMs.
3
+ """
4
+
5
+ # Basic prompt for Manim code generation
6
+ MANIM_CODE_SYSTEM_PROMPT = """
7
+ You are an expert in creating mathematical and physics visualizations using Manim (Mathematical Animation Engine).
8
+ Your task is to convert a text prompt into valid, executable Manim Python code.
9
+
10
+ IMPORTANT RULES FOR COMPILATION SUCCESS:
11
+ 1. Only return valid Python code that works with the latest version of Manim Community edition
12
+ 2. Do NOT include any explanations outside of code comments
13
+ 3. Use ONLY the Scene class as the base class
14
+ 4. Include ALL necessary imports at the top (from manim import *)
15
+ 5. Use descriptive variable names that follow Python conventions
16
+ 6. Include helpful comments for complex parts of the visualization
17
+ 7. The class name MUST be "ManimScene" - always use this exact name
18
+ 8. Always implement the construct method correctly
19
+ 9. Ensure all objects are properly added to the scene with self.play() or self.add()
20
+ 10. Do not create custom classes other than the main Scene class
21
+ 11. Include proper self.wait() calls after animations for better viewing
22
+ 12. Check all mathematical expressions are valid LaTeX syntax
23
+ 13. Avoid advanced or experimental Manim features that might not be widely available
24
+ 14. Keep animations under 20 seconds total for better performance
25
+ 15. Ensure all coordinates and dimensions are appropriate for the default canvas size
26
+
27
+ REQUIRED CODE FORMAT:
28
+ ```python
29
+ from manim import *
30
+
31
+ class ManimScene(Scene):
32
+ def construct(self):
33
+ # Your animation code here
34
+ # ...
35
+ # Final wait
36
+ self.wait(1)
37
+ ```
38
+
39
+ RESPOND WITH ONLY THE EXECUTABLE PYTHON CODE, NO INTRODUCTION OR EXPLANATION.
40
+ """
41
+
42
+ # Simple complexity prompt adjustment
43
+ SIMPLE_COMPLEXITY_PROMPT = """
44
+ Create simple, beginner-friendly Manim code with minimal elements. Focus on:
45
+ - Basic shapes and transformations
46
+ - Clear, readable labels
47
+ - Simple animations with few elements
48
+ - Step-by-step visualization of the concept
49
+ - No more than 2-3 different objects on screen
50
+ - Linear progression of concepts
51
+ """
52
+
53
+ # Medium complexity prompt adjustment
54
+ MEDIUM_COMPLEXITY_PROMPT = """
55
+ Create balanced Manim code that is both clear and somewhat detailed. Include:
56
+ - Multiple related shapes and transformations
57
+ - Clear mathematical labeling
58
+ - Moderate level of animation complexity
59
+ - Both visualization and mathematical notation
60
+ - Appropriate use of color and positioning
61
+ - A logical flow that builds understanding
62
+ """
63
+
64
+ # Complex complexity prompt adjustment
65
+ COMPLEX_COMPLEXITY_PROMPT = """
66
+ Create sophisticated Manim animations with detailed mathematical elements. Include:
67
+ - Multiple related mathematical objects and their interactions
68
+ - Precise mathematical notation and labeling
69
+ - Advanced transformations and animations
70
+ - Detailed visualization of the mathematical concept
71
+ - Professional use of color, positioning and timing
72
+ - Build from simple to complex understanding
73
+ """
74
+
75
+ def get_manim_prompt(complexity="medium"):
76
+ """Get the appropriate Manim prompt based on complexity level."""
77
+
78
+ base_prompt = MANIM_CODE_SYSTEM_PROMPT
79
+
80
+ if complexity == "simple":
81
+ return base_prompt + "\n\n" + SIMPLE_COMPLEXITY_PROMPT
82
+ elif complexity == "complex":
83
+ return base_prompt + "\n\n" + COMPLEX_COMPLEXITY_PROMPT
84
+ else: # medium is default
85
+ return base_prompt + "\n\n" + MEDIUM_COMPLEXITY_PROMPT
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio
2
+ openai
3
+ python-dotenv
4
+ manim
5
+ pydantic
6
+ pydantic-ai
simple_manim_agent.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simplified example of a Manim animation generator using pydantic-ai.
3
+ """
4
+
5
+ import os
6
+ from typing import List, Optional
7
+ from dotenv import load_dotenv
8
+ from pydantic_ai.models.openai import OpenAIModel
9
+ from pydantic_ai.providers.openai import OpenAIProvider
10
+ from pydantic_ai import Agent, RunContext
11
+ from pydantic import BaseModel, Field
12
+ from datetime import datetime
13
+ import openai
14
+ import tempfile
15
+ import subprocess
16
+ import shutil
17
+ import time
18
+ import logging
19
+
20
+ # Load environment variables
21
+ load_dotenv()
22
+
23
+ # Configure logging if not already done
24
+ logging.basicConfig(level=logging.INFO)
25
+ logger = logging.getLogger(__name__)
26
+
27
+ class AnimationPrompt(BaseModel):
28
+ """User input for animation generation."""
29
+ description: str = Field(..., description="Description of the mathematical concept to animate")
30
+ complexity: str = Field("medium", description="Desired complexity of the animation (simple, medium, complex)")
31
+
32
+ class AnimationOutput(BaseModel):
33
+ """Output of the animation generation."""
34
+ manim_code: str = Field(..., description="Generated Manim code")
35
+ explanation: str = Field(..., description="Explanation of the animation")
36
+
37
+ # Create the animation agent with basic static system prompt
38
+ model = OpenAIModel(
39
+ 'deepseek-ai/DeepSeek-V3',
40
+ provider=OpenAIProvider(
41
+ base_url='https://api.together.xyz/v1', api_key=os.environ.get('TOGETHER_API_KEY')
42
+ ),
43
+ )
44
+
45
+ animation_agent = Agent(
46
+ model,
47
+ deps_type=AnimationPrompt,
48
+ system_prompt=(
49
+ "You are a mathematical animation specialist. Your job is to convert text descriptions "
50
+ "into Manim code that visualizes mathematical concepts. Provide clear and accurate code."
51
+ )
52
+ )
53
+
54
+ # Configure OpenAI client to use Together API
55
+ client = openai.OpenAI(
56
+ api_key=os.environ.get("TOGETHER_API_KEY"),
57
+ base_url="https://api.together.xyz/v1",
58
+ )
59
+
60
+ # Add dynamic system prompts
61
+ @animation_agent.system_prompt
62
+ def add_complexity_guidance(ctx: RunContext[AnimationPrompt]) -> str:
63
+ """Add guidance based on requested complexity."""
64
+ complexity = ctx.deps.complexity
65
+ if complexity == "simple":
66
+ return "Generate simple, beginner-friendly Manim code with minimal elements and clear explanations."
67
+ elif complexity == "complex":
68
+ return "Generate advanced Manim code with sophisticated animations and detailed mathematical representations."
69
+ else: # medium
70
+ return "Generate standard Manim code that balances simplicity and detail to effectively demonstrate the concept."
71
+
72
+ @animation_agent.system_prompt
73
+ def add_timestamp() -> str:
74
+ """Add a timestamp to help with freshness of information."""
75
+ return f"Current timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
76
+
77
+ @animation_agent.tool
78
+ def generate_manim_code(ctx: RunContext[AnimationPrompt]) -> str:
79
+ """Generate Manim code based on the user's description."""
80
+ prompt = ctx.deps
81
+
82
+ # Use Together API with OpenAI client
83
+ response = client.chat.completions.create(
84
+ model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
85
+ messages=[
86
+ {"role": "system", "content": """
87
+ Generate Manim code for mathematical animations. The code MUST:
88
+ 1. Be fully compilable without errors using Manim Community edition
89
+ 2. Use only the Scene class with a class name 'ManimScene' exactly
90
+ 3. Include 'from manim import *' at the top
91
+ 4. Implement the construct method only
92
+ 5. Use only standard Manim objects and methods
93
+ 6. Include proper self.play() and self.wait() calls
94
+ 7. Use valid LaTeX syntax for any mathematical expressions
95
+ 8. Avoid experimental or uncommon Manim features
96
+ 9. Keep the animation clean, concise, and educational
97
+ 10. Include proper error handling for all mathematical operations
98
+ 11. DO NOT include any backticks (```) or markdown formatting in your response
99
+
100
+ RESPOND WITH CODE ONLY, NO EXPLANATIONS OUTSIDE OF CODE COMMENTS, NO MARKDOWN FORMATTING.
101
+ """
102
+ },
103
+ {"role": "user", "content": f"Create Manim code for a {prompt.complexity} animation of {prompt.description}"}
104
+ ]
105
+ )
106
+
107
+ generated_code = response.choices[0].message.content
108
+
109
+ # Strip markdown formatting if it appears in the response
110
+ if "```python" in generated_code:
111
+ generated_code = generated_code.split("```python")[1]
112
+ if "```" in generated_code:
113
+ generated_code = generated_code.split("```")[0]
114
+
115
+ return generated_code
116
+
117
+ @animation_agent.tool
118
+ def explain_animation(ctx: RunContext[AnimationPrompt], code: str) -> str:
119
+ """Explain the generated animation in plain language."""
120
+ prompt = ctx.deps
121
+
122
+ # Use Together API with OpenAI client
123
+ response = client.chat.completions.create(
124
+ model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
125
+ messages=[
126
+ {"role": "system", "content": "Explain mathematical animations in simple terms."},
127
+ {"role": "user", "content": f"Explain this Manim animation of {prompt.description} " +
128
+ f"with complexity {prompt.complexity} in simple terms:\n{code}"}
129
+ ]
130
+ )
131
+
132
+ return response.choices[0].message.content
133
+
134
+ def render_manim_video(code, quality="medium_quality"):
135
+ try:
136
+ temp_dir = tempfile.mkdtemp()
137
+ script_path = os.path.join(temp_dir, "manim_script.py")
138
+
139
+ with open(script_path, "w") as f:
140
+ f.write(code)
141
+
142
+ class_name = None
143
+ for line in code.split("\n"):
144
+ if line.startswith("class ") and "Scene" in line:
145
+ class_name = line.split("class ")[1].split("(")[0].strip()
146
+ break
147
+
148
+ if not class_name:
149
+ return "Error: Could not identify the Scene class in the generated code."
150
+
151
+ if quality == "high_quality":
152
+ command = ["manim", "-qh", script_path, class_name]
153
+ quality_dir = "1080p60"
154
+ elif quality == "low_quality":
155
+ command = ["manim", "-ql", script_path, class_name]
156
+ quality_dir = "480p15"
157
+ else:
158
+ command = ["manim", "-qm", script_path, class_name]
159
+ quality_dir = "720p30"
160
+
161
+ logger.info(f"Executing command: {' '.join(command)}")
162
+
163
+ result = subprocess.run(command, cwd=temp_dir, capture_output=True, text=True)
164
+
165
+ logger.info(f"Manim stdout: {result.stdout}")
166
+ logger.error(f"Manim stderr: {result.stderr}")
167
+
168
+ if result.returncode != 0:
169
+ logger.error(f"Manim execution failed: {result.stderr}")
170
+ return f"Error rendering video: {result.stderr}"
171
+
172
+ media_dir = os.path.join(temp_dir, "media")
173
+ videos_dir = os.path.join(media_dir, "videos")
174
+
175
+ if not os.path.exists(videos_dir):
176
+ return "Error: No video was generated. Check if Manim is installed correctly."
177
+
178
+ scene_dirs = [d for d in os.listdir(videos_dir) if os.path.isdir(os.path.join(videos_dir, d))]
179
+
180
+ if not scene_dirs:
181
+ return "Error: No scene directory found in the output."
182
+
183
+ scene_dir = max([os.path.join(videos_dir, d) for d in scene_dirs], key=os.path.getctime)
184
+
185
+ mp4_files = [f for f in os.listdir(os.path.join(scene_dir, quality_dir)) if f.endswith(".mp4")]
186
+
187
+ if not mp4_files:
188
+ return "Error: No MP4 file was generated."
189
+
190
+ video_file = max([os.path.join(scene_dir, quality_dir, f) for f in mp4_files], key=os.path.getctime)
191
+
192
+ output_dir = os.path.join(os.getcwd(), "generated_videos")
193
+ os.makedirs(output_dir, exist_ok=True)
194
+
195
+ timestamp = int(time.time())
196
+ output_file = os.path.join(output_dir, f"manim_video_{timestamp}.mp4")
197
+
198
+ shutil.copy2(video_file, output_file)
199
+
200
+ logger.info(f"Video generated: {output_file}")
201
+
202
+ return output_file
203
+
204
+ except Exception as e:
205
+ logger.error(f"Error rendering video: {e}")
206
+ return f"Error rendering video: {str(e)}"
207
+ finally:
208
+ if 'temp_dir' in locals():
209
+ try:
210
+ shutil.rmtree(temp_dir)
211
+ except Exception as e:
212
+ logger.error(f"Error cleaning up temporary directory: {e}")
213
+
214
+ def run_animation_agent(description: str, complexity: str = "medium", quality: str = "medium_quality") -> AnimationOutput:
215
+ """Run the animation agent to generate code and explanation."""
216
+ prompt = AnimationPrompt(description=description, complexity=complexity)
217
+
218
+ # Use the agent to process the request
219
+ result = animation_agent.run_sync(
220
+ "Generate Manim code for this animation and explain what it does",
221
+ deps=prompt
222
+ )
223
+
224
+ # Generate code and explanation
225
+ code = None
226
+ explanation = None
227
+
228
+ # As a fallback, provide a direct implementation specific to the Pythagorean theorem
229
+ if "pythagorean theorem" in description.lower():
230
+ code = f"""
231
+ from manim import *
232
+
233
+ class ManimScene(Scene):
234
+ def construct(self):
235
+ # Animation for: {prompt.description}
236
+ # Complexity level: {prompt.complexity}
237
+
238
+ # Create a right triangle
239
+ triangle = Polygon(
240
+ ORIGIN,
241
+ RIGHT * 3,
242
+ UP * 4,
243
+ color=WHITE
244
+ )
245
+
246
+ # Labels for sides
247
+ a_label = MathTex("a").next_to(triangle, DOWN)
248
+ b_label = MathTex("b").next_to(triangle, RIGHT)
249
+ c_label = MathTex("c").next_to(triangle.get_center(), UP + LEFT)
250
+
251
+ # The equation
252
+ equation = MathTex("a^2 + b^2 = c^2").to_edge(DOWN)
253
+
254
+ # Display the triangle and labels
255
+ self.play(Create(triangle))
256
+ self.play(Write(a_label), Write(b_label), Write(c_label))
257
+ self.wait()
258
+
259
+ # Show the equation
260
+ self.play(Write(equation))
261
+ self.wait()
262
+ """
263
+
264
+ explanation = (
265
+ f"This animation visualizes {prompt.description} with a {prompt.complexity} "
266
+ f"complexity level. It creates a right triangle and labels its sides a, b, and c. "
267
+ f"It then displays the Pythagorean theorem equation a² + b² = c²."
268
+ )
269
+ else:
270
+ # Generic fallback
271
+ code = f"""
272
+ from manim import *
273
+
274
+ class ManimScene(Scene):
275
+ def construct(self):
276
+ # Animation for: {prompt.description}
277
+ # Complexity level: {prompt.complexity}
278
+
279
+ # Title
280
+ title = Text("{description}")
281
+ self.play(Write(title))
282
+ self.wait()
283
+ self.play(title.animate.to_edge(UP))
284
+
285
+ # Main content based on complexity
286
+ if "{complexity}" == "simple":
287
+ # Simple visualization
288
+ circle = Circle()
289
+ self.play(Create(circle))
290
+ self.wait()
291
+ else:
292
+ # More complex visualization
293
+ axes = Axes(
294
+ x_range=[-3, 3],
295
+ y_range=[-3, 3],
296
+ axis_config={"color": BLUE}
297
+ )
298
+ self.play(Create(axes))
299
+
300
+ # Add a function graph
301
+ graph = axes.plot(lambda x: x**2, color=YELLOW)
302
+ self.play(Create(graph))
303
+ self.wait()
304
+ """
305
+
306
+ explanation = (
307
+ f"This animation visualizes {prompt.description} with a {prompt.complexity} "
308
+ f"complexity level. It displays a title and creates a visualization that matches "
309
+ f"the requested complexity."
310
+ )
311
+
312
+ # Try to render the video
313
+ if code:
314
+ video_path = render_manim_video(code, quality)
315
+ if video_path and not video_path.startswith("Error"):
316
+ print(f"Video rendered successfully at: {video_path}")
317
+
318
+ return AnimationOutput(manim_code=code, explanation=explanation)
319
+
320
+ if __name__ == "__main__":
321
+ # Example usage
322
+ result = run_animation_agent(
323
+ "the Pythagorean theorem showing how a² + b² = c²",
324
+ complexity="simple"
325
+ )
326
+ print("=== Generated Manim Code ===")
327
+ print(result.manim_code)
328
+ print("\n=== Explanation ===")
329
+ print(result.explanation)