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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +19 -116
app.py CHANGED
@@ -21,48 +21,9 @@ os.makedirs(TEMP_DIR, exist_ok=True)
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,19 +35,14 @@ def create_manim_script(problem_data, script_path):
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,7 +54,6 @@ class GeneratedMathScene(Scene):
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,16 +73,10 @@ class GeneratedMathScene(Scene):
118
  return VGroup()
119
  para = VGroup(*lines).arrange(DOWN, buff=line_spacing)
120
  return para
121
-
122
- class GeneratedMathSceneInner(Scene):
123
- pass
124
-
125
  content_group = VGroup()
126
  current_y = 3.0
127
  line_spacing = 0.8
128
-
129
  slides = {slides_repr}
130
-
131
  # Build each slide
132
  for idx, slide in enumerate(slides):
133
  obj = None
@@ -135,32 +84,22 @@ class GeneratedMathScene(Scene):
135
  animation = slide.get("animation", "write_left")
136
  duration = slide.get("duration", 1.0)
137
  slide_type = slide.get("type", "text")
138
-
139
  if slide_type == "title":
140
  # Wrap title text
141
  title_text = content
142
  # Use paragraph wrapping to keep multi-line titles readable
143
- lines = []
144
- if title_text:
145
- lines = []
146
- # Reuse make_wrapped_paragraph by simulating a single paragraph
147
- lines_group = make_wrapped_paragraph(title_text, highlight_color, default_font, title_size, line_spacing=0.2)
148
- obj = lines_group if len(lines_group) > 0 else Text(title_text, color=highlight_color, font=default_font, font_size=title_size)
149
- else:
150
- obj = Text("", color=highlight_color, font=default_font, font_size=title_size)
151
  if obj.width > wrap_width:
152
  obj.scale_to_fit_width(wrap_width)
153
-
154
  obj.move_to(ORIGIN)
155
  self.play(FadeIn(obj), run_time=duration * 0.8)
156
  self.wait(duration * 0.3)
157
  self.play(FadeOut(obj), run_time=duration * 0.3)
158
  continue
159
-
160
  elif slide_type == "text":
161
  # Use wrapping for normal text
162
  obj = make_wrapped_paragraph(content, default_color, default_font, text_size, line_spacing=0.25)
163
-
164
  elif slide_type == "equation":
165
  # Wrap long equations by splitting content into lines if needed
166
  # Heuristic: if content is too wide, create a multi-line TeX using \\ line breaks
@@ -173,19 +112,16 @@ class GeneratedMathScene(Scene):
173
  mid = len(parts)//2
174
  line1 = " ".join(parts[:mid])
175
  line2 = " ".join(parts[mid:])
176
- wrapped_eq = f"{{line1}} \\\\\\\\ {{line2}}"
177
  obj = MathTex(wrapped_eq, color=default_color, font_size=equation_size)
178
  else:
179
  obj = MathTex(eq_content, color=default_color, font_size=equation_size)
180
-
181
  if obj.width > wrap_width:
182
  obj.scale_to_fit_width(wrap_width)
183
-
184
  if obj:
185
  # Position and animate
186
  obj.to_edge(LEFT, buff=0.3)
187
  obj.shift(UP * (current_y - obj.height/2))
188
-
189
  obj_bottom = obj.get_bottom()[1]
190
  if obj_bottom < -3.5:
191
  scroll_amount = abs(obj_bottom - (-3.5)) + 0.3
@@ -193,7 +129,6 @@ class GeneratedMathScene(Scene):
193
  current_y += scroll_amount
194
  obj.shift(UP * scroll_amount)
195
  obj.to_edge(LEFT, buff=0.3)
196
-
197
  if animation == "write_left":
198
  self.play(Write(obj), run_time=duration)
199
  elif animation == "fade_in":
@@ -203,49 +138,41 @@ class GeneratedMathScene(Scene):
203
  self.play(obj.animate.set_color(highlight_color), run_time=duration * 0.4)
204
  else:
205
  self.play(Write(obj), run_time=duration)
206
-
207
  content_group.add(obj)
208
  # Decrease y for next item
209
  current_y -= (getattr(obj, "height", 0) + line_spacing)
210
  self.wait(0.3)
211
-
212
  if len(content_group) > 0:
213
  final_box = SurroundingRectangle(content_group[-1], color=highlight_color, buff=0.2)
214
  self.play(Create(final_box), run_time=0.8)
215
  self.wait(1.5)
216
  '''
217
-
218
  with open(script_path, 'w', encoding='utf-8') as f:
219
  f.write(manim_code)
220
-
221
  print(f"Generated script preview (first 500 chars):{manim_code[:500]}...")
222
 
 
223
  @app.route("/")
224
  def home():
225
  return "Flask Manim Video Generator is Running"
226
 
 
227
  @app.route("/generate", methods=["POST", "OPTIONS"])
228
  def generate_video():
229
  # Handle preflight
230
  if request.method == "OPTIONS":
231
  return '', 204
232
-
233
  try:
234
  # Optional: Check API key
235
  api_key = request.headers.get('X-API-KEY')
236
  if api_key and api_key != API_KEY:
237
  return jsonify({"error": "Invalid API key"}), 401
238
-
239
  # Get JSON data
240
-
241
-
242
  # Try reading raw body text
243
  raw_body = request.data.decode("utf-8").strip()
244
  data = None
245
-
246
  if not raw_body:
247
  return jsonify({"error": "No input data provided"}), 400
248
-
249
  # Try to detect if input is JSON or plain string
250
  if raw_body.startswith("{") or raw_body.startswith("["):
251
  # Likely JSON, try parsing
@@ -255,15 +182,12 @@ def generate_video():
255
  except json.JSONDecodeError:
256
  # Not valid JSON, fallback to manual parse
257
  data = None
258
-
259
  if data is None:
260
  print("⚙️ Detected raw string input (non-JSON). Parsing manually...")
261
-
262
  # Handle format like: [ [...], [...]] &&& Tamil explanation
263
  parts = raw_body.split("&&&")
264
  slides_part = parts[0].strip()
265
  extra_info = parts[1].strip() if len(parts) > 1 else ""
266
-
267
  try:
268
  slides = json.loads(slides_part)
269
  except json.JSONDecodeError as e:
@@ -272,22 +196,22 @@ def generate_video():
272
  "details": str(e),
273
  "raw_snippet": slides_part[:200]
274
  }), 400
275
-
276
- # Convert list of lists → list of dicts
277
  slides_json = []
278
  for s in slides:
279
- if len(s) >= 4:
280
- slide_type, content, animation, duration = s
281
- slides_json.append({
282
- "type": slide_type,
283
- "content": content,
284
- "animation": animation,
285
- "duration": duration
286
- })
287
-
 
288
  data = {
289
  "slides": slides_json,
290
- "language": "Tamil" if "Tamil" in extra_info else "English",
291
  "explanation": extra_info,
292
  "video_settings": {
293
  "background_color": "#0f0f23",
@@ -296,29 +220,19 @@ def generate_video():
296
  "font": "CMU Serif"
297
  }
298
  }
299
-
300
- # ✅ Final validation
301
  if "slides" not in data or not data["slides"]:
302
  return jsonify({"error": "No slides provided in request"}), 400
303
-
304
  print(f"✅ Parsed {len(data['slides'])} slides successfully.")
305
-
306
- # Validate input
307
- if "slides" not in data or not data["slides"]:
308
- return jsonify({"error": "No slides provided in request"}), 400
309
-
310
  print(f"Received request with {len(data['slides'])} slides")
311
-
312
  # Create unique temporary directory
313
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
314
  temp_work_dir = os.path.join(TEMP_DIR, f"manim_{timestamp}")
315
  os.makedirs(temp_work_dir, exist_ok=True)
316
-
317
  # Generate Manim script
318
  script_path = os.path.join(temp_work_dir, "scene.py")
319
  create_manim_script(data, script_path)
320
  print(f"Created Manim script at {script_path}")
321
-
322
  # Render video using subprocess
323
  quality = request.args.get('quality', 'l') # l=low, m=medium, h=high
324
  render_command = [
@@ -329,9 +243,7 @@ def generate_video():
329
  script_path,
330
  "GeneratedMathScene"
331
  ]
332
-
333
  print(f"Running command: {' '.join(render_command)}")
334
-
335
  result = subprocess.run(
336
  render_command,
337
  capture_output=True,
@@ -339,7 +251,6 @@ def generate_video():
339
  cwd=temp_work_dir,
340
  timeout=120
341
  )
342
-
343
  if result.returncode != 0:
344
  error_msg = result.stderr or result.stdout
345
  print(f"Manim rendering failed: {error_msg}")
@@ -347,13 +258,10 @@ def generate_video():
347
  "error": "Manim rendering failed",
348
  "details": error_msg
349
  }), 500
350
-
351
  print("Manim rendering completed successfully")
352
-
353
  # Find generated video
354
  quality_map = {'l': '480p15', 'm': '720p30', 'h': '1080p60'}
355
  video_quality = quality_map.get(quality, '480p15')
356
-
357
  video_path = os.path.join(
358
  temp_work_dir,
359
  "videos",
@@ -361,29 +269,24 @@ def generate_video():
361
  video_quality,
362
  "GeneratedMathScene.mp4"
363
  )
364
-
365
  if not os.path.exists(video_path):
366
  print(f"Video not found at expected path: {video_path}")
367
  return jsonify({
368
  "error": "Video file not found after rendering",
369
  "expected_path": video_path
370
  }), 500
371
-
372
  print(f"Video found at: {video_path}")
373
-
374
  # Copy to media directory
375
  output_filename = f"math_video_{timestamp}.mp4"
376
  output_path = os.path.join(MEDIA_DIR, output_filename)
377
  shutil.copy(video_path, output_path)
378
  print(f"Video copied to: {output_path}")
379
-
380
  # Clean up temp directory
381
  try:
382
  shutil.rmtree(temp_work_dir)
383
  print("Cleaned up temp directory")
384
  except Exception as e:
385
  print(f"Failed to clean temp dir: {e}")
386
-
387
  # Return video file as blob
388
  return send_file(
389
  output_path,
@@ -391,7 +294,6 @@ def generate_video():
391
  as_attachment=False,
392
  download_name=output_filename
393
  )
394
-
395
  except subprocess.TimeoutExpired:
396
  print("Video rendering timeout")
397
  return jsonify({"error": "Video rendering timeout (120s)"}), 504
@@ -403,6 +305,7 @@ def generate_video():
403
  "traceback": traceback.format_exc()
404
  }), 500
405
 
 
406
  if __name__ == '__main__':
407
  port = int(os.environ.get('PORT', 7860))
408
  app.run(host='0.0.0.0', port=port, debug=False)
 
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
  "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
  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
  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
  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
  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
  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
  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
  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
  "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
  "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
  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
  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
  "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
  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
  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
  "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)