sr4 commited on
Commit
a4ef6e2
·
verified ·
1 Parent(s): ae8e84d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +550 -551
app.py CHANGED
@@ -1,552 +1,551 @@
1
- import os
2
- import subprocess
3
- import logging
4
- import asyncio
5
- import threading
6
- import time
7
- import psutil
8
- from concurrent.futures import ThreadPoolExecutor, as_completed
9
- from typing import Optional, List, Tuple, Dict, Any
10
- from pathlib import Path
11
- import gradio as gr
12
- from dataclasses import dataclass
13
- import json
14
- import zipfile
15
- import shutil
16
-
17
-
18
- from io import BytesIO
19
-
20
- # Configuración de logging
21
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
22
- logger = logging.getLogger(__name__)
23
-
24
- # Directorios y rutas
25
- UPLOAD_DIR = os.path.join(os.getcwd(), "uploads")
26
- FFMPEG_PATH = os.path.join(os.path.dirname(__file__), "ffmpeg.exe") # Ruta al ejecutable ffmpeg
27
-
28
- # Crear directorios
29
- os.makedirs(UPLOAD_DIR, exist_ok=True)
30
-
31
- # Definir formatos soportados (solo video y audio)
32
- VIDEO_FORMATS = ['.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v', '.3gp', '.ts', '.mts', '.mpg', '.mpeg', '.ogv']
33
- AUDIO_FORMATS = ['.mp3', '.wav', '.aac', '.flac', '.ogg', '.m4a', '.wma', '.opus', '.ac3', '.dts', '.amr', '.aiff']
34
-
35
- ALL_SUPPORTED_FORMATS = VIDEO_FORMATS + AUDIO_FORMATS
36
-
37
- # Configuraciones de compresión agresiva para reducir tamaño
38
- QUALITY_PRESETS = {
39
- "Compresión Máxima": {
40
- "video_bitrate": "200k",
41
- "audio_bitrate": "32k",
42
- "preset": "slow",
43
- "crf": "35"
44
- },
45
- "Compresión Alta": {
46
- "video_bitrate": "400k",
47
- "audio_bitrate": "48k",
48
- "preset": "medium",
49
- "crf": "30"
50
- },
51
- "Compresión Media": {
52
- "video_bitrate": "600k",
53
- "audio_bitrate": "64k",
54
- "preset": "fast",
55
- "crf": "28"
56
- },
57
- "Compresión Rápida": {
58
- "video_bitrate": "800k",
59
- "audio_bitrate": "96k",
60
- "preset": "veryfast",
61
- "crf": "25"
62
- }
63
- }
64
-
65
- @dataclass
66
- class ConversionProgress:
67
- filename: str
68
- status: str
69
- progress: float
70
- original_size: int = 0
71
- compressed_size: int = 0
72
- compression_ratio: float = 0.0
73
- output_file: Optional[str] = None
74
- error: Optional[str] = None
75
-
76
- class UniversalCompressor:
77
- def __init__(self):
78
- self.conversion_progress = {}
79
- self.active_conversions = 0
80
- self.cpu_cores = psutil.cpu_count(logical=False)
81
-
82
- def get_file_size(self, file_path: str) -> int:
83
- """Obtener tamaño del archivo en bytes."""
84
- try:
85
- return os.path.getsize(file_path)
86
- except:
87
- return 0
88
-
89
- def detect_file_type(self, file_path: str) -> str:
90
- """Detectar tipo de archivo basado en extensión."""
91
- ext = os.path.splitext(file_path)[1].lower()
92
- if ext in VIDEO_FORMATS:
93
- return 'video'
94
- elif ext in AUDIO_FORMATS:
95
- return 'audio'
96
- else:
97
- return 'unsupported'
98
-
99
- def sanitize_filename(self, filename: str) -> str:
100
- """Limpiar y validar nombre de archivo."""
101
- return ''.join(c for c in filename if c.isalnum() or c in ('.', '_', '-')).rstrip()
102
-
103
- def ensure_unique_filename(self, directory: str, filename: str) -> str:
104
- """Generar nombre único para evitar sobreescrituras."""
105
- base, ext = os.path.splitext(filename)
106
- counter = 1
107
- new_filename = filename
108
- while os.path.exists(os.path.join(directory, new_filename)):
109
- new_filename = f"{base}_{counter}{ext}"
110
- counter += 1
111
- return new_filename
112
-
113
-
114
-
115
- def compress_media(self, input_file: str, quality_preset: str, progress_callback=None) -> str:
116
- """Comprimir video/audio con configuraciones optimizadas para CPU."""
117
- try:
118
- media_type = self.detect_file_type(input_file)
119
- base_name = os.path.basename(input_file)
120
- file_id = f"{base_name}_{int(time.time())}"
121
-
122
- # Inicializar progreso
123
- original_size = self.get_file_size(input_file)
124
- self.conversion_progress[file_id] = ConversionProgress(
125
- filename=base_name,
126
- status="Iniciando compresión...",
127
- progress=0.0,
128
- original_size=original_size
129
- )
130
-
131
- if progress_callback:
132
- progress_callback(file_id, self.conversion_progress[file_id])
133
-
134
- # Determinar extensión de salida
135
- output_extension = 'mp4' if media_type == 'video' else 'm4a'
136
- output_filename = self.ensure_unique_filename(
137
- UPLOAD_DIR,
138
- f"{os.path.splitext(base_name)[0]}_compressed.{output_extension}"
139
- )
140
- output_file = os.path.join(UPLOAD_DIR, output_filename)
141
-
142
- quality_config = QUALITY_PRESETS[quality_preset]
143
-
144
- # Configuraciones FFmpeg optimizadas para menor uso de CPU
145
- if media_type == 'video':
146
- ffmpeg_command = [
147
- FFMPEG_PATH,
148
- '-i', input_file,
149
- '-c:v', 'libx264',
150
- '-crf', quality_config['crf'],
151
- '-preset', quality_config['preset'],
152
- '-c:a', 'aac',
153
- '-b:a', quality_config['audio_bitrate'],
154
- '-movflags', '+faststart',
155
- '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
156
- '-threads', str(min(self.cpu_cores, 4)), # Limitar threads para menor uso CPU
157
- '-y',
158
- output_file
159
- ]
160
- else: # audio
161
- ffmpeg_command = [
162
- FFMPEG_PATH,
163
- '-i', input_file,
164
- '-vn',
165
- '-c:a', 'aac',
166
- '-b:a', quality_config['audio_bitrate'],
167
- '-ar', '44100',
168
- '-threads', str(min(self.cpu_cores, 2)),
169
- '-y',
170
- output_file
171
- ]
172
-
173
- # Ejecutar conversión
174
- self.conversion_progress[file_id].status = "Comprimiendo..."
175
- self.conversion_progress[file_id].progress = 25.0
176
- if progress_callback:
177
- progress_callback(file_id, self.conversion_progress[file_id])
178
-
179
- process = subprocess.Popen(
180
- ffmpeg_command,
181
- stdout=subprocess.PIPE,
182
- stderr=subprocess.PIPE,
183
- text=True
184
- )
185
-
186
- # Simular progreso
187
- for i in range(25, 90, 10):
188
- if process.poll() is None:
189
- self.conversion_progress[file_id].progress = float(i)
190
- if progress_callback:
191
- progress_callback(file_id, self.conversion_progress[file_id])
192
- time.sleep(0.3)
193
- else:
194
- break
195
-
196
- stdout, stderr = process.communicate()
197
-
198
- if process.returncode != 0:
199
- error_msg = f"Error FFmpeg: {stderr}"
200
- self.conversion_progress[file_id].status = "Error"
201
- self.conversion_progress[file_id].error = error_msg
202
- if progress_callback:
203
- progress_callback(file_id, self.conversion_progress[file_id])
204
- raise Exception(error_msg)
205
-
206
- # Finalizar
207
- compressed_size = self.get_file_size(output_file)
208
- compression_ratio = ((original_size - compressed_size) / original_size) * 100 if original_size > 0 else 0
209
-
210
- self.conversion_progress[file_id].status = "Completado"
211
- self.conversion_progress[file_id].progress = 100.0
212
- self.conversion_progress[file_id].compressed_size = compressed_size
213
- self.conversion_progress[file_id].compression_ratio = compression_ratio
214
- self.conversion_progress[file_id].output_file = output_file
215
-
216
- if progress_callback:
217
- progress_callback(file_id, self.conversion_progress[file_id])
218
-
219
- logger.info(f"Archivo comprimido: {output_file} (Reducción: {compression_ratio:.1f}%)")
220
- return output_file
221
-
222
- except Exception as e:
223
- error_msg = f"Error durante compresión: {str(e)}"
224
- logger.error(error_msg)
225
- if file_id in self.conversion_progress:
226
- self.conversion_progress[file_id].status = "Error"
227
- self.conversion_progress[file_id].error = error_msg
228
- if progress_callback:
229
- progress_callback(file_id, self.conversion_progress[file_id])
230
- raise Exception(error_msg)
231
-
232
- def compress_archive(self, input_file: str, progress_callback=None) -> str:
233
- """Recomprimir archivos comprimidos para mejor ratio."""
234
- try:
235
- base_name = os.path.basename(input_file)
236
- file_id = f"{base_name}_{int(time.time())}"
237
-
238
- original_size = self.get_file_size(input_file)
239
- self.conversion_progress[file_id] = ConversionProgress(
240
- filename=base_name,
241
- status="Recomprimiendo archivo...",
242
- progress=50.0,
243
- original_size=original_size
244
- )
245
-
246
- if progress_callback:
247
- progress_callback(file_id, self.conversion_progress[file_id])
248
-
249
- # Crear archivo 7z con máxima compresión
250
- output_filename = self.ensure_unique_filename(
251
- UPLOAD_DIR, f"{os.path.splitext(base_name)[0]}_compressed.7z"
252
- )
253
- output_file = os.path.join(UPLOAD_DIR, output_filename)
254
-
255
- # Usar 7z para recompresión
256
- import py7zr
257
- with py7zr.SevenZipFile(output_file, 'w', filters=[{"id": py7zr.FILTER_LZMA2, "preset": 9}]) as archive:
258
- archive.write(input_file, base_name)
259
-
260
- compressed_size = self.get_file_size(output_file)
261
- compression_ratio = ((original_size - compressed_size) / original_size) * 100 if original_size > 0 else 0
262
-
263
- self.conversion_progress[file_id].status = "Completado"
264
- self.conversion_progress[file_id].progress = 100.0
265
- self.conversion_progress[file_id].compressed_size = compressed_size
266
- self.conversion_progress[file_id].compression_ratio = compression_ratio
267
- self.conversion_progress[file_id].output_file = output_file
268
-
269
- if progress_callback:
270
- progress_callback(file_id, self.conversion_progress[file_id])
271
-
272
- return output_file
273
-
274
- except Exception as e:
275
- error_msg = f"Error recomprimiendo archivo: {str(e)}"
276
- logger.error(error_msg)
277
- if file_id in self.conversion_progress:
278
- self.conversion_progress[file_id].status = "Error"
279
- self.conversion_progress[file_id].error = error_msg
280
- if progress_callback:
281
- progress_callback(file_id, self.conversion_progress[file_id])
282
- raise Exception(error_msg)
283
-
284
- def compress_single_file(self, input_file: str, quality_preset: str, progress_callback=None) -> str:
285
- """Comprimir un archivo según su tipo."""
286
- if not os.path.exists(input_file):
287
- raise ValueError(f"El archivo no existe: {input_file}")
288
-
289
- file_type = self.detect_file_type(input_file)
290
-
291
- if file_type in ['video', 'audio']:
292
- return self.compress_media(input_file, quality_preset, progress_callback)
293
- else:
294
- raise ValueError(f"Tipo de archivo no soportado: {os.path.splitext(input_file)[1]}. Solo se soportan videos y audios: {', '.join(ALL_SUPPORTED_FORMATS)}")
295
-
296
- def compress_multiple_files(self, file_paths: List[str], quality_preset: str, max_workers: int = None, progress_callback=None) -> List[str]:
297
- """Comprimir múltiples archivos en paralelo con optimización de CPU."""
298
- if max_workers is None:
299
- max_workers = min(self.cpu_cores, 3) # Limitar para no saturar CPU
300
-
301
- compressed_files = []
302
- self.active_conversions = len(file_paths)
303
-
304
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
305
- future_to_file = {
306
- executor.submit(self.compress_single_file, file_path, quality_preset, progress_callback): file_path
307
- for file_path in file_paths
308
- }
309
-
310
- for future in as_completed(future_to_file):
311
- file_path = future_to_file[future]
312
- try:
313
- result = future.result()
314
- compressed_files.append(result)
315
- except Exception as e:
316
- logger.error(f"Error comprimiendo {file_path}: {e}")
317
-
318
- self.active_conversions = 0
319
- return compressed_files
320
-
321
- # Instancia global del compresor
322
- compressor = UniversalCompressor()
323
-
324
- def update_progress_display():
325
- """Actualizar visualización del progreso con información detallada."""
326
- if not compressor.conversion_progress:
327
- return "No hay compresiones en progreso."
328
-
329
- progress_text = "📊 **Estado de Compresiones:**\n\n"
330
- total_original = 0
331
- total_compressed = 0
332
-
333
- for file_id, progress in compressor.conversion_progress.items():
334
- status_emoji = {
335
- "Iniciando compresión...": "🔄",
336
- "Procesando imagen...": "🖼️",
337
- "Comprimiendo...": "⚙️",
338
- "Recomprimiendo archivo...": "📦",
339
- "Completado": "✅",
340
- "Error": "❌"
341
- }.get(progress.status, "🔄")
342
-
343
- progress_text += f"{status_emoji} **{progress.filename}**\n"
344
- progress_text += f" Estado: {progress.status}\n"
345
- progress_text += f" Progreso: {progress.progress:.1f}%\n"
346
-
347
- if progress.original_size > 0:
348
- original_mb = progress.original_size / (1024 * 1024)
349
- progress_text += f" Tamaño original: {original_mb:.2f} MB\n"
350
- total_original += progress.original_size
351
-
352
- if progress.compressed_size > 0:
353
- compressed_mb = progress.compressed_size / (1024 * 1024)
354
- progress_text += f" Tamaño comprimido: {compressed_mb:.2f} MB\n"
355
- progress_text += f" Reducción: {progress.compression_ratio:.1f}%\n"
356
- total_compressed += progress.compressed_size
357
-
358
- if progress.error:
359
- progress_text += f" Error: {progress.error}\n"
360
- progress_text += "\n"
361
-
362
- # Estadísticas totales
363
- if total_original > 0:
364
- total_reduction = ((total_original - total_compressed) / total_original) * 100
365
- progress_text += f"**📈 Estadísticas Totales:**\n"
366
- progress_text += f"Reducción total: {total_reduction:.1f}% ({(total_original - total_compressed) / (1024 * 1024):.2f} MB ahorrados)\n"
367
-
368
- return progress_text
369
-
370
- def gradio_single_compression(file, quality_preset):
371
- """Interfaz Gradio para compresión de archivo único."""
372
- if not file:
373
- raise gr.Error("No se ha subido ningún archivo. Por favor, sube un archivo.")
374
-
375
- try:
376
- compressor.conversion_progress.clear()
377
- compressed_file = compressor.compress_single_file(file, quality_preset)
378
- return compressed_file, update_progress_display()
379
- except Exception as e:
380
- raise gr.Error(f"Compresión fallida: {str(e)}")
381
-
382
- def gradio_multiple_compression(files, quality_preset, max_workers):
383
- """Interfaz Gradio para compresión de múltiples archivos."""
384
- if not files:
385
- raise gr.Error("No se han subido archivos. Por favor, sube al menos un archivo.")
386
-
387
- try:
388
- compressor.conversion_progress.clear()
389
- file_paths = [file.name for file in files]
390
-
391
- compressed_files = compressor.compress_multiple_files(
392
- file_paths, quality_preset, max_workers=max_workers
393
- )
394
-
395
- if len(compressed_files) > 1:
396
- zip_filename = os.path.join(UPLOAD_DIR, f"compressed_files_{int(time.time())}.zip")
397
- with zipfile.ZipFile(zip_filename, 'w') as zipf:
398
- for file_path in compressed_files:
399
- zipf.write(file_path, os.path.basename(file_path))
400
- return zip_filename, update_progress_display()
401
- elif len(compressed_files) == 1:
402
- return compressed_files[0], update_progress_display()
403
- else:
404
- raise gr.Error("No se pudo comprimir ningún archivo.")
405
-
406
- except Exception as e:
407
- raise gr.Error(f"Compresión fallida: {str(e)}")
408
-
409
- def refresh_progress():
410
- """Refrescar estado del progreso."""
411
- return update_progress_display()
412
-
413
- # Interfaz Gradio mejorada
414
- with gr.Blocks(title="🗜️ Compresor Universal Pro Max", theme=gr.themes.Soft()) as iface:
415
- gr.Markdown(
416
- """
417
- # 🗜️ Compresor Universal Pro Max
418
-
419
- **El compresor más potente y rápido para todos tus archivos**
420
-
421
- ✨ **Características Avanzadas:**
422
- - 🚀 Compresión inteligente y optimizada para CPU
423
- - 🎬 Videos, audios, imágenes, GIFs y documentos
424
- - 🖼️ Soporte completo para PNG, JPG, WebP, TIFF, GIF animado
425
- - ⚡ Procesamiento paralelo ultra-rápido
426
- - 📊 Monitoreo detallado con estadísticas de compresión
427
- - 🔧 Algoritmos optimizados para máxima reducción de tamaño
428
- - 💾 Ahorro significativo de espacio en disco
429
- """
430
- )
431
-
432
- with gr.Tabs():
433
- with gr.TabItem("📄 Archivo Único"):
434
- with gr.Row():
435
- with gr.Column(scale=2):
436
- single_file_input = gr.File(
437
- label="📁 Subir Archivo (Todos los formatos soportados)",
438
- type="filepath",
439
- file_types=ALL_SUPPORTED_FORMATS
440
- )
441
- single_quality_input = gr.Dropdown(
442
- choices=list(QUALITY_PRESETS.keys()),
443
- value="Compresión Alta",
444
- label="🎯 Nivel de Compresión"
445
- )
446
- single_compress_btn = gr.Button(
447
- "🗜️ Comprimir Archivo",
448
- variant="primary",
449
- size="lg"
450
- )
451
-
452
- with gr.Column(scale=1):
453
- single_output = gr.File(
454
- label="📥 Descargar Archivo Comprimido",
455
- type="filepath"
456
- )
457
-
458
- with gr.TabItem("📁 Múltiples Archivos"):
459
- with gr.Row():
460
- with gr.Column(scale=2):
461
- multiple_files_input = gr.File(
462
- label="📁 Subir Múltiples Archivos",
463
- file_count="multiple",
464
- type="filepath",
465
- file_types=ALL_SUPPORTED_FORMATS
466
- )
467
- with gr.Row():
468
- multiple_quality_input = gr.Dropdown(
469
- choices=list(QUALITY_PRESETS.keys()),
470
- value="Compresión Alta",
471
- label="🎯 Nivel de Compresión"
472
- )
473
- workers_input = gr.Slider(
474
- minimum=1,
475
- maximum=min(psutil.cpu_count(), 6),
476
- value=min(psutil.cpu_count(logical=False), 3),
477
- step=1,
478
- label="⚡ Archivos Simultáneos"
479
- )
480
- multiple_compress_btn = gr.Button(
481
- "🗜️ Comprimir Todos los Archivos",
482
- variant="primary",
483
- size="lg"
484
- )
485
-
486
- with gr.Column(scale=1):
487
- multiple_output = gr.File(
488
- label="📥 Descargar Archivos Comprimidos",
489
- type="filepath"
490
- )
491
-
492
- # Panel de progreso mejorado
493
- gr.Markdown("## 📊 Estado y Estadísticas de Compresión")
494
- progress_display = gr.Markdown(
495
- value="No hay compresiones en progreso.",
496
- label="Estado"
497
- )
498
- refresh_btn = gr.Button("🔄 Actualizar Estado", size="sm")
499
-
500
- # Información detallada
501
- with gr.Accordion("ℹ️ Información Detallada", open=False):
502
- gr.Markdown(
503
- f"""
504
- ### 🎯 Niveles de Compresión:
505
-
506
- - **Compresión Máxima**: Máxima reducción de tamaño, proceso más lento
507
- - **Compresión Alta**: Excelente balance compresión/velocidad
508
- - **Compresión Media**: Buena compresión, procesamiento rápido
509
- - **Compresión Rápida**: Compresión básica, velocidad máxima
510
-
511
- ### 📁 Formatos Soportados:
512
-
513
- **🎬 Video**: {', '.join(VIDEO_FORMATS)}
514
-
515
- **🎵 Audio**: {', '.join(AUDIO_FORMATS)}
516
-
517
- ### ⚡ Optimizaciones:
518
-
519
- - **CPU Cores Detectados**: {psutil.cpu_count()} (Lógicos), {psutil.cpu_count(logical=False)} (Físicos)
520
- - **Threads Optimizados**: Máximo {min(psutil.cpu_count(logical=False), 4)} para video, {min(psutil.cpu_count(logical=False), 2)} para audio
521
- - **Compresión Inteligente**: Algoritmos adaptativos según tipo de archivo
522
- - **Gestión de Memoria**: Optimizada para archivos grandes
523
- """
524
- )
525
-
526
- # Eventos
527
- single_compress_btn.click(
528
- fn=gradio_single_compression,
529
- inputs=[single_file_input, single_quality_input],
530
- outputs=[single_output, progress_display]
531
- )
532
-
533
- multiple_compress_btn.click(
534
- fn=gradio_multiple_compression,
535
- inputs=[multiple_files_input, multiple_quality_input, workers_input],
536
- outputs=[multiple_output, progress_display]
537
- )
538
-
539
- refresh_btn.click(
540
- fn=refresh_progress,
541
- outputs=[progress_display]
542
- )
543
-
544
- # Lanzar interfaz
545
- if __name__ == "__main__":
546
- iface.launch(
547
- share=True,
548
- server_name="0.0.0.0",
549
- server_port=7861,
550
- show_error=True,
551
- max_threads=min(psutil.cpu_count(), 10) # Limitar threads de Gradio
552
  )
 
1
+ import os
2
+ import subprocess
3
+ import logging
4
+ import asyncio
5
+ import threading
6
+ import time
7
+ import psutil
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from typing import Optional, List, Tuple, Dict, Any
10
+ from pathlib import Path
11
+ import gradio as gr
12
+ from dataclasses import dataclass
13
+ import json
14
+ import zipfile
15
+ import shutil
16
+
17
+
18
+ from io import BytesIO
19
+
20
+ # Configuración de logging
21
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Directorios y rutas
25
+ UPLOAD_DIR = os.path.join(os.getcwd(), "uploads")
26
+ FFMPEG_PATH = os.path.join(os.path.dirname(__file__), "ffmpeg.exe") # Ruta al ejecutable ffmpeg
27
+
28
+ # Crear directorios
29
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
30
+
31
+ # Definir formatos soportados (solo video y audio)
32
+ VIDEO_FORMATS = ['.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v', '.3gp', '.ts', '.mts', '.mpg', '.mpeg', '.ogv']
33
+ AUDIO_FORMATS = ['.mp3', '.wav', '.aac', '.flac', '.ogg', '.m4a', '.wma', '.opus', '.ac3', '.dts', '.amr', '.aiff']
34
+
35
+ ALL_SUPPORTED_FORMATS = VIDEO_FORMATS + AUDIO_FORMATS
36
+
37
+ # Configuraciones de compresión agresiva para reducir tamaño
38
+ QUALITY_PRESETS = {
39
+ "Compresión Máxima": {
40
+ "video_bitrate": "200k",
41
+ "audio_bitrate": "32k",
42
+ "preset": "slow",
43
+ "crf": "35"
44
+ },
45
+ "Compresión Alta": {
46
+ "video_bitrate": "400k",
47
+ "audio_bitrate": "48k",
48
+ "preset": "medium",
49
+ "crf": "30"
50
+ },
51
+ "Compresión Media": {
52
+ "video_bitrate": "600k",
53
+ "audio_bitrate": "64k",
54
+ "preset": "fast",
55
+ "crf": "28"
56
+ },
57
+ "Compresión Rápida": {
58
+ "video_bitrate": "800k",
59
+ "audio_bitrate": "96k",
60
+ "preset": "veryfast",
61
+ "crf": "25"
62
+ }
63
+ }
64
+
65
+ @dataclass
66
+ class ConversionProgress:
67
+ filename: str
68
+ status: str
69
+ progress: float
70
+ original_size: int = 0
71
+ compressed_size: int = 0
72
+ compression_ratio: float = 0.0
73
+ output_file: Optional[str] = None
74
+ error: Optional[str] = None
75
+
76
+ class UniversalCompressor:
77
+ def __init__(self):
78
+ self.conversion_progress = {}
79
+ self.active_conversions = 0
80
+ self.cpu_cores = psutil.cpu_count(logical=False)
81
+
82
+ def get_file_size(self, file_path: str) -> int:
83
+ """Obtener tamaño del archivo en bytes."""
84
+ try:
85
+ return os.path.getsize(file_path)
86
+ except:
87
+ return 0
88
+
89
+ def detect_file_type(self, file_path: str) -> str:
90
+ """Detectar tipo de archivo basado en extensión."""
91
+ ext = os.path.splitext(file_path)[1].lower()
92
+ if ext in VIDEO_FORMATS:
93
+ return 'video'
94
+ elif ext in AUDIO_FORMATS:
95
+ return 'audio'
96
+ else:
97
+ return 'unsupported'
98
+
99
+ def sanitize_filename(self, filename: str) -> str:
100
+ """Limpiar y validar nombre de archivo."""
101
+ return ''.join(c for c in filename if c.isalnum() or c in ('.', '_', '-')).rstrip()
102
+
103
+ def ensure_unique_filename(self, directory: str, filename: str) -> str:
104
+ """Generar nombre único para evitar sobreescrituras."""
105
+ base, ext = os.path.splitext(filename)
106
+ counter = 1
107
+ new_filename = filename
108
+ while os.path.exists(os.path.join(directory, new_filename)):
109
+ new_filename = f"{base}_{counter}{ext}"
110
+ counter += 1
111
+ return new_filename
112
+
113
+
114
+
115
+ def compress_media(self, input_file: str, quality_preset: str, progress_callback=None) -> str:
116
+ """Comprimir video/audio con configuraciones optimizadas para CPU."""
117
+ try:
118
+ media_type = self.detect_file_type(input_file)
119
+ base_name = os.path.basename(input_file)
120
+ file_id = f"{base_name}_{int(time.time())}"
121
+
122
+ # Inicializar progreso
123
+ original_size = self.get_file_size(input_file)
124
+ self.conversion_progress[file_id] = ConversionProgress(
125
+ filename=base_name,
126
+ status="Iniciando compresión...",
127
+ progress=0.0,
128
+ original_size=original_size
129
+ )
130
+
131
+ if progress_callback:
132
+ progress_callback(file_id, self.conversion_progress[file_id])
133
+
134
+ # Determinar extensión de salida
135
+ output_extension = 'mp4' if media_type == 'video' else 'm4a'
136
+ output_filename = self.ensure_unique_filename(
137
+ UPLOAD_DIR,
138
+ f"{os.path.splitext(base_name)[0]}_compressed.{output_extension}"
139
+ )
140
+ output_file = os.path.join(UPLOAD_DIR, output_filename)
141
+
142
+ quality_config = QUALITY_PRESETS[quality_preset]
143
+
144
+ # Configuraciones FFmpeg optimizadas para menor uso de CPU
145
+ if media_type == 'video':
146
+ ffmpeg_command = [
147
+ FFMPEG_PATH,
148
+ '-i', input_file,
149
+ '-c:v', 'libx264',
150
+ '-crf', quality_config['crf'],
151
+ '-preset', quality_config['preset'],
152
+ '-c:a', 'aac',
153
+ '-b:a', quality_config['audio_bitrate'],
154
+ '-movflags', '+faststart',
155
+ '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
156
+ '-threads', str(min(self.cpu_cores, 4)), # Limitar threads para menor uso CPU
157
+ '-y',
158
+ output_file
159
+ ]
160
+ else: # audio
161
+ ffmpeg_command = [
162
+ FFMPEG_PATH,
163
+ '-i', input_file,
164
+ '-vn',
165
+ '-c:a', 'aac',
166
+ '-b:a', quality_config['audio_bitrate'],
167
+ '-ar', '44100',
168
+ '-threads', str(min(self.cpu_cores, 2)),
169
+ '-y',
170
+ output_file
171
+ ]
172
+
173
+ # Ejecutar conversión
174
+ self.conversion_progress[file_id].status = "Comprimiendo..."
175
+ self.conversion_progress[file_id].progress = 25.0
176
+ if progress_callback:
177
+ progress_callback(file_id, self.conversion_progress[file_id])
178
+
179
+ process = subprocess.Popen(
180
+ ffmpeg_command,
181
+ stdout=subprocess.PIPE,
182
+ stderr=subprocess.PIPE,
183
+ text=True
184
+ )
185
+
186
+ # Simular progreso
187
+ for i in range(25, 90, 10):
188
+ if process.poll() is None:
189
+ self.conversion_progress[file_id].progress = float(i)
190
+ if progress_callback:
191
+ progress_callback(file_id, self.conversion_progress[file_id])
192
+ time.sleep(0.3)
193
+ else:
194
+ break
195
+
196
+ stdout, stderr = process.communicate()
197
+
198
+ if process.returncode != 0:
199
+ error_msg = f"Error FFmpeg: {stderr}"
200
+ self.conversion_progress[file_id].status = "Error"
201
+ self.conversion_progress[file_id].error = error_msg
202
+ if progress_callback:
203
+ progress_callback(file_id, self.conversion_progress[file_id])
204
+ raise Exception(error_msg)
205
+
206
+ # Finalizar
207
+ compressed_size = self.get_file_size(output_file)
208
+ compression_ratio = ((original_size - compressed_size) / original_size) * 100 if original_size > 0 else 0
209
+
210
+ self.conversion_progress[file_id].status = "Completado"
211
+ self.conversion_progress[file_id].progress = 100.0
212
+ self.conversion_progress[file_id].compressed_size = compressed_size
213
+ self.conversion_progress[file_id].compression_ratio = compression_ratio
214
+ self.conversion_progress[file_id].output_file = output_file
215
+
216
+ if progress_callback:
217
+ progress_callback(file_id, self.conversion_progress[file_id])
218
+
219
+ logger.info(f"Archivo comprimido: {output_file} (Reducción: {compression_ratio:.1f}%)")
220
+ return output_file
221
+
222
+ except Exception as e:
223
+ error_msg = f"Error durante compresión: {str(e)}"
224
+ logger.error(error_msg)
225
+ if file_id in self.conversion_progress:
226
+ self.conversion_progress[file_id].status = "Error"
227
+ self.conversion_progress[file_id].error = error_msg
228
+ if progress_callback:
229
+ progress_callback(file_id, self.conversion_progress[file_id])
230
+ raise Exception(error_msg)
231
+
232
+ def compress_archive(self, input_file: str, progress_callback=None) -> str:
233
+ """Recomprimir archivos comprimidos para mejor ratio."""
234
+ try:
235
+ base_name = os.path.basename(input_file)
236
+ file_id = f"{base_name}_{int(time.time())}"
237
+
238
+ original_size = self.get_file_size(input_file)
239
+ self.conversion_progress[file_id] = ConversionProgress(
240
+ filename=base_name,
241
+ status="Recomprimiendo archivo...",
242
+ progress=50.0,
243
+ original_size=original_size
244
+ )
245
+
246
+ if progress_callback:
247
+ progress_callback(file_id, self.conversion_progress[file_id])
248
+
249
+ # Crear archivo 7z con máxima compresión
250
+ output_filename = self.ensure_unique_filename(
251
+ UPLOAD_DIR, f"{os.path.splitext(base_name)[0]}_compressed.7z"
252
+ )
253
+ output_file = os.path.join(UPLOAD_DIR, output_filename)
254
+
255
+ # Usar 7z para recompresión
256
+ import py7zr
257
+ with py7zr.SevenZipFile(output_file, 'w', filters=[{"id": py7zr.FILTER_LZMA2, "preset": 9}]) as archive:
258
+ archive.write(input_file, base_name)
259
+
260
+ compressed_size = self.get_file_size(output_file)
261
+ compression_ratio = ((original_size - compressed_size) / original_size) * 100 if original_size > 0 else 0
262
+
263
+ self.conversion_progress[file_id].status = "Completado"
264
+ self.conversion_progress[file_id].progress = 100.0
265
+ self.conversion_progress[file_id].compressed_size = compressed_size
266
+ self.conversion_progress[file_id].compression_ratio = compression_ratio
267
+ self.conversion_progress[file_id].output_file = output_file
268
+
269
+ if progress_callback:
270
+ progress_callback(file_id, self.conversion_progress[file_id])
271
+
272
+ return output_file
273
+
274
+ except Exception as e:
275
+ error_msg = f"Error recomprimiendo archivo: {str(e)}"
276
+ logger.error(error_msg)
277
+ if file_id in self.conversion_progress:
278
+ self.conversion_progress[file_id].status = "Error"
279
+ self.conversion_progress[file_id].error = error_msg
280
+ if progress_callback:
281
+ progress_callback(file_id, self.conversion_progress[file_id])
282
+ raise Exception(error_msg)
283
+
284
+ def compress_single_file(self, input_file: str, quality_preset: str, progress_callback=None) -> str:
285
+ """Comprimir un archivo según su tipo."""
286
+ if not os.path.exists(input_file):
287
+ raise ValueError(f"El archivo no existe: {input_file}")
288
+
289
+ file_type = self.detect_file_type(input_file)
290
+
291
+ if file_type in ['video', 'audio']:
292
+ return self.compress_media(input_file, quality_preset, progress_callback)
293
+ else:
294
+ raise ValueError(f"Tipo de archivo no soportado: {os.path.splitext(input_file)[1]}. Solo se soportan videos y audios: {', '.join(ALL_SUPPORTED_FORMATS)}")
295
+
296
+ def compress_multiple_files(self, file_paths: List[str], quality_preset: str, max_workers: int = None, progress_callback=None) -> List[str]:
297
+ """Comprimir múltiples archivos en paralelo con optimización de CPU."""
298
+ if max_workers is None:
299
+ max_workers = min(self.cpu_cores, 3) # Limitar para no saturar CPU
300
+
301
+ compressed_files = []
302
+ self.active_conversions = len(file_paths)
303
+
304
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
305
+ future_to_file = {
306
+ executor.submit(self.compress_single_file, file_path, quality_preset, progress_callback): file_path
307
+ for file_path in file_paths
308
+ }
309
+
310
+ for future in as_completed(future_to_file):
311
+ file_path = future_to_file[future]
312
+ try:
313
+ result = future.result()
314
+ compressed_files.append(result)
315
+ except Exception as e:
316
+ logger.error(f"Error comprimiendo {file_path}: {e}")
317
+
318
+ self.active_conversions = 0
319
+ return compressed_files
320
+
321
+ # Instancia global del compresor
322
+ compressor = UniversalCompressor()
323
+
324
+ def update_progress_display():
325
+ """Actualizar visualización del progreso con información detallada."""
326
+ if not compressor.conversion_progress:
327
+ return "No hay compresiones en progreso."
328
+
329
+ progress_text = "📊 **Estado de Compresiones:**\n\n"
330
+ total_original = 0
331
+ total_compressed = 0
332
+
333
+ for file_id, progress in compressor.conversion_progress.items():
334
+ status_emoji = {
335
+ "Iniciando compresión...": "🔄",
336
+ "Procesando imagen...": "🖼️",
337
+ "Comprimiendo...": "⚙️",
338
+ "Recomprimiendo archivo...": "📦",
339
+ "Completado": "✅",
340
+ "Error": "❌"
341
+ }.get(progress.status, "🔄")
342
+
343
+ progress_text += f"{status_emoji} **{progress.filename}**\n"
344
+ progress_text += f" Estado: {progress.status}\n"
345
+ progress_text += f" Progreso: {progress.progress:.1f}%\n"
346
+
347
+ if progress.original_size > 0:
348
+ original_mb = progress.original_size / (1024 * 1024)
349
+ progress_text += f" Tamaño original: {original_mb:.2f} MB\n"
350
+ total_original += progress.original_size
351
+
352
+ if progress.compressed_size > 0:
353
+ compressed_mb = progress.compressed_size / (1024 * 1024)
354
+ progress_text += f" Tamaño comprimido: {compressed_mb:.2f} MB\n"
355
+ progress_text += f" Reducción: {progress.compression_ratio:.1f}%\n"
356
+ total_compressed += progress.compressed_size
357
+
358
+ if progress.error:
359
+ progress_text += f" Error: {progress.error}\n"
360
+ progress_text += "\n"
361
+
362
+ # Estadísticas totales
363
+ if total_original > 0:
364
+ total_reduction = ((total_original - total_compressed) / total_original) * 100
365
+ progress_text += f"**📈 Estadísticas Totales:**\n"
366
+ progress_text += f"Reducción total: {total_reduction:.1f}% ({(total_original - total_compressed) / (1024 * 1024):.2f} MB ahorrados)\n"
367
+
368
+ return progress_text
369
+
370
+ def gradio_single_compression(file, quality_preset):
371
+ """Interfaz Gradio para compresión de archivo único."""
372
+ if not file:
373
+ raise gr.Error("No se ha subido ningún archivo. Por favor, sube un archivo.")
374
+
375
+ try:
376
+ compressor.conversion_progress.clear()
377
+ compressed_file = compressor.compress_single_file(file, quality_preset)
378
+ return compressed_file, update_progress_display()
379
+ except Exception as e:
380
+ raise gr.Error(f"Compresión fallida: {str(e)}")
381
+
382
+ def gradio_multiple_compression(files, quality_preset, max_workers):
383
+ """Interfaz Gradio para compresión de múltiples archivos."""
384
+ if not files:
385
+ raise gr.Error("No se han subido archivos. Por favor, sube al menos un archivo.")
386
+
387
+ try:
388
+ compressor.conversion_progress.clear()
389
+ file_paths = [file.name for file in files]
390
+
391
+ compressed_files = compressor.compress_multiple_files(
392
+ file_paths, quality_preset, max_workers=max_workers
393
+ )
394
+
395
+ if len(compressed_files) > 1:
396
+ zip_filename = os.path.join(UPLOAD_DIR, f"compressed_files_{int(time.time())}.zip")
397
+ with zipfile.ZipFile(zip_filename, 'w') as zipf:
398
+ for file_path in compressed_files:
399
+ zipf.write(file_path, os.path.basename(file_path))
400
+ return zip_filename, update_progress_display()
401
+ elif len(compressed_files) == 1:
402
+ return compressed_files[0], update_progress_display()
403
+ else:
404
+ raise gr.Error("No se pudo comprimir ningún archivo.")
405
+
406
+ except Exception as e:
407
+ raise gr.Error(f"Compresión fallida: {str(e)}")
408
+
409
+ def refresh_progress():
410
+ """Refrescar estado del progreso."""
411
+ return update_progress_display()
412
+
413
+ # Interfaz Gradio mejorada
414
+ with gr.Blocks(title="🗜️ Compresor Universal Pro Max", theme=gr.themes.Soft()) as iface:
415
+ gr.Markdown(
416
+ """
417
+ # 🗜️ Compresor Universal Pro Max
418
+
419
+ **El compresor más potente y rápido para todos tus archivos**
420
+
421
+ ✨ **Características Avanzadas:**
422
+ - 🚀 Compresión inteligente y optimizada para CPU
423
+ - 🎬 Videos, audios, imágenes, GIFs y documentos
424
+ - 🖼️ Soporte completo para PNG, JPG, WebP, TIFF, GIF animado
425
+ - ⚡ Procesamiento paralelo ultra-rápido
426
+ - 📊 Monitoreo detallado con estadísticas de compresión
427
+ - 🔧 Algoritmos optimizados para máxima reducción de tamaño
428
+ - 💾 Ahorro significativo de espacio en disco
429
+ """
430
+ )
431
+
432
+ with gr.Tabs():
433
+ with gr.TabItem("📄 Archivo Único"):
434
+ with gr.Row():
435
+ with gr.Column(scale=2):
436
+ single_file_input = gr.File(
437
+ label="📁 Subir Archivo (Todos los formatos soportados)",
438
+ type="filepath",
439
+ file_types=ALL_SUPPORTED_FORMATS
440
+ )
441
+ single_quality_input = gr.Dropdown(
442
+ choices=list(QUALITY_PRESETS.keys()),
443
+ value="Compresión Alta",
444
+ label="🎯 Nivel de Compresión"
445
+ )
446
+ single_compress_btn = gr.Button(
447
+ "🗜️ Comprimir Archivo",
448
+ variant="primary",
449
+ size="lg"
450
+ )
451
+
452
+ with gr.Column(scale=1):
453
+ single_output = gr.File(
454
+ label="📥 Descargar Archivo Comprimido",
455
+ type="filepath"
456
+ )
457
+
458
+ with gr.TabItem("📁 Múltiples Archivos"):
459
+ with gr.Row():
460
+ with gr.Column(scale=2):
461
+ multiple_files_input = gr.File(
462
+ label="📁 Subir Múltiples Archivos",
463
+ file_count="multiple",
464
+ type="filepath",
465
+ file_types=ALL_SUPPORTED_FORMATS
466
+ )
467
+ with gr.Row():
468
+ multiple_quality_input = gr.Dropdown(
469
+ choices=list(QUALITY_PRESETS.keys()),
470
+ value="Compresión Alta",
471
+ label="🎯 Nivel de Compresión"
472
+ )
473
+ workers_input = gr.Slider(
474
+ minimum=1,
475
+ maximum=min(psutil.cpu_count(), 6),
476
+ value=min(psutil.cpu_count(logical=False), 3),
477
+ step=1,
478
+ label="⚡ Archivos Simultáneos"
479
+ )
480
+ multiple_compress_btn = gr.Button(
481
+ "🗜️ Comprimir Todos los Archivos",
482
+ variant="primary",
483
+ size="lg"
484
+ )
485
+
486
+ with gr.Column(scale=1):
487
+ multiple_output = gr.File(
488
+ label="📥 Descargar Archivos Comprimidos",
489
+ type="filepath"
490
+ )
491
+
492
+ # Panel de progreso mejorado
493
+ gr.Markdown("## 📊 Estado y Estadísticas de Compresión")
494
+ progress_display = gr.Markdown(
495
+ value="No hay compresiones en progreso.",
496
+ label="Estado"
497
+ )
498
+ refresh_btn = gr.Button("🔄 Actualizar Estado", size="sm")
499
+
500
+ # Información detallada
501
+ with gr.Accordion("ℹ️ Información Detallada", open=False):
502
+ gr.Markdown(
503
+ f"""
504
+ ### 🎯 Niveles de Compresión:
505
+
506
+ - **Compresión Máxima**: Máxima reducción de tamaño, proceso más lento
507
+ - **Compresión Alta**: Excelente balance compresión/velocidad
508
+ - **Compresión Media**: Buena compresión, procesamiento rápido
509
+ - **Compresión Rápida**: Compresión básica, velocidad máxima
510
+
511
+ ### 📁 Formatos Soportados:
512
+
513
+ **🎬 Video**: {', '.join(VIDEO_FORMATS)}
514
+
515
+ **🎵 Audio**: {', '.join(AUDIO_FORMATS)}
516
+
517
+ ### ⚡ Optimizaciones:
518
+
519
+ - **CPU Cores Detectados**: {psutil.cpu_count()} (Lógicos), {psutil.cpu_count(logical=False)} (Físicos)
520
+ - **Threads Optimizados**: Máximo {min(psutil.cpu_count(logical=False), 4)} para video, {min(psutil.cpu_count(logical=False), 2)} para audio
521
+ - **Compresión Inteligente**: Algoritmos adaptativos según tipo de archivo
522
+ - **Gestión de Memoria**: Optimizada para archivos grandes
523
+ """
524
+ )
525
+
526
+ # Eventos
527
+ single_compress_btn.click(
528
+ fn=gradio_single_compression,
529
+ inputs=[single_file_input, single_quality_input],
530
+ outputs=[single_output, progress_display]
531
+ )
532
+
533
+ multiple_compress_btn.click(
534
+ fn=gradio_multiple_compression,
535
+ inputs=[multiple_files_input, multiple_quality_input, workers_input],
536
+ outputs=[multiple_output, progress_display]
537
+ )
538
+
539
+ refresh_btn.click(
540
+ fn=refresh_progress,
541
+ outputs=[progress_display]
542
+ )
543
+
544
+ # Lanzar interfaz
545
+ if __name__ == "__main__":
546
+ iface.launch(
547
+ server_name="0.0.0.0",
548
+ server_port=7861,
549
+ show_error=True,
550
+ max_threads=min(psutil.cpu_count(), 10) # Limitar threads de Gradio
 
551
  )