Spaces:
Sleeping
Sleeping
import os | |
import math | |
import tempfile | |
import logging | |
from PIL import Image | |
from pydub import AudioSegment | |
from moviepy.editor import ( | |
VideoFileClip, AudioFileClip, ImageClip, | |
concatenate_videoclips, CompositeVideoClip | |
) | |
import edge_tts | |
import gradio as gr | |
import asyncio | |
# PATCH PARA PILLOW 10+ | |
Image.ANTIALIAS = Image.Resampling.LANCZOS | |
# CONFIGURACI脫N | |
INTRO_VIDEO = "introvideo.mp4" | |
OUTRO_VIDEO = "outrovideo.mp4" | |
MUSIC_BG = "musicafondo.mp3" | |
FX_SOUND = "fxsound.mp3" | |
WATERMARK = "watermark.png" | |
# Validar archivos | |
for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, FX_SOUND, WATERMARK]: | |
if not os.path.exists(file): | |
raise FileNotFoundError(f"Falta: {file}") | |
async def procesar_audio(texto, voz, duracion_total): | |
try: | |
communicate = edge_tts.Communicate(texto, voz) | |
# Generar TTS en WAV (formato sin compresi贸n) | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_wav: | |
await communicate.save(tmp_wav.name) | |
tts_audio = AudioSegment.from_wav(tmp_wav.name) | |
# Preparar m煤sica de fondo en loop | |
bg_music = AudioSegment.from_mp3(MUSIC_BG) - 10 # 10% volumen | |
repeticiones = math.ceil(duracion_total * 1000 / len(bg_music)) | |
bg_music_loop = bg_music * repeticiones | |
bg_music_final = bg_music_loop[:duracion_total*1000].fade_out(3000) | |
# Combinar TTS (despu茅s de la intro) con m煤sica | |
intro_duration = VideoFileClip(INTRO_VIDEO).duration * 1000 # Duraci贸n en ms | |
audio_final = bg_music_final.overlay(tts_audio, position=intro_duration) | |
# Exportar directamente como MP3 con par谩metros robustos | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_mp3: | |
audio_final.export( | |
tmp_mp3.name, | |
format="mp3", | |
parameters=["-ac", "2", "-ar", "44100", "-b:a", "320k"] | |
) | |
return tmp_mp3.name | |
except Exception as e: | |
logging.error(f"Error procesando audio: {e}") | |
raise | |
def cortar_video(video_path, metodo="inteligente", duracion=10): | |
try: | |
video = VideoFileClip(video_path) | |
if metodo == "manual": | |
return [ | |
video.subclip(i*duracion, (i+1)*duracion) | |
for i in range(math.ceil(video.duration/duracion)) | |
] | |
# Simulaci贸n de cortes autom谩ticos | |
clips = [] | |
ultimo_corte = 0 | |
for i in range(1, math.ceil(video.duration)): | |
if i % 5 == 0: # Simulaci贸n de pausas | |
clips.append(video.subclip(ultimo_corte, i)) | |
ultimo_corte = i | |
return clips if clips else [video] | |
except Exception as e: | |
logging.error(f"Error cortando video: {e}") | |
raise | |
def agregar_transiciones(clips): | |
try: | |
fx_audio = AudioFileClip(FX_SOUND).set_duration(2.5) | |
transicion = ImageClip(WATERMARK).set_duration(2.5).resize(height=clips[0].h).set_position(("center", 0.1)) | |
clips_con_fx = [] | |
for i, clip in enumerate(clips): | |
# Agregar watermark | |
clip_watermarked = CompositeVideoClip([clip, transicion]) | |
clips_con_fx.append(clip_watermarked) | |
# Agregar transici贸n entre clips | |
if i < len(clips)-1: | |
clips_con_fx.append( | |
CompositeVideoClip([transicion.set_position("center")]) | |
.set_audio(fx_audio) | |
) | |
return concatenate_videoclips(clips_con_fx) | |
except Exception as e: | |
logging.error(f"Error en transiciones: {e}") | |
raise | |
async def procesar_video( | |
video_input, | |
texto_tts, | |
voz_seleccionada, | |
metodo_corte, | |
duracion_corte | |
): | |
temp_files = [] | |
try: | |
# Procesar video principal | |
clips = cortar_video(video_input, metodo_corte, duracion_corte) | |
video_editado = agregar_transiciones(clips) | |
# Agregar intro/outro | |
intro = VideoFileClip(INTRO_VIDEO) | |
outro = VideoFileClip(OUTRO_VIDEO) | |
video_final = concatenate_videoclips([intro, video_editado, outro]) | |
# Calcular duraci贸n total para la m煤sica | |
duracion_total = video_final.duration | |
# Generar audio (m煤sica en loop + TTS despu茅s de intro) | |
audio_mix_path = await procesar_audio(texto_tts, voz_seleccionada, duracion_total) | |
# Combinar video y audio | |
video_final = video_final.set_audio(AudioFileClip(audio_mix_path)) | |
# Renderizar | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_out: | |
video_final.write_videofile( | |
tmp_out.name, | |
codec="libx264", | |
audio_codec="aac", | |
fps=24 | |
) | |
temp_files.append(tmp_out.name) | |
return tmp_out.name | |
except Exception as e: | |
logging.error(f"Error general: {e}") | |
raise | |
finally: | |
# Eliminar archivos temporales | |
for file in temp_files + [getattr(video_input, 'name', None)]: | |
try: | |
if file and os.path.exists(file): | |
os.remove(file) | |
except Exception as e: | |
logging.warning(f"Error eliminando {file}: {e}") | |
# Interfaz Gradio | |
with gr.Blocks() as demo: | |
gr.Markdown("# Video Editor IA") | |
with gr.Tab("Principal"): | |
video_input = gr.Video(label="Subir video") | |
texto_tts = gr.Textbox(label="Texto para TTS", lines=3) | |
voz_seleccionada = gr.Dropdown( | |
label="Voz", | |
choices=["es-ES-AlvaroNeural", "es-MX-BeatrizNeural"] | |
) | |
procesar_btn = gr.Button("Generar Video") | |
video_output = gr.Video(label="Resultado") | |
with gr.Tab("Ajustes"): | |
metodo_corte = gr.Radio( | |
["inteligente", "manual"], | |
label="M茅todo de corte", | |
value="inteligente" | |
) | |
duracion_corte = gr.Slider(1, 60, 10, label="Duraci贸n por corte (solo manual)") | |
procesar_btn.click( | |
procesar_video, | |
inputs=[video_input, texto_tts, voz_seleccionada, metodo_corte, duracion_corte], | |
outputs=video_output | |
) | |
if __name__ == "__main__": | |
demo.queue().launch() |