devendergarg14 commited on
Commit
e6d9ed9
·
verified ·
1 Parent(s): 12e6c81

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +124 -28
app.py CHANGED
@@ -106,28 +106,22 @@ def run_manim(code_str, orientation, quality, timeout):
106
  full_logs = ""
107
 
108
  try:
109
- # check=False prevents Python from raising exception on error code automatically.
110
- # We handle returncode manually below.
111
  process = subprocess.run(cmd, capture_output=True, timeout=timeout_sec, check=False)
112
 
113
  stdout_log = process.stdout.decode('utf-8', 'ignore')
114
  stderr_log = process.stderr.decode('utf-8', 'ignore')
115
  full_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
116
 
117
- # --- STANDARD ERROR CHECK ---
118
  if process.returncode != 0:
119
  print(f"❌ Render Failed (Process Error). Return Code: {process.returncode}", flush=True)
120
- # We do NOT stitch here. We just fall through to see if a file exists (unlikely).
121
 
122
  except subprocess.TimeoutExpired as e:
123
- # --- TIMEOUT LOGIC ONLY ---
124
  print(f"⌛ Render timed out after {timeout_sec} seconds. Attempting recovery...", flush=True)
125
  stdout_log = e.stdout.decode('utf-8', 'ignore') if e.stdout else ""
126
  stderr_log = e.stderr.decode('utf-8', 'ignore') if e.stderr else ""
127
  timeout_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
128
  full_logs = timeout_logs
129
 
130
- # 1. Parse logs for directory
131
  combined_log = stdout_log + "\n" + stderr_log
132
  path_matches = re.findall(r"movie file written in\s*'([^']+?)'", combined_log, flags=re.DOTALL)
133
 
@@ -135,7 +129,6 @@ def run_manim(code_str, orientation, quality, timeout):
135
  partial_files_dir = os.path.dirname("".join(path_matches[-1].split()))
136
 
137
  if os.path.exists(partial_files_dir):
138
- # 2. Find partial chunks
139
  partial_files = sorted(glob.glob(os.path.join(partial_files_dir, 'uncached_*.mp4')),
140
  key=lambda f: int(re.search(r'(\d+)\.mp4$', f).group(1)))
141
 
@@ -146,7 +139,6 @@ def run_manim(code_str, orientation, quality, timeout):
146
  for pf in partial_files:
147
  f.write(f"file '{os.path.abspath(pf)}'\n")
148
 
149
- # 3. Stitch
150
  combined_video_path = os.path.join(os.path.dirname(partial_files_dir), f"combined_partial_{timestamp}.mp4")
151
  ffmpeg_cmd = ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file_path, "-c", "copy", combined_video_path]
152
 
@@ -157,10 +149,7 @@ def run_manim(code_str, orientation, quality, timeout):
157
  return combined_video_path, f"⚠️ WARNING: Render Timed Out. Recovered partial video.\n\n{timeout_logs}", True
158
 
159
  print("❌ Recovery failed or no partial files found.", flush=True)
160
- # End of Timeout Logic. Falls through to final check.
161
 
162
- # --- FINAL FILE CHECK ---
163
- # Only returns success if the actual complete file exists.
164
  media_video_base = os.path.join("media", "videos", "scene")
165
  if os.path.exists(media_video_base):
166
  for root, _, files in os.walk(media_video_base):
@@ -195,6 +184,91 @@ def render_video_from_code(code, orientation, quality, timeout, preview_factor):
195
  except Exception as e:
196
  return None, f"Rendering failed: {str(e)}", gr.Button(visible=True)
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  # ---------------------------------------------------------
199
  # 3. Gradio Interface
200
  # ---------------------------------------------------------
@@ -209,23 +283,45 @@ class GenScene(Scene):
209
  """
210
 
211
  with gr.Blocks(title="Manim Render API") as demo:
212
- code_input = gr.Code(label="Python Code", language="python", value=DEFAULT_CODE, visible=True)
213
- orientation_opt = gr.Radio(choices=["Landscape (16:9)", "Portrait (9:16)"], value="Portrait (9:16)", label="Orientation", visible=True)
214
- quality_opt = gr.Dropdown(choices=["Preview (360p)", "480p", "720p", "1080p", "4k"], value="Preview (360p)", label="Quality", visible=True)
215
- timeout_input = gr.Number(label="Render Timeout (seconds)", value=60, visible=True)
216
- preview_speed_factor_input = gr.Number(label="Preview Speed Factor", value=0.5, visible=True)
217
-
218
- video_output = gr.Video(label="Result")
219
- status_output = gr.Textbox(label="Status/Logs")
220
- fix_btn_output = gr.Button("Fix Error & Re-render", variant="stop", visible=True)
221
- render_btn = gr.Button("Render")
222
-
223
- render_btn.click(
224
- fn=render_video_from_code,
225
- inputs=[code_input, orientation_opt, quality_opt, timeout_input, preview_speed_factor_input],
226
- outputs=[video_output, status_output, fix_btn_output],
227
- api_name="render"
228
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
  if __name__ == "__main__":
231
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
106
  full_logs = ""
107
 
108
  try:
 
 
109
  process = subprocess.run(cmd, capture_output=True, timeout=timeout_sec, check=False)
110
 
111
  stdout_log = process.stdout.decode('utf-8', 'ignore')
112
  stderr_log = process.stderr.decode('utf-8', 'ignore')
113
  full_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
114
 
 
115
  if process.returncode != 0:
116
  print(f"❌ Render Failed (Process Error). Return Code: {process.returncode}", flush=True)
 
117
 
118
  except subprocess.TimeoutExpired as e:
 
119
  print(f"⌛ Render timed out after {timeout_sec} seconds. Attempting recovery...", flush=True)
120
  stdout_log = e.stdout.decode('utf-8', 'ignore') if e.stdout else ""
121
  stderr_log = e.stderr.decode('utf-8', 'ignore') if e.stderr else ""
122
  timeout_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
123
  full_logs = timeout_logs
124
 
 
125
  combined_log = stdout_log + "\n" + stderr_log
126
  path_matches = re.findall(r"movie file written in\s*'([^']+?)'", combined_log, flags=re.DOTALL)
127
 
 
129
  partial_files_dir = os.path.dirname("".join(path_matches[-1].split()))
130
 
131
  if os.path.exists(partial_files_dir):
 
132
  partial_files = sorted(glob.glob(os.path.join(partial_files_dir, 'uncached_*.mp4')),
133
  key=lambda f: int(re.search(r'(\d+)\.mp4$', f).group(1)))
134
 
 
139
  for pf in partial_files:
140
  f.write(f"file '{os.path.abspath(pf)}'\n")
141
 
 
142
  combined_video_path = os.path.join(os.path.dirname(partial_files_dir), f"combined_partial_{timestamp}.mp4")
143
  ffmpeg_cmd = ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file_path, "-c", "copy", combined_video_path]
144
 
 
149
  return combined_video_path, f"⚠️ WARNING: Render Timed Out. Recovered partial video.\n\n{timeout_logs}", True
150
 
151
  print("❌ Recovery failed or no partial files found.", flush=True)
 
152
 
 
 
153
  media_video_base = os.path.join("media", "videos", "scene")
154
  if os.path.exists(media_video_base):
155
  for root, _, files in os.walk(media_video_base):
 
184
  except Exception as e:
185
  return None, f"Rendering failed: {str(e)}", gr.Button(visible=True)
186
 
187
+ # ---------------------------------------------------------
188
+ # NEW: Audio Merging Functions
189
+ # ---------------------------------------------------------
190
+
191
+ def get_media_duration(file_path):
192
+ """Uses ffprobe to get the duration of a media file."""
193
+ cmd = [
194
+ "ffprobe",
195
+ "-v", "error",
196
+ "-show_entries", "format=duration",
197
+ "-of", "default=noprint_wrappers=1:nokey=1",
198
+ file_path
199
+ ]
200
+ try:
201
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
202
+ return float(result.stdout.strip())
203
+ except (subprocess.CalledProcessError, FileNotFoundError):
204
+ print(f"⚠️ Could not get duration for {file_path}. Is ffprobe installed?", flush=True)
205
+ return None
206
+
207
+ def build_atempo_filter(factor):
208
+ """Builds a chained atempo filter for FFmpeg to handle extreme speed changes."""
209
+ filters = []
210
+ # FFmpeg's atempo filter is limited to the range [0.5, 100.0]
211
+ while factor > 100.0:
212
+ filters.append("atempo=100.0")
213
+ factor /= 100.0
214
+ while factor < 0.5:
215
+ filters.append("atempo=0.5")
216
+ factor /= 0.5
217
+
218
+ if 0.5 <= factor <= 100.0:
219
+ filters.append(f"atempo={factor}")
220
+
221
+ return ",".join(filters)
222
+
223
+ def merge_audio_to_video(video_path, audio_path):
224
+ """Merges audio into a video, stretching the audio to match the video's duration."""
225
+ if not video_path or not audio_path:
226
+ return None, "Error: Please provide both a video and an audio file."
227
+
228
+ gr.Info("Merging audio...")
229
+ video_duration = get_media_duration(video_path)
230
+ audio_duration = get_media_duration(audio_path)
231
+
232
+ if video_duration is None or audio_duration is None:
233
+ return None, "Error: Could not determine media durations. Check server logs."
234
+
235
+ if video_duration == 0:
236
+ return None, "Error: Input video has zero duration."
237
+
238
+ speed_factor = audio_duration / video_duration
239
+ atempo_filter = build_atempo_filter(speed_factor)
240
+
241
+ print(f"Video Duration: {video_duration}s, Audio Duration: {audio_duration}s, Speed Factor: {speed_factor}", flush=True)
242
+ print(f"Applying FFmpeg atempo filter: '{atempo_filter}'", flush=True)
243
+
244
+ output_dir = "temp_outputs"
245
+ os.makedirs(output_dir, exist_ok=True)
246
+ timestamp = int(time.time())
247
+ output_path = os.path.join(output_dir, f"merged_video_{timestamp}.mp4")
248
+
249
+ ffmpeg_cmd = [
250
+ "ffmpeg",
251
+ "-y", # Overwrite output file if it exists
252
+ "-i", video_path, # Input video
253
+ "-i", audio_path, # Input audio
254
+ "-c:v", "copy", # Copy video stream (no re-encoding, preserves quality)
255
+ "-filter:a", atempo_filter, # Apply speed change to audio
256
+ "-map", "0:v:0", # Select video from the first input
257
+ "-map", "1:a:0", # Select audio from the second input
258
+ "-shortest", # Ensure output duration matches the shortest stream (video)
259
+ output_path
260
+ ]
261
+
262
+ process = subprocess.run(ffmpeg_cmd, capture_output=True, text=True)
263
+
264
+ if process.returncode != 0:
265
+ error_message = f"FFmpeg Error:\n{process.stderr}"
266
+ print(error_message, flush=True)
267
+ return None, error_message
268
+
269
+ print(f"✅ Audio merged successfully: {output_path}", flush=True)
270
+ return output_path, "✅ Audio merged successfully!"
271
+
272
  # ---------------------------------------------------------
273
  # 3. Gradio Interface
274
  # ---------------------------------------------------------
 
283
  """
284
 
285
  with gr.Blocks(title="Manim Render API") as demo:
286
+ with gr.Tab("🎬 Manim Video Renderer"):
287
+ with gr.Row():
288
+ with gr.Column(scale=1):
289
+ code_input = gr.Code(label="Python Code", language="python", value=DEFAULT_CODE, visible=True)
290
+ orientation_opt = gr.Radio(choices=["Landscape (16:9)", "Portrait (9:16)"], value="Portrait (9:16)", label="Orientation", visible=True)
291
+ quality_opt = gr.Dropdown(choices=["Preview (360p)", "480p", "720p", "1080p", "4k"], value="Preview (360p)", label="Quality", visible=True)
292
+ timeout_input = gr.Number(label="Render Timeout (seconds)", value=60, visible=True)
293
+ preview_speed_factor_input = gr.Number(label="Preview Speed Factor", value=0.5, visible=True)
294
+ render_btn = gr.Button("Render", variant="primary")
295
+ with gr.Column(scale=1):
296
+ video_output = gr.Video(label="Result")
297
+ status_output = gr.Textbox(label="Status/Logs")
298
+ fix_btn_output = gr.Button("Fix Error & Re-render", variant="stop", visible=True)
299
+
300
+ render_btn.click(
301
+ fn=render_video_from_code,
302
+ inputs=[code_input, orientation_opt, quality_opt, timeout_input, preview_speed_factor_input],
303
+ outputs=[video_output, status_output, fix_btn_output],
304
+ api_name="render"
305
+ )
306
+
307
+ with gr.Tab("🎤 Add Audio to Video"):
308
+ gr.Markdown("## Merge Audio into Video")
309
+ gr.Markdown("Upload a video and an audio file. The audio will be automatically stretched or compressed to match the video's length, preserving original video quality.")
310
+ with gr.Row():
311
+ with gr.Column():
312
+ video_input_audio_tab = gr.Video(label="Input Video (MP4)")
313
+ audio_input_audio_tab = gr.Audio(label="Input Audio", type="filepath")
314
+ merge_audio_btn = gr.Button("Merge Audio", variant="primary")
315
+ with gr.Column():
316
+ video_output_audio_tab = gr.Video(label="Merged Video")
317
+ status_audio_tab = gr.Textbox(label="Status")
318
+
319
+ merge_audio_btn.click(
320
+ fn=merge_audio_to_video,
321
+ inputs=[video_input_audio_tab, audio_input_audio_tab],
322
+ outputs=[video_output_audio_tab, status_audio_tab],
323
+ api_name="add_audio"
324
+ )
325
 
326
  if __name__ == "__main__":
327
  demo.launch(server_name="0.0.0.0", server_port=7860)