sam12345324 commited on
Commit
939ca97
·
verified ·
1 Parent(s): 156e7de

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -467
app.py CHANGED
@@ -1,487 +1,118 @@
 
1
  import gradio as gr
2
- import tempfile
 
 
3
  import os
4
- import re
5
- from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips
6
- import numpy as np
7
- import logging
8
- import sys
9
- import traceback
10
- import socket
11
  import shutil
 
 
 
12
 
13
- # Set up logging to debug issues
14
- logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler(sys.stdout)])
15
  logger = logging.getLogger(__name__)
16
 
17
- # --- Functions ---
18
-
19
- def check_port(port):
20
- """
21
- Check if a port is available.
22
- Returns True if the port is free, False if it's in use.
23
- """
24
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
25
- try:
26
- s.bind(("0.0.0.0", port))
27
- return True
28
- except socket.error:
29
- return False
30
-
31
- def sort_files_by_index(file_list, prefix_pattern):
32
- """
33
- Sort files based on their numerical index in the filename.
34
- Args:
35
- file_list: List of file paths (e.g., ['file2.mp4', 'file1.mp4', 'file3.mp4'])
36
- prefix_pattern: Regex pattern to match the prefix and number (e.g., r'file(\d+)\.mp4')
37
- Returns:
38
- Sorted list of file paths
39
- """
40
- if not file_list:
41
- return []
42
-
43
- def extract_index(filename):
44
- match = re.match(prefix_pattern, os.path.basename(filename))
45
- if match:
46
- return int(match.group(1))
47
- return float('inf') # Invalid filenames go to the end
48
-
49
- sorted_files = sorted(file_list, key=extract_index)
50
- logger.info(f"Sorted files: {sorted_files}")
51
- return sorted_files
52
-
53
- def get_indices_string(file_list, prefix_pattern):
54
- """
55
- Extract the numerical indices from filenames and return as a string.
56
- Args:
57
- file_list: List of file paths
58
- prefix_pattern: Regex pattern to match the prefix and number
59
- Returns:
60
- String of indices (e.g., '123' for ['file1.mp4', 'file2.mp4', 'file3.mp4'])
61
- """
62
- if not file_list:
63
- return ""
64
- indices = []
65
- for filename in file_list:
66
- match = re.match(prefix_pattern, os.path.basename(filename))
67
- if match:
68
- indices.append(match.group(1))
69
- return "".join(indices)
70
-
71
- def split_files_by_extension(all_files):
72
- """
73
- Split a list of files into videos (.mp4) and audios (.mp3, .wav) based on extensions.
74
- Args:
75
- all_files: List of file paths (e.g., ['file1.mp4', 'audio1.mp3', 'file2.mp4', 'audio2.mp3'])
76
- Returns:
77
- Tuple of (video_files, audio_files)
78
- """
79
- video_files = []
80
- audio_files = []
81
- for file_path in all_files:
82
- filename = os.path.basename(file_path).lower()
83
- if filename.endswith('.mp4'):
84
- video_files.append(file_path)
85
- elif filename.endswith(('.mp3', '.wav')):
86
- audio_files.append(file_path)
87
- else:
88
- logger.warning(f"Unsupported file extension for {file_path}; skipping")
89
- return video_files, audio_files
90
-
91
- def trim_silence(audio_clip, threshold=0.005):
92
- """
93
- Trim silence from the start and end of an audio clip.
94
- Args:
95
- audio_clip: AudioFileClip object
96
- threshold: Amplitude threshold below which audio is considered silent
97
- Returns:
98
- Trimmed AudioFileClip
99
- """
100
- try:
101
- # Get audio data as numpy array
102
- samples = audio_clip.to_soundarray(fps=44100)
103
- # Compute amplitude (RMS)
104
- if len(samples.shape) > 1: # Stereo audio
105
- amplitudes = np.sqrt(np.mean(samples**2, axis=1))
106
- else: # Mono audio
107
- amplitudes = np.sqrt(samples**2)
108
-
109
- # Find non-silent regions
110
- non_silent = amplitudes > threshold
111
- if not np.any(non_silent):
112
- logger.warning("Audio clip is completely silent; returning original clip")
113
- return audio_clip
114
-
115
- # Find start and end indices
116
- start_idx = np.argmax(non_silent)
117
- end_idx = len(non_silent) - np.argmax(non_silent[::-1])
118
-
119
- # Convert indices to time (seconds)
120
- start_time = start_idx / 44100
121
- end_time = end_idx / 44100
122
-
123
- # Ensure the trimmed duration is reasonable
124
- if end_time <= start_time:
125
- logger.warning("Trimmed duration is zero or negative; returning original clip")
126
- return audio_clip
127
-
128
- # Trim the audio
129
- trimmed_audio = audio_clip.subclip(start_time, end_time)
130
- logger.info(f"Trimmed audio from {start_time:.2f}s to {end_time:.2f}s (original duration: {audio_clip.duration:.2f}s)")
131
- return trimmed_audio
132
- except Exception as e:
133
- logger.error(f"Error trimming silence: {str(e)}")
134
- return audio_clip
135
-
136
- def merge_videos_and_audios(video_files=None, audio_files=None, orig_vol=1.0, music_vol=0.5, temp_dir=None):
137
- """
138
- Merge multiple video clips and/or audio clips based on inputs provided.
139
- - If only video_files: Merge videos, retaining their original audio.
140
- - If only audio_files: Merge audio files into a single audio file.
141
- - If both: Merge videos and overlay the concatenated audio.
142
- Files are sorted by numerical index in their filenames (e.g., file1.mp4, file2.mp4).
143
- Args:
144
- video_files: List of video file paths (optional)
145
- audio_files: List of audio file paths (optional)
146
- orig_vol: Volume for original video audio (0.0 to 1.0)
147
- music_vol: Volume for background audio (0.0 to 1.0)
148
- temp_dir: Temporary directory to clean up (optional)
149
- Returns:
150
- Path to the merged file (video or audio) or error message.
151
- """
152
- try:
153
- # Sort files by numerical index
154
- video_files = sort_files_by_index(video_files, r'file(\d+)\.mp4')
155
- audio_files = sort_files_by_index(audio_files, r'audio(\d+)\.(mp3|wav)')
156
-
157
- # Get indices for output naming
158
- video_indices = get_indices_string(video_files, r'file(\d+)\.mp4')
159
- audio_indices = get_indices_string(audio_files, r'audio(\d+)\.(mp3|wav)')
160
-
161
- # Ensure at least two files are provided (videos, audios, or combination)
162
- video_count = len(video_files) if video_files else 0
163
- audio_count = len(audio_files) if audio_files else 0
164
- total_files = video_count + audio_count
165
-
166
- logger.info(f"Starting merge with {video_count} video files and {audio_count} audio files")
167
-
168
- if total_files < 2:
169
- error_msg = "Error: Please upload at least 2 files total (videos, audios, or a combination)."
170
- logger.error(error_msg)
171
- return error_msg
172
-
173
- # Create a temporary output path (use provided temp_dir if available)
174
- if temp_dir is None:
175
- temp_dir = tempfile.mkdtemp()
176
- output_dir = temp_dir
177
-
178
- # Case 1: Audio only
179
- if audio_count >= 2 and video_count == 0:
180
- output_filename = f"combined_audio_{audio_indices}.mp3"
181
- output_path = os.path.join(output_dir, output_filename)
182
- logger.info("Merging audio files only")
183
-
184
- # Load, normalize, and trim audio clips
185
- audio_clips = []
186
- for audio in audio_files:
187
- clip = AudioFileClip(audio).set_fps(44100) # Normalize sample rate
188
- clip = clip.to_stereo() if clip.nchannels == 1 else clip # Convert mono to stereo
189
- logger.info(f"Original audio duration for {audio}: {clip.duration:.2f}s, channels: {clip.nchannels}")
190
- trimmed_clip = trim_silence(clip)
191
- if trimmed_clip.duration > 0:
192
- audio_clips.append(trimmed_clip)
193
- else:
194
- logger.warning(f"Skipping audio file {audio} as it has zero duration after trimming")
195
-
196
- # Check if we have enough clips to concatenate
197
- if len(audio_clips) < 2:
198
- error_msg = "Error: Fewer than 2 audio clips available after trimming (clips may be silent or too short)."
199
- logger.error(error_msg)
200
- for clip in audio_clips:
201
- clip.close()
202
- return error_msg
203
-
204
- # Log durations after trimming
205
- for i, clip in enumerate(audio_clips):
206
- logger.info(f"Trimmed audio {i+1} duration: {clip.duration:.2f}s, channels: {clip.nchannels}")
207
-
208
- # Concatenate audio clips
209
- logger.info(f"Attempting to concatenate {len(audio_clips)} audio clips")
210
- final_audio_clip = concatenate_audioclips(audio_clips)
211
- logger.info(f"Concatenated audio duration: {final_audio_clip.duration:.2f}s, channels: {final_audio_clip.nchannels}")
212
-
213
- # Verify concatenated duration
214
- expected_duration = sum(clip.duration for clip in audio_clips)
215
- if abs(final_audio_clip.duration - expected_duration) > 0.1:
216
- logger.warning(f"Concatenated duration ({final_audio_clip.duration:.2f}s) does not match expected duration ({expected_duration:.2f}s)")
217
-
218
- # Write the final audio
219
- logger.info(f"Writing output audio to {output_path}")
220
- final_audio_clip.write_audiofile(output_path, codec="mp3")
221
-
222
- # Close resources
223
- final_audio_clip.close()
224
- for clip in audio_clips:
225
- clip.close()
226
-
227
- logger.info("Audio merge completed successfully")
228
- return output_path
229
-
230
- # Case 2: Video only or Video with Audio
231
- if audio_indices:
232
- output_filename = f"combined_video_{video_indices}_with_audio_{audio_indices}.mp4"
233
- else:
234
- output_filename = f"combined_video_{video_indices}.mp4"
235
- output_path = os.path.join(output_dir, output_filename)
236
-
237
- # Load and concatenate video clips
238
- video_clips = [VideoFileClip(video) for video in video_files]
239
- final_video_clip = concatenate_videoclips(video_clips, method='compose')
240
-
241
- # Determine final video duration
242
- video_duration = final_video_clip.duration or sum(clip.duration for clip in video_clips)
243
- logger.info(f"Total video duration: {video_duration:.2f}s")
244
-
245
- # Handle audio (if provided)
246
- if audio_files:
247
- logger.info("Processing audio files")
248
- # Load, normalize, and trim audio clips
249
- audio_clips = []
250
- for audio in audio_files:
251
- clip = AudioFileClip(audio).set_fps(44100) # Normalize sample rate
252
- clip = clip.to_stereo() if clip.nchannels == 1 else clip # Convert mono to stereo
253
- logger.info(f"Original audio duration for {audio}: {clip.duration:.2f}s, channels: {clip.nchannels}")
254
- trimmed_clip = trim_silence(clip)
255
- if trimmed_clip.duration > 0:
256
- audio_clips.append(trimmed_clip)
257
- else:
258
- logger.warning(f"Skipping audio file {audio} as it has zero duration after trimming")
259
-
260
- # Log durations after trimming
261
- for i, clip in enumerate(audio_clips):
262
- logger.info(f"Trimmed audio {i+1} duration: {clip.duration:.2f}s, channels: {clip.nchannels}")
263
-
264
- if not audio_clips:
265
- logger.warning("No valid audio clips after trimming; using original video audio only")
266
- final_audio = final_video_clip.audio.volumex(orig_vol) if final_video_clip.audio else None
267
- else:
268
- # Concatenate audio clips
269
- logger.info(f"Attempting to concatenate {len(audio_clips)} audio clips")
270
- concatenated_audio = concatenate_audioclips(audio_clips)
271
- logger.info(f"Concatenated audio duration: {concatenated_audio.duration:.2f}s, channels: {concatenated_audio.nchannels}")
272
-
273
- # Verify concatenated duration
274
- expected_duration = sum(clip.duration for clip in audio_clips)
275
- if abs(concatenated_audio.duration - expected_duration) > 0.1:
276
- logger.warning(f"Concatenated duration ({concatenated_audio.duration:.2f}s) does not match expected duration ({expected_duration:.2f}s)")
277
-
278
- # Adjust concatenated audio duration to match video duration (trim or loop)
279
- if concatenated_audio.duration > video_duration:
280
- concatenated_audio = concatenated_audio.subclip(0, video_duration)
281
- logger.info(f"Trimmed concatenated audio to match video duration: {concatenated_audio.duration:.2f}s")
282
- elif concatenated_audio.duration < video_duration:
283
- # Loop the audio to match video duration
284
- concatenated_audio = concatenated_audio.fx(lambda clip: clip.loop(duration=video_duration))
285
- logger.info(f"Looped concatenated audio to match video duration: {concatenated_audio.duration:.2f}s")
286
-
287
- # Apply volume to concatenated audio
288
- concatenated_audio = concatenated_audio.volumex(music_vol)
289
-
290
- # Get original video audio (if any) and apply volume
291
- original_audio = final_video_clip.audio.volumex(orig_vol) if final_video_clip.audio else None
292
-
293
- # Composite the audio tracks
294
- if original_audio:
295
- final_audio = CompositeAudioClip([original_audio, concatenated_audio])
296
- else:
297
- final_audio = concatenated_audio
298
- else:
299
- logger.info("No audio files provided; using original video audio if available")
300
- # If no audio files provided, retain original video audio (if any)
301
- final_audio = final_video_clip.audio.volumex(orig_vol) if final_video_clip.audio else None
302
-
303
- # Set the audio to the final video
304
- final_video_clip = final_video_clip.set_audio(final_audio)
305
-
306
- # Write the final video
307
- logger.info(f"Writing output video to {output_path}")
308
- final_video_clip.write_videofile(output_path, codec="libx264", fps=30, audio_codec="aac", ffmpeg_params=["-preset", "fast"])
309
-
310
- # Close resources
311
- final_video_clip.close()
312
- for clip in video_clips:
313
- clip.close()
314
- if audio_files and audio_clips:
315
- for clip in audio_clips:
316
- clip.close()
317
- if 'concatenated_audio' in locals():
318
- concatenated_audio.close()
319
-
320
- logger.info("Video merge completed successfully")
321
- return output_path
322
- except Exception as e:
323
- error_msg = f"Error during merging: {str(e)}\n{traceback.format_exc()}"
324
- logger.error(error_msg)
325
- return error_msg
326
- finally:
327
- # Clean up temporary directory if it was created in this function
328
- if temp_dir and os.path.exists(temp_dir):
329
- try:
330
- shutil.rmtree(temp_dir)
331
- logger.info(f"Cleaned up temporary directory: {temp_dir}")
332
- except Exception as e:
333
- logger.warning(f"Failed to clean up temporary directory {temp_dir}: {str(e)}")
334
-
335
- # --- Gradio App Using Blocks ---
336
-
337
- def gradio_merge_files(file_binaries, orig_vol, music_vol, file_names=None):
338
- """
339
- Gradio endpoint to merge videos and/or audio from binary file uploads.
340
- Args:
341
- file_binaries: List of binary data (bytes objects)
342
- orig_vol: Volume for original video audio (0.0 to 1.0)
343
- music_vol: Volume for background audio (0.0 to 1.0)
344
- file_names: List of original filenames (passed separately for API calls)
345
- Returns:
346
- Path to the merged file (video or audio) or error message
347
- """
348
  try:
349
- logger.info(f"Received {len(file_binaries) if file_binaries else 0} binary files")
350
-
351
- if not file_binaries or len(file_binaries) < 2:
352
- error_msg = "Error: Please upload at least 2 files."
353
- logger.error(error_msg)
354
- return error_msg, None
355
 
356
- # Create a temporary directory to store uploaded files
357
  temp_dir = tempfile.mkdtemp()
358
- all_files = []
359
-
360
- # When called via the UI, Gradio provides binary data but not filenames directly.
361
- # When called via API (e.g., n8n), we need to get filenames from the request.
362
- # For UI testing, infer filenames based on index; for API, use provided file_names.
363
- if file_names is None:
364
- # Fallback for UI: assign temporary filenames (not ideal for production)
365
- file_names = []
366
- for idx, binary in enumerate(file_binaries):
367
- ext = ".mp4" if idx % 2 == 0 else ".mp3" # Dummy assignment for UI testing
368
- file_names.append(f"temp_file_{idx}{ext}")
369
- logger.warning("No file names provided; using temporary names for UI testing. For API calls, pass file_names.")
370
-
371
- if len(file_names) != len(file_binaries):
372
- error_msg = f"Error: Mismatch between file binaries ({len(file_binaries)}) and file names ({len(file_names)})."
373
- logger.error(error_msg)
374
- return error_msg, None
375
-
376
- # Save each binary file to the temporary directory
377
- for binary, filename in zip(file_binaries, file_names):
378
- if binary is None:
379
- logger.warning(f"Skipping None binary data for file {filename}")
380
- continue
381
- # Validate filename
382
- original_filename = os.path.basename(filename)
383
- if not re.match(r'file\d+\.mp4', original_filename, re.IGNORECASE) and \
384
- not re.match(r'audio\d+\.(mp3|wav)', original_filename, re.IGNORECASE):
385
- logger.warning(f"Filename {original_filename} does not match expected pattern; skipping")
386
- continue
387
- # Create a temporary file path
388
- temp_file_path = os.path.join(temp_dir, original_filename)
389
- # Write the binary data to the temporary file
390
- with open(temp_file_path, 'wb') as temp_file:
391
- temp_file.write(binary)
392
- all_files.append(temp_file_path)
393
- logger.info(f"Saved uploaded file to {temp_file_path}")
394
-
395
- if len(all_files) < 2:
396
- error_msg = "Error: Fewer than 2 valid files after filtering."
397
- logger.error(error_msg)
398
- return error_msg, None
399
-
400
- # Split files into videos and audios based on extensions
401
- video_files, audio_files = split_files_by_extension(all_files)
402
- logger.info(f"Identified {len(video_files)} video files: {video_files}")
403
- logger.info(f"Identified {len(audio_files)} audio files: {audio_files}")
404
-
405
- result = merge_videos_and_audios(
406
  video_files=video_files,
407
  audio_files=audio_files,
408
  orig_vol=orig_vol,
409
  music_vol=music_vol,
410
- temp_dir=temp_dir
411
  )
412
-
413
- if isinstance(result, str) and result.startswith("Error"):
414
- logger.error(result)
415
- return result, None
416
- else:
417
- logger.info(f"Merge successful. Output saved at: {result}")
418
- # Return appropriate output based on file type
419
- if result.endswith(".mp3"):
420
- return None, result # Audio output
421
- else:
422
- return result, None # Video output
423
- except Exception as e:
424
- error_msg = f"Error processing files: {str(e)}\n{traceback.format_exc()}"
425
- logger.error(error_msg)
426
- return error_msg, None
427
- finally:
428
- # Cleanup is handled in merge_videos_and_audios
429
- pass
430
 
431
- # --- Main Execution ---
 
 
432
 
433
- if __name__ == "__main__":
434
- logger.info(f"Environment: {os.environ.get('HUGGINGFACE_SPACES', 'Not in HF Spaces')}")
435
- logger.info(f"Arguments: {sys.argv}")
436
-
437
- # Check ports in a wider range
438
- default_port = 7860
439
- ports_to_try = list(range(default_port, default_port + 11)) # 7860 to 7870
440
-
441
- selected_port = None
442
- for port in ports_to_try:
443
- logger.info(f"Checking if port {port} is available")
444
- if check_port(port):
445
- logger.info(f"Port {port} is available")
446
- selected_port = port
447
- break
448
- else:
449
- logger.warning(f"Port {port} is already in use")
450
-
451
- if selected_port is None:
452
- logger.error("No available ports found in range 7860-7870")
453
- sys.exit(1)
454
 
455
- logger.info("Launching Gradio Blocks interface")
456
-
457
- with gr.Blocks(title="Video and Audio Merger API") as app:
458
- gr.Markdown("## Video and Audio Merger API")
459
- gr.Markdown("Upload at least 2 files total (videos, audios, or a combination) to merge them.")
460
- gr.Markdown("For API usage, send binary files via multipart/form-data. Name videos as file1.mp4, file2.mp4, etc., and audios as audio1.mp3, audio2.mp3, etc.")
461
-
462
- with gr.Row():
463
- file_input = gr.File(label="Upload Files (Videos: .mp4, Audios: .mp3/.wav)", type="binary", file_count="multiple")
464
-
465
- with gr.Row():
466
- orig_vol_input = gr.Slider(minimum=0.0, maximum=1.0, value=1.0, step=0.05, label="Original Video Audio Volume")
467
- music_vol_input = gr.Slider(minimum=0.0, maximum=1.0, value=0.5, step=0.05, label="Background Audio Volume")
468
-
469
- output_video = gr.Video(label="Merged Video (if videos provided)")
470
- output_audio = gr.Audio(label="Merged Audio (if only audios provided)")
471
- merge_button = gr.Button("Merge Files")
472
-
473
- merge_button.click(
474
- fn=gradio_merge_files,
475
- inputs=[file_input, orig_vol_input, music_vol_input],
476
- outputs=[output_video, output_audio]
477
- )
478
 
479
- try:
480
- logger.info(f"Attempting to launch Gradio app on port {selected_port}")
481
- app.queue(api_open=True)
482
- app.launch(server_port=selected_port, share=True)
483
- logger.info(f"Gradio app launched successfully on port {app.server_port}")
484
  except Exception as e:
485
- error_msg = f"Failed to launch Gradio interface: {str(e)}\n{traceback.format_exc()}"
486
  logger.error(error_msg)
487
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
  import gradio as gr
3
+ from fastapi import FastAPI, File, UploadFile, Form
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ import uvicorn
6
  import os
7
+ import tempfile
 
 
 
 
 
 
8
  import shutil
9
+ import traceback
10
+ import logging
11
+ from merge_utils import merge_videos_and_audios # move your merge logic to merge_utils.py for clean code!
12
 
13
+ # Setup logging
14
+ logging.basicConfig(level=logging.INFO)
15
  logger = logging.getLogger(__name__)
16
 
17
+ # --- FASTAPI SETUP ---
18
+ fastapi_app = FastAPI(title="Video & Audio Merger API")
19
+
20
+ # CORS for n8n to call the API
21
+ fastapi_app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=["*"], # You can restrict to your domain if needed
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+ # FastAPI endpoint to accept binary files from n8n
30
+ @fastapi_app.post("/merge")
31
+ async def merge_endpoint(
32
+ files: list[UploadFile] = File(...),
33
+ orig_vol: float = Form(1.0),
34
+ music_vol: float = Form(0.5),
35
+ ):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  try:
37
+ logger.info(f"Received {len(files)} files from n8n")
 
 
 
 
 
38
 
39
+ # Save files to temp dir
40
  temp_dir = tempfile.mkdtemp()
41
+ saved_files = []
42
+
43
+ for upload_file in files:
44
+ filename = os.path.basename(upload_file.filename)
45
+ file_path = os.path.join(temp_dir, filename)
46
+ with open(file_path, "wb") as f:
47
+ content = await upload_file.read()
48
+ f.write(content)
49
+ saved_files.append(file_path)
50
+ logger.info(f"Saved file: {file_path}")
51
+
52
+ # Split files into video and audio
53
+ video_files = [f for f in saved_files if f.lower().endswith(".mp4")]
54
+ audio_files = [f for f in saved_files if f.lower().endswith((".mp3", ".wav"))]
55
+
56
+ # Run merge
57
+ output_path = merge_videos_and_audios(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  video_files=video_files,
59
  audio_files=audio_files,
60
  orig_vol=orig_vol,
61
  music_vol=music_vol,
62
+ temp_dir=temp_dir,
63
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ # Return result as path or error
66
+ if isinstance(output_path, str) and output_path.startswith("Error"):
67
+ return {"success": False, "error": output_path}
68
 
69
+ # Move output to persistent location (optional)
70
+ public_output_dir = "./outputs"
71
+ os.makedirs(public_output_dir, exist_ok=True)
72
+ final_output_path = os.path.join(public_output_dir, os.path.basename(output_path))
73
+ shutil.move(output_path, final_output_path)
74
+ logger.info(f"Final output moved to {final_output_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
+ return {
77
+ "success": True,
78
+ "output_url": f"/outputs/{os.path.basename(final_output_path)}"
79
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
 
 
 
 
 
81
  except Exception as e:
82
+ error_msg = f"Exception: {str(e)}\n{traceback.format_exc()}"
83
  logger.error(error_msg)
84
+ return {"success": False, "error": error_msg}
85
+
86
+ # --- GRADIO SETUP ---
87
+ with gr.Blocks(title="Video and Audio Merger UI") as gradio_app:
88
+ gr.Markdown("## Video and Audio Merger")
89
+ file_input = gr.File(label="Upload Files", type="binary", file_count="multiple")
90
+ orig_vol_input = gr.Slider(minimum=0.0, maximum=1.0, value=1.0, step=0.05, label="Original Video Audio Volume")
91
+ music_vol_input = gr.Slider(minimum=0.0, maximum=1.0, value=0.5, step=0.05, label="Background Audio Volume")
92
+ output_video = gr.Video(label="Merged Video")
93
+ output_audio = gr.Audio(label="Merged Audio")
94
+ merge_button = gr.Button("Merge Files")
95
+
96
+ def gradio_merge_files(file_binaries, orig_vol, music_vol):
97
+ # You can call your existing function here (same as before)
98
+ pass # Keep your old gradio_merge_files() here!
99
+
100
+ merge_button.click(
101
+ fn=gradio_merge_files,
102
+ inputs=[file_input, orig_vol_input, music_vol_input],
103
+ outputs=[output_video, output_audio],
104
+ )
105
+
106
+ # --- COMBINE BOTH ---
107
+ if __name__ == "__main__":
108
+ import threading
109
+
110
+ # Run FastAPI server in background
111
+ def run_fastapi():
112
+ uvicorn.run(fastapi_app, host="0.0.0.0", port=8000)
113
+
114
+ threading.Thread(target=run_fastapi).start()
115
+
116
+ # Run Gradio app on another port (HuggingFace will pick this)
117
+ gradio_app.queue(api_open=True)
118
+ gradio_app.launch(server_port=7860, share=True)