sreepathi-ravikumar commited on
Commit
a26baec
·
verified ·
1 Parent(s): 8e6a036

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +113 -19
app.py CHANGED
@@ -21,9 +21,48 @@ os.makedirs(TEMP_DIR, exist_ok=True)
21
  # API Key for security (optional)
22
  API_KEY = "rkmentormindzofficaltokenkey12345"
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  def create_manim_script(problem_data, script_path):
26
  """Generate Manim script from problem data with robust wrapping for title, text, and equations."""
 
27
  # Defaults
28
  settings = problem_data.get("video_settings", {
29
  "background_color": "#0f0f23",
@@ -35,14 +74,19 @@ def create_manim_script(problem_data, script_path):
35
  "title_size": 48,
36
  "wrap_width": 12.0 # in scene width units; adjust to taste
37
  })
 
38
  slides = problem_data.get("slides", [])
39
  if not slides:
40
  raise ValueError("No slides provided in input data")
 
41
  slides_repr = repr(slides)
 
42
  # Use a dedicated wrap width in scene units; you can adapt how max_width is computed
43
  wrap_width = float(settings.get("wrap_width", 12.0))
 
44
  manim_code = f'''
45
  from manim import *
 
46
  class GeneratedMathScene(Scene):
47
  def construct(self):
48
  # Scene settings
@@ -54,6 +98,7 @@ class GeneratedMathScene(Scene):
54
  equation_size = {settings.get('equation_size', 42)}
55
  title_size = {settings.get('title_size', 48)}
56
  wrap_width = {wrap_width}
 
57
  # Helper to wrap text into lines that fit within max width
58
  def make_wrapped_paragraph(content, color, font, font_size, line_spacing=0.2):
59
  lines = []
@@ -73,10 +118,13 @@ class GeneratedMathScene(Scene):
73
  return VGroup()
74
  para = VGroup(*lines).arrange(DOWN, buff=line_spacing)
75
  return para
 
 
76
  content_group = VGroup()
77
  current_y = 3.0
78
  line_spacing = 0.8
79
  slides = {slides_repr}
 
80
  # Build each slide
81
  for idx, slide in enumerate(slides):
82
  obj = None
@@ -84,22 +132,32 @@ class GeneratedMathScene(Scene):
84
  animation = slide.get("animation", "write_left")
85
  duration = slide.get("duration", 1.0)
86
  slide_type = slide.get("type", "text")
 
87
  if slide_type == "title":
88
  # Wrap title text
89
  title_text = content
90
  # Use paragraph wrapping to keep multi-line titles readable
91
- lines_group = make_wrapped_paragraph(title_text, highlight_color, default_font, title_size, line_spacing=0.2)
92
- obj = lines_group if len(lines_group) > 0 else Text(title_text, color=highlight_color, font=default_font, font_size=title_size)
 
 
 
 
 
 
93
  if obj.width > wrap_width:
94
  obj.scale_to_fit_width(wrap_width)
 
95
  obj.move_to(ORIGIN)
96
  self.play(FadeIn(obj), run_time=duration * 0.8)
97
  self.wait(duration * 0.3)
98
  self.play(FadeOut(obj), run_time=duration * 0.3)
99
  continue
 
100
  elif slide_type == "text":
101
  # Use wrapping for normal text
102
  obj = make_wrapped_paragraph(content, default_color, default_font, text_size, line_spacing=0.25)
 
103
  elif slide_type == "equation":
104
  # Wrap long equations by splitting content into lines if needed
105
  # Heuristic: if content is too wide, create a multi-line TeX using \\ line breaks
@@ -112,16 +170,19 @@ class GeneratedMathScene(Scene):
112
  mid = len(parts)//2
113
  line1 = " ".join(parts[:mid])
114
  line2 = " ".join(parts[mid:])
115
- wrapped_eq = f"{{line1}} \\\\ {{line2}}"
116
  obj = MathTex(wrapped_eq, color=default_color, font_size=equation_size)
117
  else:
118
  obj = MathTex(eq_content, color=default_color, font_size=equation_size)
 
119
  if obj.width > wrap_width:
120
  obj.scale_to_fit_width(wrap_width)
 
121
  if obj:
122
  # Position and animate
123
  obj.to_edge(LEFT, buff=0.3)
124
  obj.shift(UP * (current_y - obj.height/2))
 
125
  obj_bottom = obj.get_bottom()[1]
126
  if obj_bottom < -3.5:
127
  scroll_amount = abs(obj_bottom - (-3.5)) + 0.3
@@ -129,6 +190,7 @@ class GeneratedMathScene(Scene):
129
  current_y += scroll_amount
130
  obj.shift(UP * scroll_amount)
131
  obj.to_edge(LEFT, buff=0.3)
 
132
  if animation == "write_left":
133
  self.play(Write(obj), run_time=duration)
134
  elif animation == "fade_in":
@@ -138,41 +200,49 @@ class GeneratedMathScene(Scene):
138
  self.play(obj.animate.set_color(highlight_color), run_time=duration * 0.4)
139
  else:
140
  self.play(Write(obj), run_time=duration)
 
141
  content_group.add(obj)
142
  # Decrease y for next item
143
  current_y -= (getattr(obj, "height", 0) + line_spacing)
144
  self.wait(0.3)
 
145
  if len(content_group) > 0:
146
  final_box = SurroundingRectangle(content_group[-1], color=highlight_color, buff=0.2)
147
  self.play(Create(final_box), run_time=0.8)
148
  self.wait(1.5)
149
  '''
 
150
  with open(script_path, 'w', encoding='utf-8') as f:
151
  f.write(manim_code)
 
152
  print(f"Generated script preview (first 500 chars):{manim_code[:500]}...")
153
 
154
-
155
  @app.route("/")
156
  def home():
157
  return "Flask Manim Video Generator is Running"
158
 
159
-
160
  @app.route("/generate", methods=["POST", "OPTIONS"])
161
  def generate_video():
162
  # Handle preflight
163
  if request.method == "OPTIONS":
164
  return '', 204
 
165
  try:
166
  # Optional: Check API key
167
  api_key = request.headers.get('X-API-KEY')
168
  if api_key and api_key != API_KEY:
169
  return jsonify({"error": "Invalid API key"}), 401
 
170
  # Get JSON data
 
 
171
  # Try reading raw body text
172
  raw_body = request.data.decode("utf-8").strip()
173
  data = None
 
174
  if not raw_body:
175
  return jsonify({"error": "No input data provided"}), 400
 
176
  # Try to detect if input is JSON or plain string
177
  if raw_body.startswith("{") or raw_body.startswith("["):
178
  # Likely JSON, try parsing
@@ -182,12 +252,15 @@ def generate_video():
182
  except json.JSONDecodeError:
183
  # Not valid JSON, fallback to manual parse
184
  data = None
 
185
  if data is None:
186
  print("⚙️ Detected raw string input (non-JSON). Parsing manually...")
 
187
  # Handle format like: [ [...], [...]] &&& Tamil explanation
188
  parts = raw_body.split("&&&")
189
  slides_part = parts[0].strip()
190
  extra_info = parts[1].strip() if len(parts) > 1 else ""
 
191
  try:
192
  slides = json.loads(slides_part)
193
  except json.JSONDecodeError as e:
@@ -196,22 +269,22 @@ def generate_video():
196
  "details": str(e),
197
  "raw_snippet": slides_part[:200]
198
  }), 400
199
- # Convert list of lists → list of dicts with defaults for missing fields
 
200
  slides_json = []
201
  for s in slides:
202
- slide_type = s[0] if len(s) > 0 else "text"
203
- content = s[1] if len(s) > 1 else (s[0] if len(s) > 0 else "")
204
- animation = s[2] if len(s) > 2 else "write_left"
205
- duration = s[3] if len(s) > 3 else 2.0
206
- slides_json.append({
207
- "type": slide_type,
208
- "content": content,
209
- "animation": animation,
210
- "duration": duration
211
- })
212
  data = {
213
  "slides": slides_json,
214
- "language": extra_info if extra_info else "English",
215
  "explanation": extra_info,
216
  "video_settings": {
217
  "background_color": "#0f0f23",
@@ -220,19 +293,29 @@ def generate_video():
220
  "font": "CMU Serif"
221
  }
222
  }
223
- # Final validation
 
224
  if "slides" not in data or not data["slides"]:
225
  return jsonify({"error": "No slides provided in request"}), 400
 
226
  print(f"✅ Parsed {len(data['slides'])} slides successfully.")
 
 
 
 
 
227
  print(f"Received request with {len(data['slides'])} slides")
 
228
  # Create unique temporary directory
229
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
230
  temp_work_dir = os.path.join(TEMP_DIR, f"manim_{timestamp}")
231
  os.makedirs(temp_work_dir, exist_ok=True)
 
232
  # Generate Manim script
233
  script_path = os.path.join(temp_work_dir, "scene.py")
234
  create_manim_script(data, script_path)
235
  print(f"Created Manim script at {script_path}")
 
236
  # Render video using subprocess
237
  quality = request.args.get('quality', 'l') # l=low, m=medium, h=high
238
  render_command = [
@@ -243,7 +326,9 @@ def generate_video():
243
  script_path,
244
  "GeneratedMathScene"
245
  ]
 
246
  print(f"Running command: {' '.join(render_command)}")
 
247
  result = subprocess.run(
248
  render_command,
249
  capture_output=True,
@@ -251,6 +336,7 @@ def generate_video():
251
  cwd=temp_work_dir,
252
  timeout=120
253
  )
 
254
  if result.returncode != 0:
255
  error_msg = result.stderr or result.stdout
256
  print(f"Manim rendering failed: {error_msg}")
@@ -258,10 +344,13 @@ def generate_video():
258
  "error": "Manim rendering failed",
259
  "details": error_msg
260
  }), 500
 
261
  print("Manim rendering completed successfully")
 
262
  # Find generated video
263
  quality_map = {'l': '480p15', 'm': '720p30', 'h': '1080p60'}
264
  video_quality = quality_map.get(quality, '480p15')
 
265
  video_path = os.path.join(
266
  temp_work_dir,
267
  "videos",
@@ -269,24 +358,29 @@ def generate_video():
269
  video_quality,
270
  "GeneratedMathScene.mp4"
271
  )
 
272
  if not os.path.exists(video_path):
273
  print(f"Video not found at expected path: {video_path}")
274
  return jsonify({
275
  "error": "Video file not found after rendering",
276
  "expected_path": video_path
277
  }), 500
 
278
  print(f"Video found at: {video_path}")
 
279
  # Copy to media directory
280
  output_filename = f"math_video_{timestamp}.mp4"
281
  output_path = os.path.join(MEDIA_DIR, output_filename)
282
  shutil.copy(video_path, output_path)
283
  print(f"Video copied to: {output_path}")
 
284
  # Clean up temp directory
285
  try:
286
  shutil.rmtree(temp_work_dir)
287
  print("Cleaned up temp directory")
288
  except Exception as e:
289
  print(f"Failed to clean temp dir: {e}")
 
290
  # Return video file as blob
291
  return send_file(
292
  output_path,
@@ -294,6 +388,7 @@ def generate_video():
294
  as_attachment=False,
295
  download_name=output_filename
296
  )
 
297
  except subprocess.TimeoutExpired:
298
  print("Video rendering timeout")
299
  return jsonify({"error": "Video rendering timeout (120s)"}), 504
@@ -305,7 +400,6 @@ def generate_video():
305
  "traceback": traceback.format_exc()
306
  }), 500
307
 
308
-
309
  if __name__ == '__main__':
310
  port = int(os.environ.get('PORT', 7860))
311
  app.run(host='0.0.0.0', port=port, debug=False)
 
21
  # API Key for security (optional)
22
  API_KEY = "rkmentormindzofficaltokenkey12345"
23
 
24
+ import textwrap
25
+ from manim import *
26
+
27
+ def make_wrapped_paragraph(content, max_width, color, font, font_size, line_spacing, align_left=True):
28
+ """
29
+ Build a vertically stacked group of Text lines that together form a paragraph.
30
+ It splits content into lines that fit within max_width by measuring rendered width.
31
+ Each line is a separate Text object joined into a VGroup and arranged downward.
32
+ """
33
+ words = content.split()
34
+ lines = []
35
+ current = ""
36
+
37
+ # Create a temporary Text to measure width; use the same font/size as final lines
38
+ temp = Text("", color=color, font=font, font_size=font_size)
39
+
40
+ for w in words:
41
+ test = w if not current else current + " " + w
42
+ test_obj = Text(test, color=color, font=font, font_size=font_size)
43
+ if test_obj.width <= max_width:
44
+ current = test
45
+ else:
46
+ # flush the current line
47
+ line = Text(current, color=color, font=font, font_size=font_size)
48
+ lines.append(line)
49
+ current = w
50
+ if current:
51
+ lines.append(Text(current, color=color, font=font, font_size=font_size))
52
+
53
+ if not lines:
54
+ return VGroup()
55
+
56
+ para = VGroup(*lines)
57
+ # Space lines vertically; arrange them as a column
58
+ para.arrange(DOWN, buff=line_spacing)
59
+ if align_left:
60
+ para = para.align_to(LEFT)
61
+ return para.strip()
62
 
63
  def create_manim_script(problem_data, script_path):
64
  """Generate Manim script from problem data with robust wrapping for title, text, and equations."""
65
+
66
  # Defaults
67
  settings = problem_data.get("video_settings", {
68
  "background_color": "#0f0f23",
 
74
  "title_size": 48,
75
  "wrap_width": 12.0 # in scene width units; adjust to taste
76
  })
77
+
78
  slides = problem_data.get("slides", [])
79
  if not slides:
80
  raise ValueError("No slides provided in input data")
81
+
82
  slides_repr = repr(slides)
83
+
84
  # Use a dedicated wrap width in scene units; you can adapt how max_width is computed
85
  wrap_width = float(settings.get("wrap_width", 12.0))
86
+
87
  manim_code = f'''
88
  from manim import *
89
+ import textwrap
90
  class GeneratedMathScene(Scene):
91
  def construct(self):
92
  # Scene settings
 
98
  equation_size = {settings.get('equation_size', 42)}
99
  title_size = {settings.get('title_size', 48)}
100
  wrap_width = {wrap_width}
101
+
102
  # Helper to wrap text into lines that fit within max width
103
  def make_wrapped_paragraph(content, color, font, font_size, line_spacing=0.2):
104
  lines = []
 
118
  return VGroup()
119
  para = VGroup(*lines).arrange(DOWN, buff=line_spacing)
120
  return para
121
+ class GeneratedMathSceneInner(Scene):
122
+ pass
123
  content_group = VGroup()
124
  current_y = 3.0
125
  line_spacing = 0.8
126
  slides = {slides_repr}
127
+
128
  # Build each slide
129
  for idx, slide in enumerate(slides):
130
  obj = None
 
132
  animation = slide.get("animation", "write_left")
133
  duration = slide.get("duration", 1.0)
134
  slide_type = slide.get("type", "text")
135
+
136
  if slide_type == "title":
137
  # Wrap title text
138
  title_text = content
139
  # Use paragraph wrapping to keep multi-line titles readable
140
+ lines = []
141
+ if title_text:
142
+ lines = []
143
+ # Reuse make_wrapped_paragraph by simulating a single paragraph
144
+ lines_group = make_wrapped_paragraph(title_text, highlight_color, default_font, title_size, line_spacing=0.2)
145
+ obj = lines_group if len(lines_group) > 0 else Text(title_text, color=highlight_color, font=default_font, font_size=title_size)
146
+ else:
147
+ obj = Text("", color=highlight_color, font=default_font, font_size=title_size)
148
  if obj.width > wrap_width:
149
  obj.scale_to_fit_width(wrap_width)
150
+
151
  obj.move_to(ORIGIN)
152
  self.play(FadeIn(obj), run_time=duration * 0.8)
153
  self.wait(duration * 0.3)
154
  self.play(FadeOut(obj), run_time=duration * 0.3)
155
  continue
156
+
157
  elif slide_type == "text":
158
  # Use wrapping for normal text
159
  obj = make_wrapped_paragraph(content, default_color, default_font, text_size, line_spacing=0.25)
160
+
161
  elif slide_type == "equation":
162
  # Wrap long equations by splitting content into lines if needed
163
  # Heuristic: if content is too wide, create a multi-line TeX using \\ line breaks
 
170
  mid = len(parts)//2
171
  line1 = " ".join(parts[:mid])
172
  line2 = " ".join(parts[mid:])
173
+ wrapped_eq = f"{{line1}} \\\\\\\\ {{line2}}"
174
  obj = MathTex(wrapped_eq, color=default_color, font_size=equation_size)
175
  else:
176
  obj = MathTex(eq_content, color=default_color, font_size=equation_size)
177
+
178
  if obj.width > wrap_width:
179
  obj.scale_to_fit_width(wrap_width)
180
+
181
  if obj:
182
  # Position and animate
183
  obj.to_edge(LEFT, buff=0.3)
184
  obj.shift(UP * (current_y - obj.height/2))
185
+
186
  obj_bottom = obj.get_bottom()[1]
187
  if obj_bottom < -3.5:
188
  scroll_amount = abs(obj_bottom - (-3.5)) + 0.3
 
190
  current_y += scroll_amount
191
  obj.shift(UP * scroll_amount)
192
  obj.to_edge(LEFT, buff=0.3)
193
+
194
  if animation == "write_left":
195
  self.play(Write(obj), run_time=duration)
196
  elif animation == "fade_in":
 
200
  self.play(obj.animate.set_color(highlight_color), run_time=duration * 0.4)
201
  else:
202
  self.play(Write(obj), run_time=duration)
203
+
204
  content_group.add(obj)
205
  # Decrease y for next item
206
  current_y -= (getattr(obj, "height", 0) + line_spacing)
207
  self.wait(0.3)
208
+
209
  if len(content_group) > 0:
210
  final_box = SurroundingRectangle(content_group[-1], color=highlight_color, buff=0.2)
211
  self.play(Create(final_box), run_time=0.8)
212
  self.wait(1.5)
213
  '''
214
+
215
  with open(script_path, 'w', encoding='utf-8') as f:
216
  f.write(manim_code)
217
+
218
  print(f"Generated script preview (first 500 chars):{manim_code[:500]}...")
219
 
 
220
  @app.route("/")
221
  def home():
222
  return "Flask Manim Video Generator is Running"
223
 
 
224
  @app.route("/generate", methods=["POST", "OPTIONS"])
225
  def generate_video():
226
  # Handle preflight
227
  if request.method == "OPTIONS":
228
  return '', 204
229
+
230
  try:
231
  # Optional: Check API key
232
  api_key = request.headers.get('X-API-KEY')
233
  if api_key and api_key != API_KEY:
234
  return jsonify({"error": "Invalid API key"}), 401
235
+
236
  # Get JSON data
237
+
238
+
239
  # Try reading raw body text
240
  raw_body = request.data.decode("utf-8").strip()
241
  data = None
242
+
243
  if not raw_body:
244
  return jsonify({"error": "No input data provided"}), 400
245
+
246
  # Try to detect if input is JSON or plain string
247
  if raw_body.startswith("{") or raw_body.startswith("["):
248
  # Likely JSON, try parsing
 
252
  except json.JSONDecodeError:
253
  # Not valid JSON, fallback to manual parse
254
  data = None
255
+
256
  if data is None:
257
  print("⚙️ Detected raw string input (non-JSON). Parsing manually...")
258
+
259
  # Handle format like: [ [...], [...]] &&& Tamil explanation
260
  parts = raw_body.split("&&&")
261
  slides_part = parts[0].strip()
262
  extra_info = parts[1].strip() if len(parts) > 1 else ""
263
+
264
  try:
265
  slides = json.loads(slides_part)
266
  except json.JSONDecodeError as e:
 
269
  "details": str(e),
270
  "raw_snippet": slides_part[:200]
271
  }), 400
272
+
273
+ # Convert list of lists → list of dicts
274
  slides_json = []
275
  for s in slides:
276
+ if len(s) >= 4:
277
+ slide_type, content, animation, duration = s
278
+ slides_json.append({
279
+ "type": slide_type,
280
+ "content": content,
281
+ "animation": animation,
282
+ "duration": duration
283
+ })
284
+
 
285
  data = {
286
  "slides": slides_json,
287
+ "language": "Tamil" if "Tamil" in extra_info else "English",
288
  "explanation": extra_info,
289
  "video_settings": {
290
  "background_color": "#0f0f23",
 
293
  "font": "CMU Serif"
294
  }
295
  }
296
+
297
+ # ✅ Final validation
298
  if "slides" not in data or not data["slides"]:
299
  return jsonify({"error": "No slides provided in request"}), 400
300
+
301
  print(f"✅ Parsed {len(data['slides'])} slides successfully.")
302
+
303
+ # Validate input
304
+ if "slides" not in data or not data["slides"]:
305
+ return jsonify({"error": "No slides provided in request"}), 400
306
+
307
  print(f"Received request with {len(data['slides'])} slides")
308
+
309
  # Create unique temporary directory
310
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
311
  temp_work_dir = os.path.join(TEMP_DIR, f"manim_{timestamp}")
312
  os.makedirs(temp_work_dir, exist_ok=True)
313
+
314
  # Generate Manim script
315
  script_path = os.path.join(temp_work_dir, "scene.py")
316
  create_manim_script(data, script_path)
317
  print(f"Created Manim script at {script_path}")
318
+
319
  # Render video using subprocess
320
  quality = request.args.get('quality', 'l') # l=low, m=medium, h=high
321
  render_command = [
 
326
  script_path,
327
  "GeneratedMathScene"
328
  ]
329
+
330
  print(f"Running command: {' '.join(render_command)}")
331
+
332
  result = subprocess.run(
333
  render_command,
334
  capture_output=True,
 
336
  cwd=temp_work_dir,
337
  timeout=120
338
  )
339
+
340
  if result.returncode != 0:
341
  error_msg = result.stderr or result.stdout
342
  print(f"Manim rendering failed: {error_msg}")
 
344
  "error": "Manim rendering failed",
345
  "details": error_msg
346
  }), 500
347
+
348
  print("Manim rendering completed successfully")
349
+
350
  # Find generated video
351
  quality_map = {'l': '480p15', 'm': '720p30', 'h': '1080p60'}
352
  video_quality = quality_map.get(quality, '480p15')
353
+
354
  video_path = os.path.join(
355
  temp_work_dir,
356
  "videos",
 
358
  video_quality,
359
  "GeneratedMathScene.mp4"
360
  )
361
+
362
  if not os.path.exists(video_path):
363
  print(f"Video not found at expected path: {video_path}")
364
  return jsonify({
365
  "error": "Video file not found after rendering",
366
  "expected_path": video_path
367
  }), 500
368
+
369
  print(f"Video found at: {video_path}")
370
+
371
  # Copy to media directory
372
  output_filename = f"math_video_{timestamp}.mp4"
373
  output_path = os.path.join(MEDIA_DIR, output_filename)
374
  shutil.copy(video_path, output_path)
375
  print(f"Video copied to: {output_path}")
376
+
377
  # Clean up temp directory
378
  try:
379
  shutil.rmtree(temp_work_dir)
380
  print("Cleaned up temp directory")
381
  except Exception as e:
382
  print(f"Failed to clean temp dir: {e}")
383
+
384
  # Return video file as blob
385
  return send_file(
386
  output_path,
 
388
  as_attachment=False,
389
  download_name=output_filename
390
  )
391
+
392
  except subprocess.TimeoutExpired:
393
  print("Video rendering timeout")
394
  return jsonify({"error": "Video rendering timeout (120s)"}), 504
 
400
  "traceback": traceback.format_exc()
401
  }), 500
402
 
 
403
  if __name__ == '__main__':
404
  port = int(os.environ.get('PORT', 7860))
405
  app.run(host='0.0.0.0', port=port, debug=False)