sam12345324 commited on
Commit
35483fb
·
verified ·
1 Parent(s): e7abea9

Upload app (3).py

Browse files
Files changed (1) hide show
  1. app (3).py +487 -0
app (3).py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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