raksa-the-wildcats commited on
Commit
ff6940c
Β·
1 Parent(s): e49fe19

First Commit

Browse files
Files changed (2) hide show
  1. app.py +317 -0
  2. requirements.txt +7 -0
app.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import tempfile
3
+ import os
4
+ import subprocess
5
+ import json
6
+ import traceback
7
+ import shutil
8
+ 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:
26
+ if dangerous in code:
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"""
142
+ try:
143
+ shutil.rmtree(self.temp_dir)
144
+ except:
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}"
203
+ else:
204
+ return None, f"❌ {message}"
205
+
206
+ except Exception as e:
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(
238
+ choices=list(EXAMPLE_SCRIPTS.keys()),
239
+ label="Load Example",
240
+ value=None
241
+ )
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
+
274
+ video_output = gr.Video(
275
+ label="Generated Animation",
276
+ height=400
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
294
+ example_dropdown.change(
295
+ fn=load_example,
296
+ inputs=[example_dropdown],
297
+ outputs=[code_input]
298
+ )
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
+
311
+ # Launch the app
312
+ if __name__ == "__main__":
313
+ demo.launch(
314
+ server_name="0.0.0.0",
315
+ server_port=7860,
316
+ share=True
317
+ )
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ manim>=0.18.0
3
+ numpy
4
+ scipy
5
+ matplotlib
6
+ pillow
7
+ ffmpeg-python