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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +187 -59
app.py CHANGED
@@ -2,6 +2,8 @@ import tempfile
2
  import logging
3
  import os
4
  import asyncio
 
 
5
  from moviepy.editor import *
6
  import edge_tts
7
  import gradio as gr
@@ -16,15 +18,25 @@ OUTRO_VIDEO = "outrovideo.mp4"
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,8 +51,22 @@ def eliminar_archivo_tiempo(ruta, delay=1800):
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,30 +76,39 @@ def convertir_video(video_path):
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,44 +139,85 @@ def create_slide_transition(clip1, clip2, duration=TRANSITION_DURATION):
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,48 +228,88 @@ async def procesar_video(video_input, texto_tts, voz_seleccionada):
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,7 +319,7 @@ with gr.Blocks() as demo:
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,7 +377,7 @@ with gr.Blocks() as demo:
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,11 +393,23 @@ with gr.Blocks() as demo:
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()
 
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
  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
 
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
  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
  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
  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
  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
  ],
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
 
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()