sreepathi-ravikumar commited on
Commit
71db123
·
verified ·
1 Parent(s): 5ac48a5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +274 -0
app.py CHANGED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_file
2
+ from werkzeug.utils import secure_filename
3
+ import os
4
+ import subprocess
5
+ import tempfile
6
+ import shutil
7
+ from datetime import datetime
8
+ import traceback
9
+ import json
10
+
11
+ app = Flask(__name__)
12
+
13
+ # Configuration
14
+ BASE_DIR = "/app"
15
+ MEDIA_DIR = os.path.join(BASE_DIR, "media")
16
+ TEMP_DIR = os.path.join(BASE_DIR, "temp")
17
+ os.makedirs(MEDIA_DIR, exist_ok=True)
18
+ os.makedirs(TEMP_DIR, exist_ok=True)
19
+
20
+ def create_manim_script(problem_data, script_path):
21
+ """Generate Manim script from problem data"""
22
+
23
+ # Default settings
24
+ settings = problem_data.get("video_settings", {
25
+ "background_color": "#0f0f23",
26
+ "text_color": "WHITE",
27
+ "highlight_color": "YELLOW",
28
+ "font": "CMU Serif",
29
+ "text_size": 36,
30
+ "equation_size": 42,
31
+ "title_size": 48
32
+ })
33
+
34
+ slides = problem_data.get("slides", [])
35
+
36
+ if not slides:
37
+ raise ValueError("No slides provided in input data")
38
+
39
+ # Generate Manim script
40
+ manim_code = f'''
41
+ from manim import *
42
+
43
+ class GeneratedMathScene(Scene):
44
+ def construct(self):
45
+ # Settings
46
+ self.camera.background_color = "{settings.get('background_color', '#0f0f23')}"
47
+ default_color = {settings.get('text_color', 'WHITE')}
48
+ highlight_color = {settings.get('highlight_color', 'YELLOW')}
49
+ default_font = "{settings.get('font', 'CMU Serif')}"
50
+ text_size = {settings.get('text_size', 36)}
51
+ equation_size = {settings.get('equation_size', 42)}
52
+ title_size = {settings.get('title_size', 48)}
53
+
54
+ # Content group for scrolling
55
+ content_group = VGroup()
56
+ current_y = 3.0
57
+ line_spacing = 0.8
58
+ screen_bottom = -3.5
59
+
60
+ # Process slides
61
+ slides = {json.dumps(slides)}
62
+
63
+ for idx, slide in enumerate(slides):
64
+ obj = None
65
+ content = slide.get("content", "")
66
+ animation = slide.get("animation", "write_left")
67
+ duration = slide.get("duration", 1.0)
68
+ slide_type = slide.get("type", "text")
69
+
70
+ # Create title (centered)
71
+ if slide_type == "title":
72
+ obj = Text(content, color=highlight_color, font=default_font, font_size=title_size)
73
+ obj.move_to(ORIGIN)
74
+ self.play(FadeIn(obj), run_time=duration * 0.8)
75
+ self.wait(duration * 0.3)
76
+ self.play(FadeOut(obj), run_time=duration * 0.3)
77
+ continue
78
+
79
+ # Create text
80
+ elif slide_type == "text":
81
+ obj = Text(content, color=default_color, font=default_font, font_size=text_size)
82
+
83
+ # Create equation
84
+ elif slide_type == "equation":
85
+ obj = MathTex(content, color=default_color, font_size=equation_size)
86
+
87
+ if obj:
88
+ # Position at left edge
89
+ obj.to_edge(LEFT, buff=0.3)
90
+ obj.shift(UP * (current_y - obj.height/2))
91
+
92
+ # Check if scrolling needed
93
+ obj_bottom = obj.get_bottom()[1]
94
+ if obj_bottom < screen_bottom:
95
+ scroll_amount = abs(obj_bottom - screen_bottom) + 0.3
96
+ self.play(content_group.animate.shift(UP * scroll_amount), run_time=0.5)
97
+ current_y += scroll_amount
98
+ obj.shift(UP * scroll_amount)
99
+
100
+ # Animations
101
+ if animation == "write_left":
102
+ self.play(Write(obj), run_time=duration)
103
+ elif animation == "fade_in":
104
+ self.play(FadeIn(obj), run_time=duration)
105
+ elif animation == "highlight_left":
106
+ self.play(Write(obj), run_time=duration * 0.6)
107
+ self.play(obj.animate.set_color(highlight_color), run_time=duration * 0.4)
108
+ else:
109
+ self.play(Write(obj), run_time=duration)
110
+
111
+ content_group.add(obj)
112
+ current_y -= (obj.height + line_spacing)
113
+ self.wait(0.3)
114
+
115
+ # Highlight final answer
116
+ if len(content_group) > 0:
117
+ final_box = SurroundingRectangle(content_group[-1], color=highlight_color, buff=0.2)
118
+ self.play(Create(final_box), run_time=0.8)
119
+ self.wait(1.5)
120
+ '''
121
+
122
+ with open(script_path, 'w') as f:
123
+ f.write(manim_code)
124
+
125
+ @app.route('/', methods=['GET'])
126
+ def home():
127
+ """Health check endpoint"""
128
+ return jsonify({
129
+ "status": "running",
130
+ "message": "Manim Video Generation API",
131
+ "endpoints": {
132
+ "/generate": "POST - Generate video from JSON",
133
+ "/health": "GET - Health check"
134
+ }
135
+ })
136
+
137
+ @app.route('/health', methods=['GET'])
138
+ def health():
139
+ """Health check"""
140
+ return jsonify({"status": "healthy"})
141
+
142
+ @app.route('/generate', methods=['POST'])
143
+ def generate_video():
144
+ """
145
+ Generate Manim video from JSON input
146
+
147
+ Expected JSON format:
148
+ {
149
+ "video_settings": {
150
+ "background_color": "#0f0f23",
151
+ "text_color": "WHITE",
152
+ "highlight_color": "YELLOW",
153
+ "font": "CMU Serif",
154
+ "text_size": 36,
155
+ "equation_size": 42,
156
+ "title_size": 48
157
+ },
158
+ "slides": [
159
+ {
160
+ "type": "title",
161
+ "content": "Your Title",
162
+ "animation": "fade_in",
163
+ "duration": 1.0
164
+ },
165
+ {
166
+ "type": "text",
167
+ "content": "Your text content",
168
+ "animation": "write_left",
169
+ "duration": 0.8
170
+ },
171
+ {
172
+ "type": "equation",
173
+ "content": "x = \\\\frac{-b \\\\pm \\\\sqrt{b^2 - 4ac}}{2a}",
174
+ "animation": "write_left",
175
+ "duration": 1.2
176
+ }
177
+ ]
178
+ }
179
+ """
180
+ try:
181
+ # Get JSON data
182
+ if not request.json:
183
+ return jsonify({"error": "No JSON data provided"}), 400
184
+
185
+ problem_data = request.json
186
+
187
+ # Validate input
188
+ if "slides" not in problem_data or not problem_data["slides"]:
189
+ return jsonify({"error": "No slides provided in request"}), 400
190
+
191
+ # Create unique temporary directory
192
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
193
+ temp_work_dir = os.path.join(TEMP_DIR, f"manim_{timestamp}")
194
+ os.makedirs(temp_work_dir, exist_ok=True)
195
+
196
+ # Generate Manim script
197
+ script_path = os.path.join(temp_work_dir, "scene.py")
198
+ create_manim_script(problem_data, script_path)
199
+
200
+ # Render video
201
+ quality = request.args.get('quality', 'l') # l=low, m=medium, h=high
202
+ render_command = [
203
+ "manim",
204
+ f"-q{quality}",
205
+ "--disable_caching",
206
+ "--media_dir", temp_work_dir,
207
+ script_path,
208
+ "GeneratedMathScene"
209
+ ]
210
+
211
+ result = subprocess.run(
212
+ render_command,
213
+ capture_output=True,
214
+ text=True,
215
+ cwd=temp_work_dir,
216
+ timeout=120
217
+ )
218
+
219
+ if result.returncode != 0:
220
+ error_msg = result.stderr or result.stdout
221
+ return jsonify({
222
+ "error": "Manim rendering failed",
223
+ "details": error_msg
224
+ }), 500
225
+
226
+ # Find generated video
227
+ quality_map = {'l': '480p15', 'm': '720p30', 'h': '1080p60'}
228
+ video_quality = quality_map.get(quality, '480p15')
229
+
230
+ video_path = os.path.join(
231
+ temp_work_dir,
232
+ "videos",
233
+ "scene",
234
+ video_quality,
235
+ "GeneratedMathScene.mp4"
236
+ )
237
+
238
+ if not os.path.exists(video_path):
239
+ return jsonify({
240
+ "error": "Video file not found after rendering",
241
+ "expected_path": video_path
242
+ }), 500
243
+
244
+ # Copy to media directory for serving
245
+ output_filename = f"math_video_{timestamp}.mp4"
246
+ output_path = os.path.join(MEDIA_DIR, output_filename)
247
+ shutil.copy(video_path, output_path)
248
+
249
+ # Clean up temp directory
250
+ try:
251
+ shutil.rmtree(temp_work_dir)
252
+ except:
253
+ pass
254
+
255
+ # Return video file
256
+ return send_file(
257
+ output_path,
258
+ mimetype='video/mp4',
259
+ as_attachment=True,
260
+ download_name=output_filename
261
+ )
262
+
263
+ except subprocess.TimeoutExpired:
264
+ return jsonify({"error": "Video rendering timeout (120s)"}), 504
265
+ except Exception as e:
266
+ return jsonify({
267
+ "error": str(e),
268
+ "traceback": traceback.format_exc()
269
+ }), 500
270
+
271
+ if __name__ == '__main__':
272
+ # Use gunicorn in production, Flask dev server for local testing
273
+ port = int(os.environ.get('PORT', 7860))
274
+ app.run(host='0.0.0.0', port=port, debug=False)