|
import os |
|
import math |
|
import tempfile |
|
from pydub import AudioSegment |
|
from moviepy.editor import ( |
|
VideoFileClip, AudioFileClip, ImageClip, |
|
concatenate_videoclips, CompositeVideoClip |
|
) |
|
import edge_tts |
|
import gradio as gr |
|
import asyncio |
|
import logging |
|
from PIL import Image |
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") |
|
|
|
|
|
INTRO_VIDEO = "introvideo.mp4" |
|
OUTRO_VIDEO = "outrovideo.mp4" |
|
MUSIC_BG = "musicafondo.mp3" |
|
FX_SOUND = "fxsound.mp3" |
|
WATERMARK = "watermark.png" |
|
|
|
|
|
for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, FX_SOUND, WATERMARK]: |
|
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): |
|
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 |
|
|
|
|
|
clips = [] |
|
ultimo_corte = 0 |
|
for i in range(1, math.ceil(video.duration)): |
|
if i % 5 == 0: |
|
clips.append(video.subclip(ultimo_corte, i)) |
|
ultimo_corte = i |
|
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 |
|
|
|
def procesar_audio(texto, voz, clips_duracion): |
|
try: |
|
logging.info("Generando TTS y mezclando audio...") |
|
communicate = edge_tts.Communicate(texto, voz) |
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp: |
|
asyncio.run(communicate.save(tmp.name)) |
|
tts_audio = AudioFileClip(tmp.name) |
|
|
|
|
|
if tts_audio.duration < clips_duracion: |
|
tts_audio = tts_audio.loop(duration=clips_duracion) |
|
else: |
|
tts_audio = tts_audio.subclip(0, clips_duracion) |
|
|
|
|
|
bg_music = AudioSegment.from_mp3(MUSIC_BG) |
|
if len(bg_music) < clips_duracion * 1000: |
|
bg_music = bg_music * math.ceil(clips_duracion * 1000 / len(bg_music)) |
|
bg_music = bg_music[:clips_duracion * 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) |
|
|
|
logging.info("Audio procesado correctamente.") |
|
return CompositeAudioClip([bg_audio, tts_audio.volumex(0.9)]) |
|
except Exception as e: |
|
logging.error(f"Error al procesar audio: {e}") |
|
raise |
|
|
|
def agregar_transiciones(clips): |
|
try: |
|
logging.info("Agregando transiciones...") |
|
fx_audio = AudioFileClip(FX_SOUND).set_duration(2.5) |
|
transicion = ImageClip(WATERMARK).set_duration(2.5) |
|
transicion = transicion.resize(height=clips[0].h, resample=Image.Resampling.LANCZOS).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 |
|
): |
|
temp_files = [] |
|
try: |
|
logging.info("Iniciando procesamiento de video...") |
|
|
|
clips = cortar_video(video_input, metodo_corte, duracion_corte) |
|
video_editado = agregar_transiciones(clips) |
|
|
|
|
|
intro = VideoFileClip(INTRO_VIDEO) |
|
outro = VideoFileClip(OUTRO_VIDEO) |
|
video_final = concatenate_videoclips([intro, video_editado, outro]) |
|
|
|
|
|
audio_final = procesar_audio(texto_tts, voz_seleccionada, video_editado.duration) |
|
|
|
|
|
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) |
|
logging.info("Video procesado y guardado temporalmente.") |
|
temp_files.append(tmp.name) |
|
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}") |
|
|
|
|
|
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="Seleccionar 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 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() |