Update app.py
Browse files
app.py
CHANGED
@@ -19,13 +19,13 @@ MUSIC_BG = "musicafondo.mp3"
|
|
19 |
EJEMPLO_VIDEO = "ejemplo.mp4"
|
20 |
|
21 |
# CONSTANTES DE LIMITACIONES
|
22 |
-
|
23 |
-
|
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]:
|
@@ -38,7 +38,7 @@ def mostrar_uso_memoria():
|
|
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=
|
42 |
def eliminar():
|
43 |
try:
|
44 |
if os.path.exists(ruta):
|
@@ -61,11 +61,6 @@ def validar_video(video_path):
|
|
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:
|
@@ -77,23 +72,15 @@ def convertir_video(video_path):
|
|
77 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_converted:
|
78 |
output_path = tmp_converted.name
|
79 |
|
80 |
-
#
|
81 |
-
os.system(f'ffmpeg -i "{video_path}" -vf "scale=
|
82 |
|
83 |
-
# Comprobar si ahora cumple las limitaciones
|
84 |
if not validar_video(output_path):
|
85 |
-
# Si sigue sin cumplir,
|
86 |
-
|
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 |
-
|
97 |
|
98 |
return output_path
|
99 |
except Exception as e:
|
@@ -104,10 +91,10 @@ 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
|
108 |
-
if len(texto) >
|
109 |
-
texto = texto[:
|
110 |
-
logging.info("Texto para TTS truncado a
|
111 |
|
112 |
logging.info(f"Generando TTS con voz: {voz}")
|
113 |
communicate = edge_tts.Communicate(texto, voz)
|
@@ -137,9 +124,9 @@ def create_slide_transition(clip1, clip2, duration=TRANSITION_DURATION):
|
|
137 |
transition = CompositeVideoClip([
|
138 |
part1.fx(vfx.fadeout, duration),
|
139 |
part2.fx(vfx.fadein, duration).set_position(
|
140 |
-
lambda t: ('center',
|
141 |
)
|
142 |
-
], size=
|
143 |
return transition
|
144 |
|
145 |
def liberar_memoria(objetos_cerrar=None):
|
@@ -167,7 +154,7 @@ async def procesar_video(video_input, texto_tts, voz_seleccionada, progress=gr.P
|
|
167 |
progress(0, desc="Validando video")
|
168 |
|
169 |
if not validar_video(video_input):
|
170 |
-
progress(0.05, desc="
|
171 |
video_input = convertir_video(video_input)
|
172 |
temp_files.append(video_input)
|
173 |
|
@@ -175,12 +162,11 @@ async def procesar_video(video_input, texto_tts, voz_seleccionada, progress=gr.P
|
|
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 |
|
@@ -192,84 +178,116 @@ async def procesar_video(video_input, texto_tts, voz_seleccionada, progress=gr.P
|
|
192 |
bg_audio, bg_path = crear_musica_fondo(duracion_video)
|
193 |
temp_files.append(bg_path)
|
194 |
|
195 |
-
|
196 |
-
|
197 |
-
|
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 |
-
|
204 |
-
|
205 |
-
|
206 |
-
clips = []
|
207 |
-
num_segments = int(duracion_video // SEGMENT_DURATION) + (1 if duracion_video % SEGMENT_DURATION > 0 else 0)
|
208 |
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
|
|
|
|
|
|
231 |
|
232 |
-
|
233 |
-
|
234 |
-
|
|
|
235 |
|
236 |
-
|
237 |
-
|
238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
239 |
|
240 |
-
|
241 |
-
|
242 |
-
|
|
|
243 |
|
244 |
-
#
|
245 |
-
|
246 |
-
|
247 |
-
outro = VideoFileClip(OUTRO_VIDEO, target_resolution=(360, 640)) # Reducido para optimizar
|
248 |
|
249 |
-
#
|
|
|
|
|
250 |
with tempfile.NamedTemporaryFile(delete=False, suffix="_intro.mp4") as tmp_intro:
|
251 |
-
intro.write_videofile(
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
segmentos_temp.
|
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 |
-
|
269 |
-
|
270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
271 |
|
272 |
-
# Unir los segmentos con ffmpeg
|
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
|
@@ -287,7 +305,7 @@ async def procesar_video(video_input, texto_tts, voz_seleccionada, progress=gr.P
|
|
287 |
if os.path.exists(segment):
|
288 |
os.remove(segment)
|
289 |
|
290 |
-
eliminar_archivo_tiempo(output_path, 3600) #
|
291 |
progress(1.0, desc="隆Video listo!")
|
292 |
logging.info(f"Video final guardado: {output_path}")
|
293 |
mostrar_uso_memoria()
|
@@ -319,7 +337,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.
|
323 |
lines=3,
|
324 |
placeholder="Escribe aqu铆 tu texto..."
|
325 |
)
|
@@ -393,15 +411,15 @@ with gr.Blocks() as demo:
|
|
393 |
|
394 |
gr.Markdown("""
|
395 |
### 鈩癸笍 Notas importantes:
|
396 |
-
- **
|
397 |
-
-
|
398 |
-
- M谩ximo tama帽o de archivo:
|
399 |
-
- Resoluci贸n reducida a 640x360 para procesamiento
|
400 |
-
- Texto TTS limitado a
|
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
|
405 |
""")
|
406 |
|
407 |
if __name__ == "__main__":
|
|
|
19 |
EJEMPLO_VIDEO = "ejemplo.mp4"
|
20 |
|
21 |
# CONSTANTES DE LIMITACIONES
|
22 |
+
MAX_VIDEO_SIZE = 200 * 1024 * 1024 # Tama帽o m谩ximo en bytes (200MB)
|
23 |
+
MAX_RESOLUTION = (640, 360) # Resoluci贸n m谩xima (360p para optimizar)
|
|
|
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 |
+
PROCESSING_CHUNK = 120 # Procesar en bloques de 2 minutos para optimizar memoria
|
29 |
|
30 |
# Validar existencia de archivos
|
31 |
for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, EJEMPLO_VIDEO]:
|
|
|
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=3600):
|
42 |
def eliminar():
|
43 |
try:
|
44 |
if os.path.exists(ruta):
|
|
|
61 |
clip = VideoFileClip(video_path)
|
62 |
duracion = clip.duration
|
63 |
clip.close()
|
|
|
|
|
|
|
|
|
|
|
64 |
|
65 |
return True
|
66 |
except Exception as e:
|
|
|
72 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_converted:
|
73 |
output_path = tmp_converted.name
|
74 |
|
75 |
+
# Convertir a un formato m谩s eficiente y con menor resoluci贸n para optimizar
|
76 |
+
os.system(f'ffmpeg -i "{video_path}" -vf "scale={MAX_RESOLUTION[0]}:{MAX_RESOLUTION[1]}" -c:v libx264 -crf 28 -preset ultrafast -c:a aac -b:a 96k "{output_path}" -y')
|
77 |
|
78 |
+
# Comprobar si ahora cumple las limitaciones de tama帽o
|
79 |
if not validar_video(output_path):
|
80 |
+
# Si sigue sin cumplir, aumentar la compresi贸n
|
81 |
+
os.system(f'ffmpeg -i "{output_path}" -vf "scale={MAX_RESOLUTION[0]}:{MAX_RESOLUTION[1]}" -c:v libx264 -crf 32 -preset ultrafast -c:a aac -b:a 64k "{output_path}.tmp" -y')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
os.remove(output_path)
|
83 |
+
os.rename(f"{output_path}.tmp", output_path)
|
84 |
|
85 |
return output_path
|
86 |
except Exception as e:
|
|
|
91 |
try:
|
92 |
if not texto.strip():
|
93 |
raise ValueError("El texto para TTS no puede estar vac铆o.")
|
94 |
+
# Limitar el texto a 1000 caracteres para procesar m谩s r谩pido
|
95 |
+
if len(texto) > 1000:
|
96 |
+
texto = texto[:1000]
|
97 |
+
logging.info("Texto para TTS truncado a 1000 caracteres para optimizar rendimiento")
|
98 |
|
99 |
logging.info(f"Generando TTS con voz: {voz}")
|
100 |
communicate = edge_tts.Communicate(texto, voz)
|
|
|
124 |
transition = CompositeVideoClip([
|
125 |
part1.fx(vfx.fadeout, duration),
|
126 |
part2.fx(vfx.fadein, duration).set_position(
|
127 |
+
lambda t: ('center', MAX_RESOLUTION[1] - (MAX_RESOLUTION[1] * (t/duration)))
|
128 |
)
|
129 |
+
], size=MAX_RESOLUTION).set_duration(duration) # Reducido para optimizar
|
130 |
return transition
|
131 |
|
132 |
def liberar_memoria(objetos_cerrar=None):
|
|
|
154 |
progress(0, desc="Validando video")
|
155 |
|
156 |
if not validar_video(video_input):
|
157 |
+
progress(0.05, desc="Optimizando formato de video")
|
158 |
video_input = convertir_video(video_input)
|
159 |
temp_files.append(video_input)
|
160 |
|
|
|
162 |
# Reducir resoluci贸n para optimizar procesamiento
|
163 |
video_original = VideoFileClip(video_input)
|
164 |
duracion_video = video_original.duration
|
165 |
+
video_original.close() # Cerrar para liberar memoria
|
166 |
+
|
167 |
+
# Informaci贸n importante sobre el video original
|
168 |
+
logging.info(f"Duraci贸n total del video: {duracion_video} segundos")
|
169 |
|
|
|
|
|
|
|
|
|
|
|
170 |
if duracion_video <= 0:
|
171 |
raise ValueError("El video debe tener una duraci贸n mayor que cero.")
|
172 |
|
|
|
178 |
bg_audio, bg_path = crear_musica_fondo(duracion_video)
|
179 |
temp_files.append(bg_path)
|
180 |
|
181 |
+
# Procesar por bloques para optimizar memoria
|
182 |
+
num_chunks = int(duracion_video // PROCESSING_CHUNK) + (1 if duracion_video % PROCESSING_CHUNK > 0 else 0)
|
183 |
+
logging.info(f"Procesando video en {num_chunks} bloques")
|
|
|
|
|
|
|
|
|
184 |
|
185 |
+
for chunk_idx in range(num_chunks):
|
186 |
+
chunk_start = chunk_idx * PROCESSING_CHUNK
|
187 |
+
chunk_end = min((chunk_idx + 1) * PROCESSING_CHUNK, duracion_video)
|
|
|
|
|
188 |
|
189 |
+
progress(0.35 + (0.45 * chunk_idx / num_chunks),
|
190 |
+
desc=f"Procesando bloque {chunk_idx+1}/{num_chunks} ({chunk_start:.1f}s - {chunk_end:.1f}s)")
|
191 |
+
|
192 |
+
# Cargar solo la porci贸n del video que necesitamos
|
193 |
+
chunk_video = VideoFileClip(video_input).subclip(chunk_start, chunk_end)
|
194 |
+
|
195 |
+
# Extraer la porci贸n de audio correspondiente a este bloque
|
196 |
+
chunk_tts = tts_audio.subclip(chunk_start, chunk_end) if chunk_start < tts_audio.duration else None
|
197 |
+
chunk_bg = bg_audio.subclip(chunk_start, chunk_end)
|
198 |
+
|
199 |
+
# Crear la mezcla de audio para este bloque
|
200 |
+
audio_chunks = [chunk_bg]
|
201 |
+
if chunk_video.audio:
|
202 |
+
audio_chunks.append(chunk_video.audio.volumex(0.5))
|
203 |
+
if chunk_tts:
|
204 |
+
audio_chunks.append(chunk_tts.volumex(0.85))
|
205 |
+
|
206 |
+
chunk_audio_final = CompositeAudioClip(audio_chunks)
|
207 |
+
chunk_video = chunk_video.set_audio(chunk_audio_final)
|
208 |
+
|
209 |
+
# Procesar las transiciones dentro de este chunk si es necesario
|
210 |
+
if chunk_end - chunk_start > SEGMENT_DURATION:
|
211 |
+
segments_in_chunk = []
|
212 |
+
segments_count = int((chunk_end - chunk_start) // SEGMENT_DURATION) + \
|
213 |
+
(1 if (chunk_end - chunk_start) % SEGMENT_DURATION > 0 else 0)
|
214 |
|
215 |
+
for i in range(segments_count):
|
216 |
+
seg_start = i * SEGMENT_DURATION
|
217 |
+
seg_end = min(seg_start + SEGMENT_DURATION, chunk_end - chunk_start)
|
218 |
+
segment = chunk_video.subclip(seg_start, seg_end)
|
219 |
|
220 |
+
if i == 0:
|
221 |
+
segments_in_chunk.append(segment)
|
222 |
+
else:
|
223 |
+
prev_segment = segments_in_chunk[-1]
|
224 |
+
transition = create_slide_transition(prev_segment, segment)
|
225 |
+
|
226 |
+
prev_end = prev_segment.duration - TRANSITION_DURATION
|
227 |
+
if prev_end > 0:
|
228 |
+
segments_in_chunk[-1] = prev_segment.subclip(0, prev_end)
|
229 |
+
|
230 |
+
segments_in_chunk.append(transition)
|
231 |
+
segments_in_chunk.append(segment)
|
232 |
+
|
233 |
+
chunk_processed = concatenate_videoclips(segments_in_chunk, method="compose")
|
234 |
+
else:
|
235 |
+
chunk_processed = chunk_video
|
236 |
+
|
237 |
+
# Guardar este chunk procesado como archivo temporal
|
238 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=f"_chunk{chunk_idx}.mp4") as chunk_file:
|
239 |
+
chunk_path = chunk_file.name
|
240 |
+
chunk_processed.write_videofile(
|
241 |
+
chunk_path,
|
242 |
+
codec="libx264",
|
243 |
+
audio_codec="aac",
|
244 |
+
preset="ultrafast",
|
245 |
+
bitrate="1M",
|
246 |
+
ffmpeg_params=["-crf", "28"],
|
247 |
+
verbose=False
|
248 |
+
)
|
249 |
+
segmentos_temp.append(chunk_path)
|
250 |
|
251 |
+
# Liberar memoria
|
252 |
+
chunk_video.close()
|
253 |
+
chunk_processed.close()
|
254 |
+
liberar_memoria()
|
255 |
|
256 |
+
# Liberar memoria antes de procesar intro/outro
|
257 |
+
liberar_memoria([tts_audio, bg_audio])
|
258 |
+
tts_audio = bg_audio = None
|
|
|
259 |
|
260 |
+
# A帽adir intro y outro
|
261 |
+
progress(0.85, desc="Preparando intro y outro")
|
262 |
+
intro = VideoFileClip(INTRO_VIDEO, target_resolution=MAX_RESOLUTION)
|
263 |
with tempfile.NamedTemporaryFile(delete=False, suffix="_intro.mp4") as tmp_intro:
|
264 |
+
intro.write_videofile(
|
265 |
+
tmp_intro.name,
|
266 |
+
codec="libx264",
|
267 |
+
audio_codec="aac",
|
268 |
+
preset="ultrafast",
|
269 |
+
bitrate="1M",
|
270 |
+
ffmpeg_params=["-crf", "28"],
|
271 |
+
verbose=False
|
272 |
+
)
|
273 |
+
segmentos_temp.insert(0, tmp_intro.name) # Intro al principio
|
274 |
+
intro.close()
|
|
|
|
|
|
|
|
|
|
|
275 |
|
276 |
+
outro = VideoFileClip(OUTRO_VIDEO, target_resolution=MAX_RESOLUTION)
|
277 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix="_outro.mp4") as tmp_outro:
|
278 |
+
outro.write_videofile(
|
279 |
+
tmp_outro.name,
|
280 |
+
codec="libx264",
|
281 |
+
audio_codec="aac",
|
282 |
+
preset="ultrafast",
|
283 |
+
bitrate="1M",
|
284 |
+
ffmpeg_params=["-crf", "28"],
|
285 |
+
verbose=False
|
286 |
+
)
|
287 |
+
segmentos_temp.append(tmp_outro.name) # Outro al final
|
288 |
+
outro.close()
|
289 |
|
290 |
+
# Unir todos los segmentos con ffmpeg
|
291 |
progress(0.9, desc="Generando video final")
|
292 |
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as concat_file:
|
293 |
# Escribir archivo de lista para concatenaci贸n
|
|
|
305 |
if os.path.exists(segment):
|
306 |
os.remove(segment)
|
307 |
|
308 |
+
eliminar_archivo_tiempo(output_path, 3600) # Eliminaci贸n despu茅s de 1 hora
|
309 |
progress(1.0, desc="隆Video listo!")
|
310 |
logging.info(f"Video final guardado: {output_path}")
|
311 |
mostrar_uso_memoria()
|
|
|
337 |
with gr.Tab("Principal"):
|
338 |
video_input = gr.Video(label="Subir video")
|
339 |
texto_tts = gr.Textbox(
|
340 |
+
label="Texto para TTS (m谩x. 1000 caracteres)",
|
341 |
lines=3,
|
342 |
placeholder="Escribe aqu铆 tu texto..."
|
343 |
)
|
|
|
411 |
|
412 |
gr.Markdown("""
|
413 |
### 鈩癸笍 Notas importantes:
|
414 |
+
- **Optimizaciones para Hugging Face Spaces:**
|
415 |
+
- Procesamiento por bloques para videos largos
|
416 |
+
- M谩ximo tama帽o de archivo: 200MB
|
417 |
+
- Resoluci贸n reducida a 640x360 para procesamiento m谩s r谩pido
|
418 |
+
- Texto TTS limitado a 1000 caracteres
|
419 |
- Las transiciones ocurren cada 30 segundos
|
420 |
- El video contiene intro y outro predefinidos
|
421 |
- El archivo generado se elimina despu茅s de 1 hora
|
422 |
+
- Para videos de alta calidad, considera usar este c贸digo localmente
|
423 |
""")
|
424 |
|
425 |
if __name__ == "__main__":
|