devendergarg14 commited on
Commit
52795f7
·
verified ·
1 Parent(s): 3f1ca1c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -68
app.py CHANGED
@@ -5,6 +5,8 @@ import re
5
  import time
6
  import shutil
7
  import glob
 
 
8
 
9
  # ---------------------------------------------------------
10
  # 1. Helper Functions
@@ -185,18 +187,12 @@ def render_video_from_code(code, orientation, quality, timeout, preview_factor):
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())
@@ -209,80 +205,110 @@ def build_atempo_filter(factor):
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
  # ---------------------------------------------------------
275
 
276
- DEFAULT_CODE = """from manim import *
277
-
278
- class GenScene(Scene):
279
- def construct(self):
280
- c = Circle(color=BLUE, fill_opacity=0.5)
281
- self.play(Create(c))
282
- self.wait(1)
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):
@@ -310,6 +336,7 @@ with gr.Blocks(title="Manim Render API") as demo:
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():
 
5
  import time
6
  import shutil
7
  import glob
8
+ import base64
9
+ import tempfile
10
 
11
  # ---------------------------------------------------------
12
  # 1. Helper Functions
 
187
  return None, f"Rendering failed: {str(e)}", gr.Button(visible=True)
188
 
189
  # ---------------------------------------------------------
190
+ # NEW: Audio Merging Functions (API-Safe)
191
  # ---------------------------------------------------------
192
 
193
  def get_media_duration(file_path):
194
  """Uses ffprobe to get the duration of a media file."""
195
+ cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", file_path]
 
 
 
 
 
 
196
  try:
197
  result = subprocess.run(cmd, capture_output=True, text=True, check=True)
198
  return float(result.stdout.strip())
 
205
  filters = []
206
  # FFmpeg's atempo filter is limited to the range [0.5, 100.0]
207
  while factor > 100.0:
208
+ filters.append("atempo=100.0"); factor /= 100.0
 
209
  while factor < 0.5:
210
+ filters.append("atempo=0.5"); factor /= 0.5
 
 
211
  if 0.5 <= factor <= 100.0:
212
  filters.append(f"atempo={factor}")
 
213
  return ",".join(filters)
214
 
215
+ def decode_base64_to_tempfile(data_obj):
216
+ """Decodes a base64 string from a Gradio file object and saves to a temp file."""
217
+ if not data_obj or 'data' not in data_obj:
218
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
+ # Gradio sends base64 data in the format "data:video/mp4;base64,xxxx..."
221
+ header, encoded_data = data_obj['data'].split(",", 1)
222
+ file_extension = header.split('/')[1].split(';')[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
+ decoded_data = base64.b64decode(encoded_data)
225
 
226
+ with tempfile.NamedTemporaryFile(delete=False, suffix=f".{file_extension}") as tmp_file:
227
+ tmp_file.write(decoded_data)
228
+ return tmp_file.name
 
229
 
230
+ def merge_audio_to_video(video_input, audio_input):
231
+ """
232
+ Merges audio into a video. Handles both file paths (from UI) and
233
+ base64 dictionaries (from API) as inputs.
234
+ """
235
+ video_path, audio_path = None, None
236
+ temp_files_to_clean = []
237
+
238
+ try:
239
+ # --- Handle UI call (inputs are file paths) ---
240
+ if isinstance(video_input, str) and os.path.exists(video_input):
241
+ video_path = video_input
242
+ # --- Handle API call (inputs are base64 dictionaries) ---
243
+ elif isinstance(video_input, dict):
244
+ print("API call detected: Decoding video from base64.", flush=True)
245
+ video_path = decode_base64_to_tempfile(video_input)
246
+ if video_path: temp_files_to_clean.append(video_path)
247
+
248
+ if isinstance(audio_input, str) and os.path.exists(audio_input):
249
+ audio_path = audio_input
250
+ elif isinstance(audio_input, dict):
251
+ print("API call detected: Decoding audio from base64.", flush=True)
252
+ audio_path = decode_base64_to_tempfile(audio_input)
253
+ if audio_path: temp_files_to_clean.append(audio_path)
254
+
255
+ # --- Validation ---
256
+ if not video_path or not audio_path:
257
+ return None, "Error: Missing video or audio file. Please provide both."
258
+
259
+ gr.Info("Merging audio...")
260
+ video_duration = get_media_duration(video_path)
261
+ audio_duration = get_media_duration(audio_path)
262
+
263
+ if video_duration is None or audio_duration is None:
264
+ return None, "Error: Could not determine media durations."
265
+ if video_duration == 0:
266
+ return None, "Error: Input video has zero duration."
267
+
268
+ # --- Core FFmpeg Logic ---
269
+ speed_factor = audio_duration / video_duration
270
+ atempo_filter = build_atempo_filter(speed_factor)
271
+
272
+ output_dir = "temp_outputs"; os.makedirs(output_dir, exist_ok=True)
273
+ timestamp = int(time.time())
274
+ output_path = os.path.join(output_dir, f"merged_video_{timestamp}.mp4")
275
+
276
+ ffmpeg_cmd = [
277
+ "ffmpeg", "-y",
278
+ "-i", video_path,
279
+ "-i", audio_path,
280
+ "-c:v", "copy",
281
+ "-filter:a", atempo_filter,
282
+ "-map", "0:v:0",
283
+ "-map", "1:a:0",
284
+ "-shortest",
285
+ output_path
286
+ ]
287
+
288
+ process = subprocess.run(ffmpeg_cmd, capture_output=True, text=True)
289
+
290
+ if process.returncode != 0:
291
+ error_message = f"FFmpeg Error:\n{process.stderr}"
292
+ print(error_message, flush=True)
293
+ return None, error_message
294
+
295
+ print(f"✅ Audio merged successfully: {output_path}", flush=True)
296
+ return output_path, "✅ Audio merged successfully!"
297
+
298
+ finally:
299
+ # --- Cleanup ---
300
+ print(f"Cleaning up {len(temp_files_to_clean)} temporary files...", flush=True)
301
+ for f in temp_files_to_clean:
302
+ try:
303
+ os.remove(f)
304
+ except OSError as e:
305
+ print(f"Error removing temp file {f}: {e}", flush=True)
306
 
307
  # ---------------------------------------------------------
308
  # 3. Gradio Interface
309
  # ---------------------------------------------------------
310
 
311
+ with gr.Blocks(title="Manim Render & Audio Tool") as demo:
 
 
 
 
 
 
 
 
 
312
  with gr.Tab("🎬 Manim Video Renderer"):
313
  with gr.Row():
314
  with gr.Column(scale=1):
 
336
  with gr.Row():
337
  with gr.Column():
338
  video_input_audio_tab = gr.Video(label="Input Video (MP4)")
339
+ # For the API, this audio component will accept base64 data
340
  audio_input_audio_tab = gr.Audio(label="Input Audio", type="filepath")
341
  merge_audio_btn = gr.Button("Merge Audio", variant="primary")
342
  with gr.Column():