raksa-the-wildcats commited on
Commit
c528fe0
Β·
1 Parent(s): fc72a6f

Third commit

Browse files
Files changed (1) hide show
  1. app.py +221 -182
app.py CHANGED
@@ -9,17 +9,22 @@ from pathlib import Path
9
  import time
10
  import threading
11
  import queue
 
 
 
 
 
12
 
13
- class ManimMCPServer:
14
  def __init__(self):
15
  self.temp_dir = tempfile.mkdtemp()
16
- self.max_execution_time = 60 # 60 seconds timeout
17
 
18
- def validate_manim_code(self, code):
19
- """ Basic validation of Manim code """
20
  dangerous_imports = [
21
  'subprocess', 'os.system', 'eval', 'exec', 'open',
22
- '__import__', 'compile', 'globals', 'locals'
23
  ]
24
 
25
  for dangerous in dangerous_imports:
@@ -27,115 +32,66 @@ class ManimMCPServer:
27
  return False, f"Dangerous operation detected: {dangerous}"
28
 
29
  # Check if it's a valid Python-like structure
30
- if 'from manim import *' not in code and 'import manim' not in code:
31
- return False, "Code must import manim"
32
 
33
- if 'class' not in code or 'Scene' not in code:
34
- return False, "Code must contain a Scene class"
35
 
36
  return True, "Valid"
37
 
38
- def execute_with_timeout(self, cmd, timeout):
39
- """ Execute command with timeout """
40
- result_queue = queue.Queue()
41
-
42
- def run_command():
43
- try:
44
- result = subprocess.run(
45
- cmd,
46
- shell=True,
47
- capture_output=True,
48
- text=True,
49
- cwd=self.temp_dir
50
- )
51
- result_queue.put(('success', result))
52
- except Exception as e:
53
- result_queue.put(('error', str(e)))
54
-
55
- thread = threading.Thread(target=run_command)
56
- thread.daemon = True
57
- thread.start()
58
- thread.join(timeout)
59
-
60
- if thread.is_alive():
61
- return False, "Execution timed out"
62
-
63
- if result_queue.empty():
64
- return False, "Execution failed"
65
-
66
- status, result = result_queue.get()
67
- if status == 'error':
68
- return False, result
69
-
70
- return True, result
71
-
72
- def render_manim_animation(self, code, scene_name=None, quality="medium_quality"):
73
- """ Render Manim animation and return video path """
74
  try:
75
  # Validate code
76
- is_valid, message = self.validate_manim_code(code)
77
  if not is_valid:
78
  return None, f"Code validation failed: {message}"
79
 
80
- # Create temporary Python file
81
- script_path = os.path.join(self.temp_dir, "animation.py")
82
- with open(script_path, 'w') as f:
83
- f.write(code)
84
-
85
- # Auto-detect scene name if not provided
86
- if not scene_name:
87
- lines = code.split('\n')
88
- for line in lines:
89
- if line.strip().startswith('class ') and 'Scene' in line:
90
- scene_name = line.split('class ')[1].split('(')[0].strip()
91
- break
92
-
93
- if not scene_name:
94
- return None, "Could not detect scene name. Please specify scene_name parameter."
95
-
96
- # Quality settings
97
- quality_map = {
98
- "low_quality": "-ql",
99
- "medium_quality": "-qm",
100
- "high_quality": "-qh",
101
- "production_quality": "-qp"
 
 
 
102
  }
103
 
104
- quality_flag = quality_map.get(quality, "-qm")
105
-
106
- # Render animation
107
- cmd = f"manim {quality_flag} animation.py {scene_name}"
108
-
109
- success, result = self.execute_with_timeout(cmd, self.max_execution_time)
110
-
111
- if not success:
112
- return None, f"Rendering failed: {result}"
113
 
114
- if result.returncode != 0:
115
- return None, f"Manim execution failed:\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}"
116
 
117
- # Find generated video file
118
- media_dir = os.path.join(self.temp_dir, "media", "videos", "animation")
119
 
120
- if not os.path.exists(media_dir):
121
- return None, "No output directory found. Animation may not have been generated."
122
-
123
- # Look for MP4 files
124
- video_files = []
125
- for root, dirs, files in os.walk(media_dir):
126
- for file in files:
127
- if file.endswith('.mp4'):
128
- video_files.append(os.path.join(root, file))
129
-
130
- if not video_files:
131
- return None, "No MP4 files generated. Check your scene code."
132
 
133
- # Return the most recent video file
134
- latest_video = max(video_files, key=os.path.getctime)
135
- return latest_video, "Animation rendered successfully!"
136
-
137
  except Exception as e:
138
- return None, f"Unexpected error: {str(e)}\n{traceback.format_exc()}"
139
 
140
  def cleanup(self):
141
  """Clean up temporary files"""
@@ -145,58 +101,125 @@ class ManimMCPServer:
145
  pass
146
 
147
  # Initialize the server
148
- mcp_server = ManimMCPServer()
149
 
150
- # Example Manim scripts
151
  EXAMPLE_SCRIPTS = {
152
- "Hello World": '''from manim import *
 
 
153
 
154
- class HelloWorld(Scene):
155
- def construct(self):
156
- text = Text("Hello, World!", font_size=72)
157
- text.set_color(BLUE)
158
- self.play(Write(text))
159
- self.wait(2)
160
- self.play(FadeOut(text))''',
161
-
162
- "Simple Animation": '''from manim import *
163
 
164
- class SimpleAnimation(Scene):
165
- def construct(self):
166
- circle = Circle()
167
- circle.set_fill(PINK, opacity=0.5)
168
-
169
- square = Square()
170
- square.set_fill(BLUE, opacity=0.5)
171
-
172
- self.play(Create(circle))
173
- self.play(Transform(circle, square))
174
- self.play(Rotate(square, PI/2))
175
- self.wait()''',
176
-
177
- "Mathematical Formula": '''from manim import *
178
 
179
- class MathFormula(Scene):
180
- def construct(self):
181
- formula = MathTex(r"\\frac{d}{dx}\\left(\\frac{1}{x}\\right) = -\\frac{1}{x^2}")
182
- formula.scale(2)
183
- formula.set_color(YELLOW)
184
-
185
- self.play(Write(formula))
186
- self.wait(2)
187
-
188
- box = SurroundingRectangle(formula, color=WHITE)
189
- self.play(Create(box))
190
- self.wait()'''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  }
192
 
193
- def render_animation(code, scene_name, quality):
194
- """ Gradio interface function """
195
  if not code.strip():
196
- return None, "Please provide Manim code"
197
 
198
  try:
199
- video_path, message = mcp_server.render_manim_animation(code, scene_name, quality)
200
 
201
  if video_path:
202
  return video_path, f"βœ… {message}"
@@ -207,31 +230,52 @@ def render_animation(code, scene_name, quality):
207
  return None, f"❌ Error: {str(e)}"
208
 
209
  def load_example(example_name):
210
- """ Load example script """
211
  return EXAMPLE_SCRIPTS.get(example_name, "")
212
 
213
  def clear_all():
214
  """Clear all fields"""
215
- return "", "", None, ""
 
 
 
 
 
 
 
216
 
217
  # Create Gradio interface
218
- with gr.Blocks(title="Manim MCP Server", theme=gr.themes.Soft()) as demo:
219
- gr.Markdown("""
220
- # 🎬 Manim MCP Server
221
-
222
- A Gradio-based MCP-like server for rendering Manim Community Edition animations.
223
- Write your Manim code, specify the scene name, and get an MP4 video output!
 
 
 
 
 
 
 
 
 
224
 
 
225
  ## How to use:
226
- 1. Write or select a Manim script
227
- 2. Specify the scene class name
228
- 3. Choose quality settings
229
- 4. Click "Render Animation"
 
 
 
 
230
  """)
231
 
232
  with gr.Row():
233
  with gr.Column(scale=2):
234
- gr.Markdown("### πŸ“ Manim Code")
235
 
236
  with gr.Row():
237
  example_dropdown = gr.Dropdown(
@@ -242,32 +286,26 @@ with gr.Blocks(title="Manim MCP Server", theme=gr.themes.Soft()) as demo:
242
  clear_btn = gr.Button("Clear All", size="sm")
243
 
244
  code_input = gr.Code(
245
- label="Manim Script",
246
  language="python",
247
- lines=20,
248
- value=EXAMPLE_SCRIPTS["Hello World"]
249
  )
250
 
251
- with gr.Row():
252
- scene_name_input = gr.Textbox(
253
- label="Scene Class Name",
254
- placeholder="e.g., HelloWorld",
255
- value="HelloWorld"
256
- )
257
- quality_input = gr.Dropdown(
258
- choices=["low_quality", "medium_quality", "high_quality", "production_quality"],
259
- label="Quality",
260
- value="medium_quality"
261
- )
262
 
263
- render_btn = gr.Button("🎬 Render Animation", variant="primary", size="lg")
264
 
265
  with gr.Column(scale=1):
266
  gr.Markdown("### πŸŽ₯ Output")
267
 
268
  status_output = gr.Textbox(
269
  label="Status",
270
- lines=3,
271
  interactive=False
272
  )
273
 
@@ -277,17 +315,18 @@ with gr.Blocks(title="Manim MCP Server", theme=gr.themes.Soft()) as demo:
277
  )
278
 
279
  gr.Markdown("""
280
- ### πŸ“‹ Tips:
281
- - Make sure your scene class inherits from `Scene`
282
- - Import manim with `from manim import *`
283
- - Use `self.play()` to animate objects
284
- - End with `self.wait()` for a pause
285
- - Keep animations short to avoid timeouts
286
 
287
  ### ⚠️ Limitations:
288
- - 60-second execution timeout
289
- - No dangerous operations allowed
290
- - Medium quality by default to save processing time
 
291
  """)
292
 
293
  # Event handlers
@@ -299,12 +338,12 @@ with gr.Blocks(title="Manim MCP Server", theme=gr.themes.Soft()) as demo:
299
 
300
  clear_btn.click(
301
  fn=clear_all,
302
- outputs=[code_input, scene_name_input, video_output, status_output]
303
  )
304
 
305
- render_btn.click(
306
- fn=render_animation,
307
- inputs=[code_input, scene_name_input, quality_input],
308
  outputs=[video_output, status_output]
309
  )
310
 
 
9
  import time
10
  import threading
11
  import queue
12
+ import matplotlib.pyplot as plt
13
+ import matplotlib.animation as animation
14
+ import numpy as np
15
+ from io import StringIO
16
+ import sys
17
 
18
+ class LightweightAnimationServer:
19
  def __init__(self):
20
  self.temp_dir = tempfile.mkdtemp()
21
+ self.max_execution_time = 30 # 30 seconds timeout
22
 
23
+ def validate_animation_code(self, code):
24
+ """Basic validation of animation code"""
25
  dangerous_imports = [
26
  'subprocess', 'os.system', 'eval', 'exec', 'open',
27
+ '__import__', 'compile', 'globals', 'locals', 'input'
28
  ]
29
 
30
  for dangerous in dangerous_imports:
 
32
  return False, f"Dangerous operation detected: {dangerous}"
33
 
34
  # Check if it's a valid Python-like structure
35
+ if 'import matplotlib' not in code and 'from matplotlib' not in code:
36
+ return False, "Code must import matplotlib"
37
 
38
+ if 'def animate' not in code and 'def create_animation' not in code:
39
+ return False, "Code must contain an animate or create_animation function"
40
 
41
  return True, "Valid"
42
 
43
+ def execute_animation_code(self, code):
44
+ """Execute animation code safely"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  try:
46
  # Validate code
47
+ is_valid, message = self.validate_animation_code(code)
48
  if not is_valid:
49
  return None, f"Code validation failed: {message}"
50
 
51
+ # Create a restricted execution environment
52
+ restricted_globals = {
53
+ '__builtins__': {
54
+ 'range': range,
55
+ 'len': len,
56
+ 'enumerate': enumerate,
57
+ 'zip': zip,
58
+ 'min': min,
59
+ 'max': max,
60
+ 'abs': abs,
61
+ 'round': round,
62
+ 'sum': sum,
63
+ 'print': print,
64
+ 'int': int,
65
+ 'float': float,
66
+ 'str': str,
67
+ 'list': list,
68
+ 'dict': dict,
69
+ 'tuple': tuple,
70
+ 'set': set,
71
+ },
72
+ 'np': np,
73
+ 'plt': plt,
74
+ 'animation': animation,
75
+ 'matplotlib': plt.matplotlib,
76
  }
77
 
78
+ # Create output file path
79
+ output_path = os.path.join(self.temp_dir, "animation.mp4")
 
 
 
 
 
 
 
80
 
81
+ # Add the output path to the environment
82
+ restricted_globals['output_path'] = output_path
83
 
84
+ # Execute the code
85
+ exec(code, restricted_globals)
86
 
87
+ # Check if animation was created
88
+ if os.path.exists(output_path):
89
+ return output_path, "Animation created successfully!"
90
+ else:
91
+ return None, "No animation file was generated. Make sure your code saves to 'output_path'."
 
 
 
 
 
 
 
92
 
 
 
 
 
93
  except Exception as e:
94
+ return None, f"Execution error: {str(e)}\n{traceback.format_exc()}"
95
 
96
  def cleanup(self):
97
  """Clean up temporary files"""
 
101
  pass
102
 
103
  # Initialize the server
104
+ animation_server = LightweightAnimationServer()
105
 
106
+ # Example animation scripts
107
  EXAMPLE_SCRIPTS = {
108
+ "Sine Wave": '''import matplotlib.pyplot as plt
109
+ import matplotlib.animation as animation
110
+ import numpy as np
111
 
112
+ # Create figure and axis
113
+ fig, ax = plt.subplots(figsize=(10, 6))
114
+ ax.set_xlim(0, 4*np.pi)
115
+ ax.set_ylim(-2, 2)
116
+ ax.set_xlabel('x')
117
+ ax.set_ylabel('sin(x)')
118
+ ax.set_title('Animated Sine Wave')
 
 
119
 
120
+ x = np.linspace(0, 4*np.pi, 1000)
121
+ line, = ax.plot([], [], 'b-', linewidth=2)
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ def animate(frame):
124
+ y = np.sin(x + frame/10)
125
+ line.set_data(x, y)
126
+ return line,
127
+
128
+ # Create animation
129
+ anim = animation.FuncAnimation(fig, animate, frames=200, interval=50, blit=True)
130
+ anim.save(output_path, writer='ffmpeg', fps=20)
131
+ plt.close()''',
132
+
133
+ "Growing Circle": '''import matplotlib.pyplot as plt
134
+ import matplotlib.animation as animation
135
+ import numpy as np
136
+
137
+ fig, ax = plt.subplots(figsize=(8, 8))
138
+ ax.set_xlim(-2, 2)
139
+ ax.set_ylim(-2, 2)
140
+ ax.set_aspect('equal')
141
+ ax.set_title('Growing Circle')
142
+
143
+ circle = plt.Circle((0, 0), 0, fill=False, color='red', linewidth=3)
144
+ ax.add_patch(circle)
145
+
146
+ def animate(frame):
147
+ radius = (frame / 100) * 1.5
148
+ circle.set_radius(radius)
149
+ return circle,
150
+
151
+ anim = animation.FuncAnimation(fig, animate, frames=150, interval=100, blit=True)
152
+ anim.save(output_path, writer='ffmpeg', fps=10)
153
+ plt.close()''',
154
+
155
+ "Rotating Plot": '''import matplotlib.pyplot as plt
156
+ import matplotlib.animation as animation
157
+ import numpy as np
158
+
159
+ fig = plt.figure(figsize=(10, 8))
160
+ ax = fig.add_subplot(111, projection='3d')
161
+
162
+ # Generate data
163
+ t = np.linspace(0, 2*np.pi, 100)
164
+ x = np.cos(t)
165
+ y = np.sin(t)
166
+ z = t
167
+
168
+ line, = ax.plot(x, y, z, 'b-', linewidth=2)
169
+ ax.set_xlabel('X')
170
+ ax.set_ylabel('Y')
171
+ ax.set_zlabel('Z')
172
+ ax.set_title('Rotating 3D Helix')
173
+
174
+ def animate(frame):
175
+ ax.view_init(elev=30, azim=frame*2)
176
+ return line,
177
+
178
+ anim = animation.FuncAnimation(fig, animate, frames=180, interval=100, blit=False)
179
+ anim.save(output_path, writer='ffmpeg', fps=10)
180
+ plt.close()''',
181
+
182
+ "Bar Chart Race": '''import matplotlib.pyplot as plt
183
+ import matplotlib.animation as animation
184
+ import numpy as np
185
+
186
+ fig, ax = plt.subplots(figsize=(12, 8))
187
+
188
+ # Sample data
189
+ categories = ['A', 'B', 'C', 'D', 'E']
190
+ colors = ['red', 'blue', 'green', 'orange', 'purple']
191
+
192
+ def animate(frame):
193
+ ax.clear()
194
+
195
+ # Generate some dynamic data
196
+ values = np.random.rand(5) * (frame + 1) * 10
197
+
198
+ bars = ax.bar(categories, values, color=colors, alpha=0.7)
199
+ ax.set_ylim(0, 100)
200
+ ax.set_title(f'Animated Bar Chart - Frame {frame}')
201
+ ax.set_ylabel('Values')
202
+
203
+ # Add value labels on bars
204
+ for bar, value in zip(bars, values):
205
+ height = bar.get_height()
206
+ ax.text(bar.get_x() + bar.get_width()/2., height + 1,
207
+ f'{value:.1f}', ha='center', va='bottom')
208
+
209
+ return bars
210
+
211
+ anim = animation.FuncAnimation(fig, animate, frames=50, interval=200, blit=False)
212
+ anim.save(output_path, writer='ffmpeg', fps=5)
213
+ plt.close()'''
214
  }
215
 
216
+ def create_animation(code, animation_type):
217
+ """Gradio interface function for creating animations"""
218
  if not code.strip():
219
+ return None, "Please provide animation code"
220
 
221
  try:
222
+ video_path, message = animation_server.execute_animation_code(code)
223
 
224
  if video_path:
225
  return video_path, f"βœ… {message}"
 
230
  return None, f"❌ Error: {str(e)}"
231
 
232
  def load_example(example_name):
233
+ """Load example script"""
234
  return EXAMPLE_SCRIPTS.get(example_name, "")
235
 
236
  def clear_all():
237
  """Clear all fields"""
238
+ return "", None, ""
239
+
240
+ # Try to install manim if not available, fallback to matplotlib-only mode
241
+ try:
242
+ import manim
243
+ MANIM_AVAILABLE = True
244
+ except ImportError:
245
+ MANIM_AVAILABLE = False
246
 
247
  # Create Gradio interface
248
+ with gr.Blocks(title="Animation MCP Server", theme=gr.themes.Soft()) as demo:
249
+ if not MANIM_AVAILABLE:
250
+ gr.Markdown("""
251
+ # 🎬 Animation MCP Server (Lightweight Mode)
252
+
253
+ **Note**: Full Manim is not available in this environment. This is a lightweight version using Matplotlib animations.
254
+
255
+ Create animated plots, charts, and visualizations using matplotlib's animation capabilities!
256
+ """)
257
+ else:
258
+ gr.Markdown("""
259
+ # 🎬 Animation MCP Server
260
+
261
+ Create animations using matplotlib or Manim Community Edition!
262
+ """)
263
 
264
+ gr.Markdown("""
265
  ## How to use:
266
+ 1. Write or select an animation script
267
+ 2. Make sure your code saves the animation to `output_path`
268
+ 3. Click "Create Animation"
269
+
270
+ ### Key Requirements:
271
+ - Import matplotlib: `import matplotlib.pyplot as plt`
272
+ - Create animation: Use `matplotlib.animation.FuncAnimation`
273
+ - Save to output: `anim.save(output_path, writer='ffmpeg', fps=10)`
274
  """)
275
 
276
  with gr.Row():
277
  with gr.Column(scale=2):
278
+ gr.Markdown("### πŸ“ Animation Code")
279
 
280
  with gr.Row():
281
  example_dropdown = gr.Dropdown(
 
286
  clear_btn = gr.Button("Clear All", size="sm")
287
 
288
  code_input = gr.Code(
289
+ label="Animation Script",
290
  language="python",
291
+ lines=25,
292
+ value=EXAMPLE_SCRIPTS["Sine Wave"]
293
  )
294
 
295
+ animation_type = gr.Dropdown(
296
+ choices=["matplotlib", "manim"] if MANIM_AVAILABLE else ["matplotlib"],
297
+ label="Animation Type",
298
+ value="matplotlib"
299
+ )
 
 
 
 
 
 
300
 
301
+ create_btn = gr.Button("🎬 Create Animation", variant="primary", size="lg")
302
 
303
  with gr.Column(scale=1):
304
  gr.Markdown("### πŸŽ₯ Output")
305
 
306
  status_output = gr.Textbox(
307
  label="Status",
308
+ lines=4,
309
  interactive=False
310
  )
311
 
 
315
  )
316
 
317
  gr.Markdown("""
318
+ ### πŸ“‹ Tips for Matplotlib Animations:
319
+ - Use `fig, ax = plt.subplots()` to create your plot
320
+ - Define an `animate(frame)` function that updates your plot
321
+ - Use `FuncAnimation(fig, animate, frames=N, interval=ms)`
322
+ - Always save with `anim.save(output_path, writer='ffmpeg')`
323
+ - Call `plt.close()` at the end to free memory
324
 
325
  ### ⚠️ Limitations:
326
+ - 30-second execution timeout
327
+ - No file I/O operations
328
+ - Limited to safe Python operations
329
+ - Matplotlib animations only (unless full Manim is available)
330
  """)
331
 
332
  # Event handlers
 
338
 
339
  clear_btn.click(
340
  fn=clear_all,
341
+ outputs=[code_input, video_output, status_output]
342
  )
343
 
344
+ create_btn.click(
345
+ fn=create_animation,
346
+ inputs=[code_input, animation_type],
347
  outputs=[video_output, status_output]
348
  )
349