Politrees commited on
Commit
196f79a
·
verified ·
1 Parent(s): 15dbe52

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +373 -192
app.py CHANGED
@@ -6,10 +6,13 @@ import subprocess
6
  import gradio as gr
7
  from tqdm import tqdm
8
  from datetime import datetime
9
- from pydub import AudioSegment
10
- from moviepy import VideoFileClip
 
11
  from PIL import Image
12
-
 
 
13
 
14
  # ----------------------- Internationalization -----------------------
15
  from i18n_local import en, ru, es, fr, de, it, ja, ko, ar, hi, tr
@@ -18,8 +21,365 @@ i18n = gr.I18n(
18
  ja=ja, ko=ko, ar=ar, hi=hi, tr=tr
19
  )
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- # ----------------------- FFmpeg utils -----------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  def _run_ffmpeg(args):
24
  try:
25
  res = subprocess.run(["ffmpeg", *args], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
@@ -28,16 +388,11 @@ def _run_ffmpeg(args):
28
  return None
29
 
30
  def ffmpeg_writable_formats():
31
- """
32
- Returns a set of FFmpeg format names (including aliases) available for writing (E flag).
33
- Example: {'mp4', 'mov', 'm4a', '3gp', 'matroska', 'webm', ...}
34
- """
35
  out = _run_ffmpeg(["-hide_banner", "-v", "error", "-formats"])
36
  if not out:
37
  return set()
38
  fmts = set()
39
  for line in out.splitlines():
40
- # lines look like: " DE matroska,webm Matroska / WebM"
41
  if re.match(r"^\s*[D\s]*E\s+", line):
42
  m = re.search(r"^\s*[D\s]*E\s+([^\s]+)", line)
43
  if not m:
@@ -48,21 +403,17 @@ def ffmpeg_writable_formats():
48
  return fmts
49
 
50
  def ffmpeg_audio_encoders():
51
- """
52
- Returns a set of available audio encoders, e.g. {'aac','libmp3lame','libopus',...}
53
- """
54
  out = _run_ffmpeg(["-hide_banner", "-v", "error", "-encoders"])
55
  if not out:
56
  return set()
57
  enc = set()
58
  for line in out.splitlines():
59
- # lines look like: " A..... libmp3lame MP3 (MPEG audio layer 3) (codec mp3)"
60
  m = re.match(r"^\s*A\S*\s+([^\s]+)", line)
61
  if m:
62
  enc.add(m.group(1).strip())
63
  return enc
64
 
65
- # Extension -> FFmpeg container mapping (curated, common ones)
66
  AUDIO_EXT_TO_FFMPEG_FORMAT = {
67
  "mp3": "mp3",
68
  "wav": "wav",
@@ -70,8 +421,8 @@ AUDIO_EXT_TO_FFMPEG_FORMAT = {
70
  "flac": "flac",
71
  "ogg": "ogg",
72
  "oga": "ogg",
73
- "opus": "ogg", # ogg container; needs libopus
74
- "spx": "ogg", # ogg container; needs libspeex
75
  "aac": "adts",
76
  "m4a": "mp4",
77
  "m4b": "mp4",
@@ -90,12 +441,10 @@ AUDIO_EXT_TO_FFMPEG_FORMAT = {
90
  "mka": "matroska",
91
  }
92
 
93
- # Some extensions require specific encoders
94
  AUDIO_REQUIRED_CODECS = {
95
  "mp3": ["libmp3lame"],
96
  "opus": ["libopus"],
97
  "spx": ["libspeex"],
98
- # others rely on FFmpeg defaults
99
  }
100
 
101
  VIDEO_EXT_TO_FFMPEG_FORMAT = {
@@ -130,7 +479,6 @@ def available_audio_extensions():
130
  if req and not any(r in encoders for r in req):
131
  continue
132
  exts.append(ext)
133
- # fallback if ffmpeg is missing or query failed
134
  if not exts:
135
  exts = ["mp3", "wav", "flac", "ogg", "aac", "m4a", "aiff", "wma", "opus"]
136
  return sorted(set(exts))
@@ -142,13 +490,10 @@ def available_video_extensions():
142
  exts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "mpeg", "mpg", "ts"]
143
  return sorted(set(exts))
144
 
145
- # ----------------------- Pillow utils (images) -----------------------
146
  def available_image_extensions():
147
- # All registered extensions Pillow knows how to save (best-effort)
148
- ext2fmt = Image.registered_extensions() # {".jpg":"JPEG", ...}
149
  save_ok = set(getattr(Image, "SAVE", {}).keys()) or set()
150
  if not save_ok:
151
- # If SAVE registry is unavailable, assume registered formats are savable
152
  save_ok = set(ext2fmt.values())
153
  exts = []
154
  for ext, fmt in ext2fmt.items():
@@ -186,169 +531,6 @@ def pil_format_for_ext(ext):
186
  }
187
  return fallback.get(ext, None)
188
 
189
-
190
- # ---------- AUDIO PROCESSING ----------
191
- def convert_audio(input_files, output_ext, session_id, merge_files, gap_duration):
192
- """Convert/merge audio into the selected format (by extension)."""
193
- output_files = []
194
- merged_audio = AudioSegment.silent(duration=0)
195
- os.makedirs(session_id, exist_ok=True)
196
-
197
- ff_format = AUDIO_EXT_TO_FFMPEG_FORMAT.get(output_ext, output_ext)
198
- codec = None
199
- if output_ext == "opus":
200
- codec = "libopus"
201
- elif output_ext == "spx":
202
- codec = "libspeex"
203
-
204
- for input_file in tqdm(input_files, desc="Converting audio files"):
205
- file_path = input_file if isinstance(input_file, str) else input_file.name
206
- audio = AudioSegment.from_file(file_path)
207
-
208
- base_name = os.path.splitext(os.path.basename(file_path))[0]
209
- output_filename = f"{base_name}.{output_ext}"
210
- output_path = os.path.join(session_id, output_filename)
211
-
212
- audio.export(output_path, format=ff_format, codec=codec)
213
-
214
- if merge_files:
215
- merged_audio += audio + AudioSegment.silent(duration=gap_duration)
216
- else:
217
- output_files.append(output_path)
218
-
219
- if merge_files:
220
- merged_output_path = os.path.join(session_id, f"merged_output.{output_ext}")
221
- merged_audio.export(merged_output_path, format=ff_format, codec=codec)
222
- return [merged_output_path]
223
-
224
- return output_files
225
-
226
-
227
- # ---------- IMAGE PROCESSING ----------
228
- def convert_images(input_files, output_ext, session_id):
229
- """Simple image format conversion."""
230
- os.makedirs(session_id, exist_ok=True)
231
- output_files = []
232
-
233
- pil_fmt = pil_format_for_ext(output_ext)
234
- if not pil_fmt:
235
- raise gr.Error(f"Pillow cannot save to format: {output_ext}")
236
-
237
- for input_file in tqdm(input_files, desc="Converting images"):
238
- file_path = input_file if isinstance(input_file, str) else input_file.name
239
- base_name = os.path.splitext(os.path.basename(file_path))[0]
240
- output_filename = f"{base_name}.{output_ext}"
241
- output_path = os.path.join(session_id, output_filename)
242
-
243
- with Image.open(file_path) as img:
244
- img.load()
245
- # For JPEG ensure RGB mode
246
- if pil_fmt.upper() == "JPEG":
247
- img = img.convert("RGB")
248
- img.save(output_path, format=pil_fmt)
249
-
250
- output_files.append(output_path)
251
-
252
- return output_files
253
-
254
-
255
- # ---------- ZIP CREATION ----------
256
- def create_zip(files_to_zip, session_id):
257
- zip_filename = f"{session_id}.zip"
258
- with zipfile.ZipFile(zip_filename, 'w') as zipf:
259
- for file in tqdm(files_to_zip, desc="Creating ZIP archive"):
260
- zipf.write(file, os.path.basename(file))
261
- return zip_filename
262
-
263
-
264
- # ---------- AUDIO HANDLER ----------
265
- def process_audio_files(files, output_ext, merge_files, gap_duration, progress=gr.Progress(track_tqdm=True)):
266
- if not files:
267
- raise gr.Error("Please upload at least one audio file!")
268
-
269
- session_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "_" + str(uuid.uuid4())[:8]
270
- print(f"\nStarting audio session: {session_id}")
271
- print(f"Files to convert: {len(files)} to .{output_ext}")
272
-
273
- output_files = convert_audio(files, output_ext, session_id, merge_files, gap_duration)
274
-
275
- if len(output_files) > 1:
276
- print("Creating ZIP archive...")
277
- zip_filename = create_zip(output_files, session_id)
278
- return zip_filename
279
-
280
- return output_files[0]
281
-
282
-
283
- # ---------- IMAGE HANDLER ----------
284
- def process_image_files(files, output_ext, progress=gr.Progress(track_tqdm=True)):
285
- if not files:
286
- raise gr.Error("Please upload at least one image!")
287
-
288
- session_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "_" + str(uuid.uuid4())[:8]
289
- print(f"\nStarting image session: {session_id}")
290
- print(f"Files to convert: {len(files)} to .{output_ext}")
291
-
292
- output_files = convert_images(files, output_ext, session_id)
293
-
294
- if len(output_files) > 1:
295
- print("Creating ZIP archive...")
296
- zip_filename = create_zip(output_files, session_id)
297
- return zip_filename
298
-
299
- return output_files[0]
300
-
301
-
302
- # ---------- VIDEO HANDLER ----------
303
- def process_video(input_video, conversion_type, output_ext, progress=gr.Progress(track_tqdm=True)):
304
- if not input_video:
305
- raise gr.Error("Please upload a video file!")
306
-
307
- session_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "_" + str(uuid.uuid4())[:8]
308
- os.makedirs(session_id, exist_ok=True)
309
-
310
- input_path = input_video if isinstance(input_video, str) else input_video.name
311
- base_name = os.path.splitext(os.path.basename(input_path))[0]
312
- output_filename = f"{base_name}_converted.{output_ext}"
313
- output_path = os.path.join(session_id, output_filename)
314
-
315
- print(f"\nStarting video session: {session_id}")
316
- print(f"Conversion type: {conversion_type}, Output: .{output_ext}")
317
-
318
- try:
319
- clip = VideoFileClip(input_path)
320
-
321
- if conversion_type == "Video to Video":
322
- # Let MoviePy use its defaults; for some containers (e.g. webm) this may require specific codecs
323
- clip.write_videofile(output_path, logger=None)
324
-
325
- elif conversion_type == "Video to Audio":
326
- if clip.audio is None:
327
- raise gr.Error("The uploaded video does not contain an audio track.")
328
- audio_clip = clip.audio
329
-
330
- audio_codec = None
331
- if output_ext == "opus":
332
- if "libopus" in ffmpeg_audio_encoders():
333
- audio_codec = "libopus"
334
- elif output_ext == "spx":
335
- if "libspeex" in ffmpeg_audio_encoders():
336
- audio_codec = "libspeex"
337
-
338
- audio_clip.write_audiofile(output_path, logger=None, codec=audio_codec)
339
- audio_clip.close()
340
-
341
- clip.close()
342
-
343
- except Exception as e:
344
- print(f"An error occurred: {e}")
345
- raise gr.Error(f"Processing error: {e}")
346
-
347
- print("Video processing complete!")
348
- return output_path
349
-
350
-
351
- # ---------- FORMAT CHOICES ----------
352
  def update_format_choices(conversion_type):
353
  if conversion_type == "Video to Video":
354
  vf = available_video_extensions()
@@ -359,8 +541,7 @@ def update_format_choices(conversion_type):
359
  value = "mp3" if "mp3" in af else (af[0] if af else None)
360
  return gr.Dropdown(choices=af, value=value, label="Output Audio Format")
361
 
362
-
363
- # ---------- UI ----------
364
  AUDIO_FORMATS = available_audio_extensions()
365
  VIDEO_FORMATS = available_video_extensions()
366
  IMAGE_FORMATS = available_image_extensions()
@@ -386,7 +567,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
386
  audio_output_file = gr.File(label=i18n("download_result"))
387
 
388
  audio_submit_button.click(
389
- fn=process_audio_files,
390
  inputs=[audio_file_input, audio_format_choice, merge_files_checkbox, gap_slider],
391
  outputs=audio_output_file
392
  )
@@ -406,7 +587,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
406
  image_output_file = gr.File(label=i18n("download_result"))
407
 
408
  image_submit_button.click(
409
- fn=process_image_files,
410
  inputs=[image_file_input, image_format_choice],
411
  outputs=image_output_file
412
  )
@@ -441,10 +622,10 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
441
  )
442
 
443
  video_submit_button.click(
444
- fn=process_video,
445
  inputs=[video_input, conversion_type_radio, video_format_dropdown],
446
  outputs=video_output_file
447
  )
448
 
449
  if __name__ == "__main__":
450
- demo.launch(i18n=i18n, debug=True)
 
6
  import gradio as gr
7
  from tqdm import tqdm
8
  from datetime import datetime
9
+ from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
10
+ from threading import Thread
11
+ import multiprocessing
12
  from PIL import Image
13
+ import shutil
14
+ import platform
15
+ import json
16
 
17
  # ----------------------- Internationalization -----------------------
18
  from i18n_local import en, ru, es, fr, de, it, ja, ko, ar, hi, tr
 
21
  ja=ja, ko=ko, ar=ar, hi=hi, tr=tr
22
  )
23
 
24
+ # ----------------------- Performance Settings -----------------------
25
+ MAX_WORKERS = multiprocessing.cpu_count()
26
+ CHUNK_SIZE = 1024 * 1024 * 10 # 10MB chunks for streaming
27
+
28
+ # ----------------------- Hardware Acceleration Detection -----------------------
29
+ def detect_hardware_acceleration():
30
+ """Determines the available methods for hardware acceleration."""
31
+ accelerations = {
32
+ 'nvidia': False,
33
+ 'intel': False,
34
+ 'amd': False,
35
+ 'videotoolbox': False # macOS
36
+ }
37
+
38
+ try:
39
+ result = subprocess.run(['ffmpeg', '-hide_banner', '-hwaccels'],
40
+ capture_output=True, text=True)
41
+ output = result.stdout.lower()
42
+
43
+ if 'cuda' in output or 'nvenc' in output:
44
+ accelerations['nvidia'] = True
45
+ if 'qsv' in output or 'vaapi' in output:
46
+ accelerations['intel'] = True
47
+ if 'amf' in output:
48
+ accelerations['amd'] = True
49
+ if 'videotoolbox' in output:
50
+ accelerations['videotoolbox'] = True
51
+ except:
52
+ pass
53
+
54
+ return accelerations
55
+
56
+ HARDWARE_ACCEL = detect_hardware_acceleration()
57
+
58
+ # ----------------------- FFmpeg Optimized Utils -----------------------
59
+ def get_optimal_ffmpeg_params(input_file, output_file, conversion_type="audio", use_hw=True):
60
+ """Returns the optimal FFmpeg parameters for conversion."""
61
+ params = []
62
+
63
+ # Basic parameters for acceleration
64
+ params.extend(['-hide_banner', '-y', '-loglevel', 'error', '-stats'])
65
+
66
+ # Hardware acceleration (if available and enabled)
67
+ if use_hw and conversion_type == "video":
68
+ if HARDWARE_ACCEL['nvidia']:
69
+ params.extend(['-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda'])
70
+ elif HARDWARE_ACCEL['intel']:
71
+ params.extend(['-hwaccel', 'qsv'])
72
+ elif HARDWARE_ACCEL['videotoolbox'] and platform.system() == 'Darwin':
73
+ params.extend(['-hwaccel', 'videotoolbox'])
74
+
75
+ params.extend(['-i', input_file])
76
+
77
+ # Optimization by Conversion Type
78
+ if conversion_type == "audio":
79
+ ext = os.path.splitext(output_file)[1].lower()[1:]
80
+ if ext == 'mp3':
81
+ params.extend(['-codec:a', 'libmp3lame', '-q:a', '2', '-threads', str(MAX_WORKERS)])
82
+ elif ext == 'aac' or ext == 'm4a':
83
+ params.extend(['-codec:a', 'aac', '-b:a', '192k', '-threads', str(MAX_WORKERS)])
84
+ elif ext == 'opus':
85
+ params.extend(['-codec:a', 'libopus', '-b:a', '128k', '-threads', str(MAX_WORKERS)])
86
+ elif ext == 'flac':
87
+ params.extend(['-codec:a', 'flac', '-compression_level', '5', '-threads', str(MAX_WORKERS)])
88
+ else:
89
+ params.extend(['-threads', str(MAX_WORKERS)])
90
+
91
+ elif conversion_type == "video":
92
+ ext = os.path.splitext(output_file)[1].lower()[1:]
93
+
94
+ # Use of hardware codecs
95
+ if use_hw and HARDWARE_ACCEL['nvidia']:
96
+ params.extend(['-c:v', 'h264_nvenc', '-preset', 'p4', '-tune', 'hq', '-rc', 'vbr', '-cq', '23'])
97
+ elif use_hw and HARDWARE_ACCEL['intel']:
98
+ params.extend(['-c:v', 'h264_qsv', '-preset', 'faster', '-global_quality', '23'])
99
+ elif use_hw and HARDWARE_ACCEL['videotoolbox'] and platform.system() == 'Darwin':
100
+ params.extend(['-c:v', 'h264_videotoolbox', '-b:v', '5000k'])
101
+ else:
102
+ # CPU optimization
103
+ params.extend(['-c:v', 'libx264', '-preset', 'faster', '-crf', '23', '-threads', str(MAX_WORKERS)])
104
+
105
+ # Copy audio without transcoding (faster)
106
+ params.extend(['-c:a', 'copy'])
107
+
108
+ elif conversion_type == "video_to_audio":
109
+ # Extracting only the audio track
110
+ params.extend(['-vn']) # Turn off video
111
+ ext = os.path.splitext(output_file)[1].lower()[1:]
112
+ if ext == 'mp3':
113
+ params.extend(['-codec:a', 'libmp3lame', '-q:a', '2'])
114
+ elif ext == 'aac' or ext == 'm4a':
115
+ params.extend(['-codec:a', 'aac', '-b:a', '192k'])
116
+ else:
117
+ params.extend(['-codec:a', 'copy']) # Try copying without transcoding
118
+
119
+ params.extend(['-threads', str(MAX_WORKERS)])
120
+
121
+ # Оптимизация для больших файлов
122
+ params.extend(['-max_muxing_queue_size', '9999'])
123
+
124
+ params.append(output_file)
125
+ return params
126
+
127
+ def run_ffmpeg_with_progress(params, duration=None):
128
+ """Launches FFmpeg with a progress indicator."""
129
+ try:
130
+ process = subprocess.Popen(
131
+ ['ffmpeg'] + params,
132
+ stdout=subprocess.PIPE,
133
+ stderr=subprocess.PIPE,
134
+ universal_newlines=True
135
+ )
136
+
137
+ for line in process.stderr:
138
+ if 'time=' in line:
139
+ print('.', end='', flush=True)
140
+
141
+ process.wait()
142
+ if process.returncode != 0:
143
+ raise Exception(f"FFmpeg failed with return code {process.returncode}")
144
+
145
+ return True
146
+ except Exception as e:
147
+ print(f"FFmpeg error: {e}")
148
+ return False
149
+
150
+ # ----------------------- Optimized Conversion Functions -----------------------
151
+ def convert_audio_ffmpeg(input_file, output_file, output_ext):
152
+ """Fast audio conversion using a direct FFmpeg call."""
153
+ params = get_optimal_ffmpeg_params(input_file, output_file, "audio")
154
+ return run_ffmpeg_with_progress(params)
155
+
156
+ def convert_video_ffmpeg(input_file, output_file, conversion_type):
157
+ """Fast video conversion using a direct FFmpeg call."""
158
+ conv_type = "video_to_audio" if conversion_type == "Video to Audio" else "video"
159
+ params = get_optimal_ffmpeg_params(input_file, output_file, conv_type)
160
+ return run_ffmpeg_with_progress(params)
161
+
162
+ def merge_audio_files_ffmpeg(input_files, output_file, gap_duration):
163
+ """Efficiently merge audio files using FFmpeg."""
164
+ list_file = f"concat_list_{uuid.uuid4().hex}.txt"
165
+
166
+ try:
167
+ with open(list_file, 'w') as f:
168
+ for i, file in enumerate(input_files):
169
+ f.write(f"file '{os.path.abspath(file)}'\n")
170
+ if i < len(input_files) - 1 and gap_duration > 0:
171
+ f.write(f"file 'silence.wav'\n")
172
+
173
+ # Create a silent file if needed
174
+ if gap_duration > 0:
175
+ silence_params = [
176
+ '-f', 'lavfi',
177
+ '-i', f'anullsrc=duration={gap_duration/1000}:sample_rate=44100',
178
+ '-t', str(gap_duration/1000),
179
+ 'silence.wav'
180
+ ]
181
+ subprocess.run(['ffmpeg', '-y'] + silence_params, check=True, capture_output=True)
182
+
183
+ # We are merging files
184
+ params = [
185
+ '-f', 'concat',
186
+ '-safe', '0',
187
+ '-i', list_file,
188
+ '-c', 'copy',
189
+ '-threads', str(MAX_WORKERS),
190
+ output_file
191
+ ]
192
+
193
+ return run_ffmpeg_with_progress(params)
194
+
195
+ finally:
196
+ if os.path.exists(list_file):
197
+ os.remove(list_file)
198
+ if os.path.exists('silence.wav'):
199
+ os.remove('silence.wav')
200
+
201
+ # ----------------------- Parallel Processing -----------------------
202
+ def process_file_parallel(args):
203
+ """Function for parallel file processing."""
204
+ input_file, output_path, file_type, output_ext = args
205
+
206
+ try:
207
+ if file_type == "audio":
208
+ success = convert_audio_ffmpeg(input_file, output_path, output_ext)
209
+ elif file_type == "image":
210
+ success = convert_image_optimized(input_file, output_path, output_ext)
211
+ else:
212
+ success = False
213
+
214
+ return output_path if success else None
215
+ except Exception as e:
216
+ print(f"Error processing {input_file}: {e}")
217
+ return None
218
+
219
+ def convert_image_optimized(input_file, output_path, output_ext):
220
+ """Optimized image conversion"""
221
+ try:
222
+ pil_fmt = pil_format_for_ext(output_ext)
223
+ if not pil_fmt:
224
+ return False
225
+
226
+ with Image.open(input_file) as img:
227
+ if pil_fmt.upper() == "JPEG":
228
+ img = img.convert("RGB")
229
+ img.save(output_path, format=pil_fmt, quality=90, optimize=True)
230
+ elif pil_fmt.upper() == "PNG":
231
+ img.save(output_path, format=pil_fmt, optimize=True, compress_level=6)
232
+ else:
233
+ img.save(output_path, format=pil_fmt)
234
+
235
+ return True
236
+ except Exception as e:
237
+ print(f"Image conversion error: {e}")
238
+ return False
239
+
240
+ # ----------------------- Main Processing Functions -----------------------
241
+ def process_audio_files_optimized(files, output_ext, merge_files, gap_duration, progress=gr.Progress(track_tqdm=True)):
242
+ """Optimized audio file processing"""
243
+ if not files:
244
+ raise gr.Error("Please upload at least one audio file!")
245
+
246
+ session_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "_" + str(uuid.uuid4())[:8]
247
+ os.makedirs(session_id, exist_ok=True)
248
+
249
+ print(f"\nStarting optimized audio session: {session_id}")
250
+ print(f"Files to convert: {len(files)} to .{output_ext}")
251
+
252
+ file_paths = [f if isinstance(f, str) else f.name for f in files]
253
+
254
+ if merge_files:
255
+ merged_output_path = os.path.join(session_id, f"merged_output.{output_ext}")
256
+
257
+ temp_files = []
258
+ with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
259
+ tasks = []
260
+ for i, file_path in enumerate(file_paths):
261
+ temp_output = os.path.join(session_id, f"temp_{i}.{output_ext}")
262
+ tasks.append(executor.submit(convert_audio_ffmpeg, file_path, temp_output, output_ext))
263
+
264
+ for future in tqdm(tasks, desc="Converting files"):
265
+ result = future.result()
266
+ if result:
267
+ temp_files.append(result)
268
+
269
+ if merge_audio_files_ffmpeg(temp_files, merged_output_path, gap_duration):
270
+ for temp_file in temp_files:
271
+ try:
272
+ os.remove(temp_file)
273
+ except:
274
+ pass
275
+ return merged_output_path
276
+ else:
277
+ raise gr.Error("Failed to merge audio files")
278
+
279
+ else:
280
+ output_files = []
281
+
282
+ with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
283
+ tasks = []
284
+ for file_path in file_paths:
285
+ base_name = os.path.splitext(os.path.basename(file_path))[0]
286
+ output_filename = f"{base_name}.{output_ext}"
287
+ output_path = os.path.join(session_id, output_filename)
288
+ tasks.append((file_path, output_path, "audio", output_ext))
289
+
290
+ results = list(tqdm(
291
+ executor.map(process_file_parallel, tasks),
292
+ total=len(tasks),
293
+ desc="Converting audio files"
294
+ ))
295
+
296
+ output_files = [r for r in results if r is not None]
297
+
298
+ if not output_files:
299
+ raise gr.Error("No files were successfully converted")
300
+
301
+ if len(output_files) > 1:
302
+ print("Creating ZIP archive...")
303
+ zip_filename = create_zip_optimized(output_files, session_id)
304
+ return zip_filename
305
+
306
+ return output_files[0]
307
+
308
+ def process_image_files_optimized(files, output_ext, progress=gr.Progress(track_tqdm=True)):
309
+ """Optimized image processing"""
310
+ if not files:
311
+ raise gr.Error("Please upload at least one image!")
312
+
313
+ session_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "_" + str(uuid.uuid4())[:8]
314
+ os.makedirs(session_id, exist_ok=True)
315
+
316
+ print(f"\nStarting optimized image session: {session_id}")
317
+ print(f"Files to convert: {len(files)} to .{output_ext}")
318
+
319
+ file_paths = [f if isinstance(f, str) else f.name for f in files]
320
+ output_files = []
321
 
322
+ with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
323
+ tasks = []
324
+ for file_path in file_paths:
325
+ base_name = os.path.splitext(os.path.basename(file_path))[0]
326
+ output_filename = f"{base_name}.{output_ext}"
327
+ output_path = os.path.join(session_id, output_filename)
328
+ tasks.append((file_path, output_path, "image", output_ext))
329
+
330
+ results = list(tqdm(
331
+ executor.map(process_file_parallel, tasks),
332
+ total=len(tasks),
333
+ desc="Converting images"
334
+ ))
335
+
336
+ output_files = [r for r in results if r is not None]
337
+
338
+ if not output_files:
339
+ raise gr.Error("No images were successfully converted")
340
+
341
+ if len(output_files) > 1:
342
+ print("Creating ZIP archive...")
343
+ zip_filename = create_zip_optimized(output_files, session_id)
344
+ return zip_filename
345
+
346
+ return output_files[0]
347
+
348
+ def process_video_optimized(input_video, conversion_type, output_ext, progress=gr.Progress(track_tqdm=True)):
349
+ """Optimized video processing"""
350
+ if not input_video:
351
+ raise gr.Error("Please upload a video file!")
352
+
353
+ session_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "_" + str(uuid.uuid4())[:8]
354
+ os.makedirs(session_id, exist_ok=True)
355
+
356
+ input_path = input_video if isinstance(input_video, str) else input_video.name
357
+ base_name = os.path.splitext(os.path.basename(input_path))[0]
358
+ output_filename = f"{base_name}_converted.{output_ext}"
359
+ output_path = os.path.join(session_id, output_filename)
360
+
361
+ print(f"\nStarting optimized video session: {session_id}")
362
+ print(f"Conversion type: {conversion_type}, Output: .{output_ext}")
363
+
364
+ success = convert_video_ffmpeg(input_path, output_path, conversion_type)
365
+
366
+ if success:
367
+ print("Video processing complete!")
368
+ return output_path
369
+ else:
370
+ raise gr.Error("Video processing failed")
371
+
372
+ def create_zip_optimized(files_to_zip, session_id):
373
+ """Optimized ZIP archive creation"""
374
+ zip_filename = f"{session_id}.zip"
375
+
376
+ with zipfile.ZipFile(zip_filename, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=1) as zipf:
377
+ for file in tqdm(files_to_zip, desc="Creating ZIP archive"):
378
+ zipf.write(file, os.path.basename(file))
379
+
380
+ return zip_filename
381
+
382
+ # ----------------------- Keep original helper functions -----------------------
383
  def _run_ffmpeg(args):
384
  try:
385
  res = subprocess.run(["ffmpeg", *args], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
 
388
  return None
389
 
390
  def ffmpeg_writable_formats():
 
 
 
 
391
  out = _run_ffmpeg(["-hide_banner", "-v", "error", "-formats"])
392
  if not out:
393
  return set()
394
  fmts = set()
395
  for line in out.splitlines():
 
396
  if re.match(r"^\s*[D\s]*E\s+", line):
397
  m = re.search(r"^\s*[D\s]*E\s+([^\s]+)", line)
398
  if not m:
 
403
  return fmts
404
 
405
  def ffmpeg_audio_encoders():
 
 
 
406
  out = _run_ffmpeg(["-hide_banner", "-v", "error", "-encoders"])
407
  if not out:
408
  return set()
409
  enc = set()
410
  for line in out.splitlines():
 
411
  m = re.match(r"^\s*A\S*\s+([^\s]+)", line)
412
  if m:
413
  enc.add(m.group(1).strip())
414
  return enc
415
 
416
+ # Extension mappings
417
  AUDIO_EXT_TO_FFMPEG_FORMAT = {
418
  "mp3": "mp3",
419
  "wav": "wav",
 
421
  "flac": "flac",
422
  "ogg": "ogg",
423
  "oga": "ogg",
424
+ "opus": "ogg",
425
+ "spx": "ogg",
426
  "aac": "adts",
427
  "m4a": "mp4",
428
  "m4b": "mp4",
 
441
  "mka": "matroska",
442
  }
443
 
 
444
  AUDIO_REQUIRED_CODECS = {
445
  "mp3": ["libmp3lame"],
446
  "opus": ["libopus"],
447
  "spx": ["libspeex"],
 
448
  }
449
 
450
  VIDEO_EXT_TO_FFMPEG_FORMAT = {
 
479
  if req and not any(r in encoders for r in req):
480
  continue
481
  exts.append(ext)
 
482
  if not exts:
483
  exts = ["mp3", "wav", "flac", "ogg", "aac", "m4a", "aiff", "wma", "opus"]
484
  return sorted(set(exts))
 
490
  exts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "mpeg", "mpg", "ts"]
491
  return sorted(set(exts))
492
 
 
493
  def available_image_extensions():
494
+ ext2fmt = Image.registered_extensions()
 
495
  save_ok = set(getattr(Image, "SAVE", {}).keys()) or set()
496
  if not save_ok:
 
497
  save_ok = set(ext2fmt.values())
498
  exts = []
499
  for ext, fmt in ext2fmt.items():
 
531
  }
532
  return fallback.get(ext, None)
533
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  def update_format_choices(conversion_type):
535
  if conversion_type == "Video to Video":
536
  vf = available_video_extensions()
 
541
  value = "mp3" if "mp3" in af else (af[0] if af else None)
542
  return gr.Dropdown(choices=af, value=value, label="Output Audio Format")
543
 
544
+ # ----------------------- UI with optimized handlers -----------------------
 
545
  AUDIO_FORMATS = available_audio_extensions()
546
  VIDEO_FORMATS = available_video_extensions()
547
  IMAGE_FORMATS = available_image_extensions()
 
567
  audio_output_file = gr.File(label=i18n("download_result"))
568
 
569
  audio_submit_button.click(
570
+ fn=process_audio_files_optimized,
571
  inputs=[audio_file_input, audio_format_choice, merge_files_checkbox, gap_slider],
572
  outputs=audio_output_file
573
  )
 
587
  image_output_file = gr.File(label=i18n("download_result"))
588
 
589
  image_submit_button.click(
590
+ fn=process_image_files_optimized,
591
  inputs=[image_file_input, image_format_choice],
592
  outputs=image_output_file
593
  )
 
622
  )
623
 
624
  video_submit_button.click(
625
+ fn=process_video_optimized,
626
  inputs=[video_input, conversion_type_radio, video_format_dropdown],
627
  outputs=video_output_file
628
  )
629
 
630
  if __name__ == "__main__":
631
+ demo.launch(i18n=i18n, debug=True)