gnosticdev commited on
Commit
6f71c41
verified
1 Parent(s): ca5997d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +59 -187
app.py CHANGED
@@ -2,8 +2,6 @@ import tempfile
2
  import logging
3
  import os
4
  import asyncio
5
- import gc
6
- import psutil
7
  from moviepy.editor import *
8
  import edge_tts
9
  import gradio as gr
@@ -18,25 +16,15 @@ OUTRO_VIDEO = "outrovideo.mp4"
18
  MUSIC_BG = "musicafondo.mp3"
19
  EJEMPLO_VIDEO = "ejemplo.mp4"
20
 
21
- # CONSTANTES DE LIMITACIONES
22
- MAX_VIDEO_DURATION = 300 # M谩xima duraci贸n en segundos (5 minutos)
23
- MAX_VIDEO_SIZE = 100 * 1024 * 1024 # Tama帽o m谩ximo en bytes (100MB)
24
- MAX_RESOLUTION = (1280, 720) # Resoluci贸n m谩xima (720p)
25
-
26
- # Configuraci贸n de chunks
27
- SEGMENT_DURATION = 30 # Duraci贸n exacta entre transiciones (sin overlap)
28
- TRANSITION_DURATION = 1.5 # Duraci贸n del efecto slide
29
-
30
  # Validar existencia de archivos
31
  for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, EJEMPLO_VIDEO]:
32
  if not os.path.exists(file):
33
  logging.error(f"Falta archivo necesario: {file}")
34
  raise FileNotFoundError(f"Falta: {file}")
35
 
36
- def mostrar_uso_memoria():
37
- proceso = psutil.Process(os.getpid())
38
- memoria_uso = proceso.memory_info().rss / 1024 / 1024
39
- logging.info(f"Uso de memoria: {memoria_uso:.2f} MB")
40
 
41
  def eliminar_archivo_tiempo(ruta, delay=1800):
42
  def eliminar():
@@ -51,22 +39,8 @@ def eliminar_archivo_tiempo(ruta, delay=1800):
51
 
52
  def validar_video(video_path):
53
  try:
54
- # Comprobar tama帽o del archivo
55
- file_size = os.path.getsize(video_path)
56
- if file_size > MAX_VIDEO_SIZE:
57
- logging.warning(f"El video excede el tama帽o m谩ximo: {file_size/1024/1024:.2f}MB > {MAX_VIDEO_SIZE/1024/1024}MB")
58
- return False
59
-
60
- # Validar que es un video
61
  clip = VideoFileClip(video_path)
62
- duracion = clip.duration
63
  clip.close()
64
-
65
- # Comprobar duraci贸n
66
- if duracion > MAX_VIDEO_DURATION:
67
- logging.warning(f"El video excede la duraci贸n m谩xima: {duracion}s > {MAX_VIDEO_DURATION}s")
68
- return False
69
-
70
  return True
71
  except Exception as e:
72
  logging.error(f"El video no es v谩lido: {e}")
@@ -76,39 +50,30 @@ def convertir_video(video_path):
76
  try:
77
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_converted:
78
  output_path = tmp_converted.name
79
-
80
- # Primero convertir a un formato m谩s eficiente y con menor resoluci贸n
81
- os.system(f'ffmpeg -i "{video_path}" -vf "scale=640:360" -c:v libx264 -crf 28 -preset ultrafast -c:a aac -b:a 96k "{output_path}" -y')
82
-
83
- # Comprobar si ahora cumple las limitaciones
84
- if not validar_video(output_path):
85
- # Si sigue sin cumplir, recortar duraci贸n
86
- nuevo_clip = VideoFileClip(output_path)
87
- duracion_maxima = min(nuevo_clip.duration, MAX_VIDEO_DURATION)
88
- nuevo_clip = nuevo_clip.subclip(0, duracion_maxima)
89
-
90
- temp_recortado = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
91
- nuevo_clip.write_videofile(temp_recortado, codec="libx264", audio_codec="aac",
92
- preset="ultrafast", bitrate="1M")
93
- nuevo_clip.close()
94
-
95
- os.remove(output_path)
96
- return temp_recortado
97
-
98
  return output_path
99
  except Exception as e:
100
  logging.error(f"Error al convertir el video: {e}")
101
  raise
102
 
 
 
 
 
 
 
 
 
 
 
103
  async def generar_tts(texto, voz, duracion_total):
104
  try:
105
  if not texto.strip():
106
  raise ValueError("El texto para TTS no puede estar vac铆o.")
107
- # Limitar el texto a 500 caracteres para procesar m谩s r谩pido
108
- if len(texto) > 500:
109
- texto = texto[:500]
110
- logging.info("Texto para TTS truncado a 500 caracteres para optimizar rendimiento")
111
 
 
112
  logging.info(f"Generando TTS con voz: {voz}")
113
  communicate = edge_tts.Communicate(texto, voz)
114
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_tts:
@@ -139,85 +104,44 @@ def create_slide_transition(clip1, clip2, duration=TRANSITION_DURATION):
139
  part2.fx(vfx.fadein, duration).set_position(
140
  lambda t: ('center', 720 - (720 * (t/duration)))
141
  )
142
- ], size=(640, 360)).set_duration(duration) # Reducido a 640x360 para optimizar
143
  return transition
144
 
145
- def liberar_memoria(objetos_cerrar=None):
146
- """Forzar liberaci贸n de memoria cerrando objetos y llamando al recolector de basura"""
147
- if objetos_cerrar:
148
- for obj in objetos_cerrar:
149
- if obj is not None:
150
- try:
151
- obj.close()
152
- except:
153
- pass
154
-
155
- # Forzar recolecci贸n de basura
156
- gc.collect()
157
- mostrar_uso_memoria()
158
-
159
- async def procesar_video(video_input, texto_tts, voz_seleccionada, progress=gr.Progress()):
160
  temp_files = []
161
  intro, outro, video_original = None, None, None
162
- segmentos_temp = []
163
-
164
  try:
165
- mostrar_uso_memoria()
166
  logging.info("Iniciando procesamiento")
167
- progress(0, desc="Validando video")
168
 
169
  if not validar_video(video_input):
170
- progress(0.05, desc="Convirtiendo formato de video")
171
  video_input = convertir_video(video_input)
172
  temp_files.append(video_input)
173
-
174
- progress(0.1, desc="Preparando video")
175
- # Reducir resoluci贸n para optimizar procesamiento
176
- video_original = VideoFileClip(video_input)
177
  duracion_video = video_original.duration
178
 
179
- # Limitar duraci贸n si es necesario
180
- if duracion_video > MAX_VIDEO_DURATION:
181
- duracion_video = MAX_VIDEO_DURATION
182
- video_original = video_original.subclip(0, duracion_video)
183
-
184
  if duracion_video <= 0:
185
  raise ValueError("El video debe tener una duraci贸n mayor que cero.")
186
 
187
- progress(0.2, desc="Generando narraci贸n (TTS)")
188
  tts_audio, tts_path = await generar_tts(texto_tts, voz_seleccionada, duracion_video)
189
- temp_files.append(tts_path)
190
-
191
- progress(0.3, desc="Preparando m煤sica de fondo")
192
  bg_audio, bg_path = crear_musica_fondo(duracion_video)
193
- temp_files.append(bg_path)
194
 
195
- progress(0.35, desc="Mezclando audio")
196
- audio_original = video_original.audio.volumex(0.5) if video_original.audio else None
197
  audios = [bg_audio.set_duration(duracion_video)]
198
  if audio_original:
199
  audios.append(audio_original)
200
  audios.append(tts_audio.set_start(0).volumex(0.85))
201
  audio_final = CompositeAudioClip(audios).set_duration(duracion_video)
202
 
203
- # Procesar por segmentos para optimizar memoria
204
  if duracion_video > SEGMENT_DURATION:
205
- progress(0.4, desc="Procesando segmentos de video")
206
  clips = []
207
  num_segments = int(duracion_video // SEGMENT_DURATION) + (1 if duracion_video % SEGMENT_DURATION > 0 else 0)
208
-
209
  for i in range(num_segments):
210
- progress_val = 0.4 + (0.3 * (i / num_segments))
211
- progress(progress_val, desc=f"Procesando segmento {i+1}/{num_segments}")
212
-
213
  start_time = i * SEGMENT_DURATION
214
  end_time = min(start_time + SEGMENT_DURATION, duracion_video)
215
  segment = video_original.subclip(start_time, end_time)
216
-
217
- # Reducir resoluci贸n si es necesario
218
- if segment.size[0] > MAX_RESOLUTION[0] or segment.size[1] > MAX_RESOLUTION[1]:
219
- segment = segment.resize(height=MAX_RESOLUTION[1])
220
-
221
  if i == 0:
222
  clips.append(segment)
223
  else:
@@ -228,88 +152,48 @@ async def procesar_video(video_input, texto_tts, voz_seleccionada, progress=gr.P
228
  clips[-1] = prev_segment.subclip(0, prev_end)
229
  clips.append(transition)
230
  clips.append(segment)
231
-
232
- # Liberar memoria despu茅s de cada 2 segmentos
233
- if i % 2 == 1:
234
- liberar_memoria()
235
-
236
  video_final = concatenate_videoclips(clips, method="compose")
237
- else:
238
- video_final = video_original.copy()
239
-
240
- # Asignar audio final
241
- progress(0.7, desc="Asignando audio")
242
- video_final = video_final.set_audio(audio_final)
243
 
244
- # A帽adir intro y outro
245
- progress(0.75, desc="A帽adiendo intro y outro")
246
- intro = VideoFileClip(INTRO_VIDEO, target_resolution=(360, 640)) # Reducido para optimizar
247
- outro = VideoFileClip(OUTRO_VIDEO, target_resolution=(360, 640)) # Reducido para optimizar
248
-
249
- # Crear el video final por partes
250
- with tempfile.NamedTemporaryFile(delete=False, suffix="_intro.mp4") as tmp_intro:
251
- intro.write_videofile(tmp_intro.name, codec="libx264", audio_codec="aac",
252
- preset="ultrafast", bitrate="1M",
253
- ffmpeg_params=["-crf", "30"])
254
- segmentos_temp.append(tmp_intro.name)
255
-
256
- with tempfile.NamedTemporaryFile(delete=False, suffix="_main.mp4") as tmp_main:
257
- video_final.write_videofile(tmp_main.name, codec="libx264", audio_codec="aac",
258
- preset="ultrafast", bitrate="1M",
259
- ffmpeg_params=["-crf", "30"])
260
- segmentos_temp.append(tmp_main.name)
261
-
262
- with tempfile.NamedTemporaryFile(delete=False, suffix="_outro.mp4") as tmp_outro:
263
- outro.write_videofile(tmp_outro.name, codec="libx264", audio_codec="aac",
264
- preset="ultrafast", bitrate="1M",
265
- ffmpeg_params=["-crf", "30"])
266
- segmentos_temp.append(tmp_outro.name)
267
-
268
- # Liberar memoria antes de la uni贸n final
269
- liberar_memoria([video_original, intro, outro, video_final])
270
- video_original = intro = outro = video_final = None
271
 
272
- # Unir los segmentos con ffmpeg directamente
273
- progress(0.9, desc="Generando video final")
274
- with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as concat_file:
275
- # Escribir archivo de lista para concatenaci贸n
276
- for segment in segmentos_temp:
277
- concat_file.write(f"file '{segment}'\n".encode())
278
- concat_path = concat_file.name
279
-
280
- with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp_final:
281
- output_path = tmp_final.name
282
- os.system(f'ffmpeg -f concat -safe 0 -i "{concat_path}" -c copy "{output_path}" -y')
283
-
284
- # Limpiar archivos temporales
285
- os.remove(concat_path)
286
- for segment in segmentos_temp:
287
- if os.path.exists(segment):
288
- os.remove(segment)
289
-
290
- eliminar_archivo_tiempo(output_path, 3600) # Extendido a 1 hora
291
- progress(1.0, desc="隆Video listo!")
292
- logging.info(f"Video final guardado: {output_path}")
293
- mostrar_uso_memoria()
294
- return output_path
295
  except Exception as e:
296
  logging.error(f"Fallo general: {str(e)}")
297
  raise
298
  finally:
299
  try:
300
- liberar_memoria([video_original, intro, outro])
 
 
 
 
 
301
  for file in temp_files:
302
  try:
303
- if os.path.exists(file):
304
- os.remove(file)
305
  except Exception as e:
306
  logging.warning(f"Error limpiando {file}: {e}")
307
- for segment in segmentos_temp:
308
- try:
309
- if os.path.exists(segment):
310
- os.remove(segment)
311
- except Exception as e:
312
- logging.warning(f"Error limpiando segmento {segment}: {e}")
313
  except Exception as e:
314
  logging.warning(f"Error al cerrar recursos: {str(e)}")
315
 
@@ -319,7 +203,7 @@ with gr.Blocks() as demo:
319
  with gr.Tab("Principal"):
320
  video_input = gr.Video(label="Subir video")
321
  texto_tts = gr.Textbox(
322
- label="Texto para TTS (m谩x. 500 caracteres)",
323
  lines=3,
324
  placeholder="Escribe aqu铆 tu texto..."
325
  )
@@ -377,7 +261,7 @@ with gr.Blocks() as demo:
377
  ],
378
  value="es-ES-AlvaroNeural"
379
  )
380
- procesar_btn = gr.Button("Generar Video (Modo Optimizado)")
381
  video_output = gr.Video(label="Video Procesado")
382
  with gr.Accordion("Ejemplos de Uso", open=False):
383
  gr.Examples(
@@ -393,23 +277,11 @@ with gr.Blocks() as demo:
393
 
394
  gr.Markdown("""
395
  ### 鈩癸笍 Notas importantes:
396
- - **Limitaciones para Hugging Face Spaces:**
397
- - M谩xima duraci贸n de video: 5 minutos
398
- - M谩ximo tama帽o de archivo: 100MB
399
- - Resoluci贸n reducida a 640x360 para procesamiento
400
- - Texto TTS limitado a 500 caracteres
401
- - Las transiciones ocurren cada 30 segundos
402
  - El video contiene intro y outro predefinidos
403
- - El archivo generado se elimina despu茅s de 1 hora
404
- - Para videos m谩s pesados, considera usar este c贸digo localmente
405
  """)
406
 
407
  if __name__ == "__main__":
408
- # Instalar psutil si no est谩 disponible
409
- try:
410
- import psutil
411
- except ImportError:
412
- os.system("pip install psutil")
413
- import psutil
414
-
415
  demo.queue().launch()
 
2
  import logging
3
  import os
4
  import asyncio
 
 
5
  from moviepy.editor import *
6
  import edge_tts
7
  import gradio as gr
 
16
  MUSIC_BG = "musicafondo.mp3"
17
  EJEMPLO_VIDEO = "ejemplo.mp4"
18
 
 
 
 
 
 
 
 
 
 
19
  # Validar existencia de archivos
20
  for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, EJEMPLO_VIDEO]:
21
  if not os.path.exists(file):
22
  logging.error(f"Falta archivo necesario: {file}")
23
  raise FileNotFoundError(f"Falta: {file}")
24
 
25
+ # Configuraci贸n de chunks
26
+ SEGMENT_DURATION = 30 # Duraci贸n exacta entre transiciones (sin overlap)
27
+ TRANSITION_DURATION = 1.5 # Duraci贸n del efecto slide
 
28
 
29
  def eliminar_archivo_tiempo(ruta, delay=1800):
30
  def eliminar():
 
39
 
40
  def validar_video(video_path):
41
  try:
 
 
 
 
 
 
 
42
  clip = VideoFileClip(video_path)
 
43
  clip.close()
 
 
 
 
 
 
44
  return True
45
  except Exception as e:
46
  logging.error(f"El video no es v谩lido: {e}")
 
50
  try:
51
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_converted:
52
  output_path = tmp_converted.name
53
+ os.system(f'ffmpeg -i "{video_path}" -vcodec libx264 -acodec aac "{output_path}" -y')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  return output_path
55
  except Exception as e:
56
  logging.error(f"Error al convertir el video: {e}")
57
  raise
58
 
59
+ def ajustar_resolucion(video_path):
60
+ try:
61
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_resized:
62
+ output_path = tmp_resized.name
63
+ os.system(f'ffmpeg -i "{video_path}" -vf "scale=1280:720" -vcodec libx264 -acodec aac "{output_path}" -y')
64
+ return output_path
65
+ except Exception as e:
66
+ logging.error(f"Error al ajustar la resoluci贸n del video: {e}")
67
+ raise
68
+
69
  async def generar_tts(texto, voz, duracion_total):
70
  try:
71
  if not texto.strip():
72
  raise ValueError("El texto para TTS no puede estar vac铆o.")
73
+ if len(texto) > 1000:
74
+ texto = texto[:1000]
 
 
75
 
76
+ # Eliminado la validaci贸n restrictiva de voces para permitir todas las que est谩n en el dropdown
77
  logging.info(f"Generando TTS con voz: {voz}")
78
  communicate = edge_tts.Communicate(texto, voz)
79
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_tts:
 
104
  part2.fx(vfx.fadein, duration).set_position(
105
  lambda t: ('center', 720 - (720 * (t/duration)))
106
  )
107
+ ], size=(1280, 720)).set_duration(duration)
108
  return transition
109
 
110
+ async def procesar_video(video_input, texto_tts, voz_seleccionada):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  temp_files = []
112
  intro, outro, video_original = None, None, None
 
 
113
  try:
 
114
  logging.info("Iniciando procesamiento")
 
115
 
116
  if not validar_video(video_input):
 
117
  video_input = convertir_video(video_input)
118
  temp_files.append(video_input)
119
+
120
+ video_original = VideoFileClip(video_input, target_resolution=(720, 1280))
 
 
121
  duracion_video = video_original.duration
122
 
 
 
 
 
 
123
  if duracion_video <= 0:
124
  raise ValueError("El video debe tener una duraci贸n mayor que cero.")
125
 
 
126
  tts_audio, tts_path = await generar_tts(texto_tts, voz_seleccionada, duracion_video)
 
 
 
127
  bg_audio, bg_path = crear_musica_fondo(duracion_video)
128
+ temp_files.extend([tts_path, bg_path])
129
 
130
+ audio_original = video_original.audio.volumex(0.7) if video_original.audio else None
 
131
  audios = [bg_audio.set_duration(duracion_video)]
132
  if audio_original:
133
  audios.append(audio_original)
134
  audios.append(tts_audio.set_start(0).volumex(0.85))
135
  audio_final = CompositeAudioClip(audios).set_duration(duracion_video)
136
 
137
+ video_final = video_original.copy()
138
  if duracion_video > SEGMENT_DURATION:
 
139
  clips = []
140
  num_segments = int(duracion_video // SEGMENT_DURATION) + (1 if duracion_video % SEGMENT_DURATION > 0 else 0)
 
141
  for i in range(num_segments):
 
 
 
142
  start_time = i * SEGMENT_DURATION
143
  end_time = min(start_time + SEGMENT_DURATION, duracion_video)
144
  segment = video_original.subclip(start_time, end_time)
 
 
 
 
 
145
  if i == 0:
146
  clips.append(segment)
147
  else:
 
152
  clips[-1] = prev_segment.subclip(0, prev_end)
153
  clips.append(transition)
154
  clips.append(segment)
 
 
 
 
 
155
  video_final = concatenate_videoclips(clips, method="compose")
 
 
 
 
 
 
156
 
157
+ video_final = video_final.set_audio(audio_final)
158
+ intro = VideoFileClip(INTRO_VIDEO, target_resolution=(720, 1280))
159
+ outro = VideoFileClip(OUTRO_VIDEO, target_resolution=(720, 1280))
160
+ video_final = concatenate_videoclips([intro, video_final, outro], method="compose")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
+ with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
163
+ video_final.write_videofile(
164
+ tmp.name,
165
+ codec="libx264",
166
+ audio_codec="aac",
167
+ fps=24,
168
+ threads=2,
169
+ bitrate="3M",
170
+ ffmpeg_params=[
171
+ "-preset", "ultrafast",
172
+ "-crf", "28",
173
+ "-movflags", "+faststart",
174
+ "-vf", "scale=1280:720"
175
+ ],
176
+ verbose=False
177
+ )
178
+ eliminar_archivo_tiempo(tmp.name, 1800)
179
+ logging.info(f"Video final guardado: {tmp.name}")
180
+ return tmp.name
 
 
 
 
181
  except Exception as e:
182
  logging.error(f"Fallo general: {str(e)}")
183
  raise
184
  finally:
185
  try:
186
+ if video_original:
187
+ video_original.close()
188
+ if intro:
189
+ intro.close()
190
+ if outro: # Corregido: outro en lugar de otro
191
+ outro.close()
192
  for file in temp_files:
193
  try:
194
+ os.remove(file)
 
195
  except Exception as e:
196
  logging.warning(f"Error limpiando {file}: {e}")
 
 
 
 
 
 
197
  except Exception as e:
198
  logging.warning(f"Error al cerrar recursos: {str(e)}")
199
 
 
203
  with gr.Tab("Principal"):
204
  video_input = gr.Video(label="Subir video")
205
  texto_tts = gr.Textbox(
206
+ label="Texto para TTS",
207
  lines=3,
208
  placeholder="Escribe aqu铆 tu texto..."
209
  )
 
261
  ],
262
  value="es-ES-AlvaroNeural"
263
  )
264
+ procesar_btn = gr.Button("Generar Video")
265
  video_output = gr.Video(label="Video Procesado")
266
  with gr.Accordion("Ejemplos de Uso", open=False):
267
  gr.Examples(
 
277
 
278
  gr.Markdown("""
279
  ### 鈩癸笍 Notas importantes:
280
+ - Las transiciones ocurren solamente cada 30 segundos
 
 
 
 
 
281
  - El video contiene intro y outro predefinidos
282
+ - El archivo generado se elimina despu茅s de 30 minutos
283
+ - Para mejores resultados, usa videos de dimensiones 720p o 1080p
284
  """)
285
 
286
  if __name__ == "__main__":
 
 
 
 
 
 
 
287
  demo.queue().launch()