Spaces:
Sleeping
Sleeping
import math | |
import tempfile | |
import logging | |
import os | |
import asyncio | |
from moviepy.editor import ( | |
VideoFileClip, AudioFileClip, ImageClip, | |
concatenate_videoclips, CompositeVideoClip, CompositeAudioClip | |
) | |
import edge_tts | |
import gradio as gr | |
from pydub import AudioSegment | |
# Configuración de Logs | |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") | |
# CONSTANTES DE ARCHIVOS | |
INTRO_VIDEO = "introvideo.mp4" | |
OUTRO_VIDEO = "outrovideo.mp4" | |
MUSIC_BG = "musicafondo.mp3" | |
FX_SOUND = "fxsound.mp3" | |
WATERMARK = "watermark.png" | |
EJEMPLO_VIDEO = "ejemplo.mp4" # Video de ejemplo en el root | |
# Validar existencia de archivos obligatorios | |
for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, FX_SOUND, WATERMARK, EJEMPLO_VIDEO]: | |
if not os.path.exists(file): | |
logging.error(f"Falta archivo necesario: {file}") | |
raise FileNotFoundError(f"Falta archivo necesario: {file}") | |
def cortar_video(video_path, metodo="inteligente", duracion=10): | |
"""Corta el video en clips según el método especificado.""" | |
try: | |
logging.info("Iniciando corte de video...") | |
video = VideoFileClip(video_path) | |
if metodo == "manual": | |
clips = [video.subclip(i * duracion, (i + 1) * duracion) | |
for i in range(math.ceil(video.duration / duracion))] | |
logging.info(f"Video cortado en {len(clips)} clips manuales.") | |
return clips | |
# Método "inteligente" simplificado (cortes cada 5 segundos como fallback) | |
clips = [] | |
ultimo_corte = 0 | |
for i in range(1, math.ceil(video.duration)): | |
if i % 5 == 0: # Corte cada 5 segundos (puedes mejorar esto con VAD más adelante) | |
clips.append(video.subclip(ultimo_corte, i)) | |
ultimo_corte = i | |
if ultimo_corte < video.duration: | |
clips.append(video.subclip(ultimo_corte, video.duration)) | |
logging.info(f"Video cortado en {len(clips)} clips automáticos.") | |
return clips | |
except Exception as e: | |
logging.error(f"Error al cortar video: {e}") | |
raise | |
async def procesar_audio(texto, voz, duracion_total, duracion_intro, duracion_video_editado): | |
"""Genera el TTS y mezcla con la música de fondo.""" | |
try: | |
logging.info("Generando TTS y mezclando audio...") | |
communicate = edge_tts.Communicate(texto, voz) | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp: | |
await communicate.save(tmp.name) | |
tts_audio = AudioFileClip(tmp.name) | |
# Ajustar duración del TTS | |
if tts_audio.duration > duracion_video_editado: | |
tts_audio = tts_audio.subclip(0, duracion_video_editado) | |
# Preparar música de fondo | |
bg_music = AudioSegment.from_mp3(MUSIC_BG) | |
if len(bg_music) < duracion_total * 1000: | |
bg_music = bg_music * math.ceil(duracion_total * 1000 / len(bg_music)) | |
bg_music = bg_music[:duracion_total * 1000].fade_out(3000) | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp: | |
bg_music.export(tmp.name, format="mp3") | |
bg_audio = AudioFileClip(tmp.name).volumex(0.10) | |
# Combinar audios | |
audio_final = CompositeAudioClip([ | |
bg_audio, | |
tts_audio.volumex(0.9).set_start(duracion_intro) | |
]) | |
logging.info("Audio procesado correctamente.") | |
return audio_final | |
except Exception as e: | |
logging.error(f"Error al procesar audio: {e}") | |
raise | |
def agregar_transiciones(clips): | |
"""Agrega transiciones con watermark y sonido FX entre clips.""" | |
try: | |
logging.info("Agregando transiciones...") | |
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): | |
clip_watermarked = CompositeVideoClip([clip, transicion]) | |
clips_con_fx.append(clip_watermarked) | |
if i < len(clips) - 1: | |
clips_con_fx.append( | |
CompositeVideoClip([transicion.set_position("center")]).set_audio(fx_audio) | |
) | |
logging.info("Transiciones agregadas correctamente.") | |
return concatenate_videoclips(clips_con_fx) | |
except Exception as e: | |
logging.error(f"Error al agregar transiciones: {e}") | |
raise | |
async def procesar_video(video_input, texto_tts, voz_seleccionada, metodo_corte, duracion_corte): | |
"""Procesa el video completo con intro, cortes, TTS y outro.""" | |
temp_files = [] | |
try: | |
logging.info("Iniciando procesamiento de video...") | |
# Cortar y editar video | |
clips = cortar_video(video_input, metodo_corte, duracion_corte) | |
video_editado = agregar_transiciones(clips) | |
# Cargar intro y outro | |
intro = VideoFileClip(INTRO_VIDEO) | |
outro = VideoFileClip(OUTRO_VIDEO) | |
# Calcular duraciones | |
duracion_intro = intro.duration | |
duracion_video_editado = video_editado.duration | |
duracion_total = duracion_intro + duracion_video_editado + outro.duration | |
# Concatenar clips | |
video_final = concatenate_videoclips([intro, video_editado, outro]) | |
# Procesar audio | |
audio_final = await procesar_audio(texto_tts, voz_seleccionada, duracion_total, duracion_intro, duracion_video_editado) | |
# Renderizar video final | |
video_final = video_final.set_audio(audio_final) | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp: | |
video_final.write_videofile(tmp.name, codec="libx264", fps=24) | |
temp_files.append(tmp.name) | |
logging.info("Video procesado y guardado temporalmente.") | |
return tmp.name | |
except Exception as e: | |
logging.error(f"Error durante el procesamiento: {e}") | |
raise | |
finally: | |
for file in temp_files: | |
try: | |
if os.path.exists(file): | |
os.remove(file) | |
logging.info(f"Archivo temporal eliminado: {file}") | |
except Exception as e: | |
logging.warning(f"No se pudo eliminar el archivo temporal {file}: {e}") | |
# Interfaz Gradio con ejemplo de uso | |
with gr.Blocks() as demo: | |
gr.Markdown("# Video Editor IA") | |
with gr.Tab("Principal"): | |
video_input = gr.Video(label="Subir video", value=EJEMPLO_VIDEO) # Video de ejemplo | |
texto_tts = gr.Textbox( | |
label="Texto para TTS", | |
lines=3, | |
value="hola esto es una prueba suscribete al canal ya empieza el video" # Texto de ejemplo | |
) | |
voz_seleccionada = gr.Dropdown( | |
label="Seleccionar voz", | |
choices=["es-ES-AlvaroNeural", "es-MX-BeatrizNeural"], | |
value="es-ES-AlvaroNeural" | |
) | |
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 cortes", | |
value="inteligente" | |
) | |
duracion_corte = gr.Slider( | |
1, 60, 10, | |
label="Segundos 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__": | |
logging.info("Iniciando aplicación Gradio...") | |
demo.queue().launch() |