Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -10,10 +10,8 @@ import edge_tts
|
|
| 10 |
import gradio as gr
|
| 11 |
from pydub import AudioSegment
|
| 12 |
|
| 13 |
-
# Configuraci贸n de Logs
|
| 14 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 15 |
|
| 16 |
-
# CONSTANTES DE ARCHIVOS
|
| 17 |
INTRO_VIDEO = "introvideo.mp4"
|
| 18 |
OUTRO_VIDEO = "outrovideo.mp4"
|
| 19 |
MUSIC_BG = "musicafondo.mp3"
|
|
@@ -21,14 +19,12 @@ FX_SOUND = "fxsound.mp3"
|
|
| 21 |
WATERMARK = "watermark.png"
|
| 22 |
EJEMPLO_VIDEO = "ejemplo.mp4"
|
| 23 |
|
| 24 |
-
# Validar existencia de archivos
|
| 25 |
for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, FX_SOUND, WATERMARK, EJEMPLO_VIDEO]:
|
| 26 |
if not os.path.exists(file):
|
| 27 |
logging.error(f"Falta archivo necesario: {file}")
|
| 28 |
raise FileNotFoundError(f"Falta: {file}")
|
| 29 |
|
| 30 |
def eliminar_archivo_tiempo(ruta, delay=1800):
|
| 31 |
-
"""Elimina archivos despu茅s de 30 minutos"""
|
| 32 |
def eliminar():
|
| 33 |
try:
|
| 34 |
if os.path.exists(ruta):
|
|
@@ -39,7 +35,6 @@ def eliminar_archivo_tiempo(ruta, delay=1800):
|
|
| 39 |
Timer(delay, eliminar).start()
|
| 40 |
|
| 41 |
def validar_texto(texto):
|
| 42 |
-
"""Validaci贸n de texto para TTS"""
|
| 43 |
texto_limpio = texto.strip()
|
| 44 |
if len(texto_limpio) < 3:
|
| 45 |
raise gr.Error("鈿狅笍 El texto debe tener al menos 3 caracteres")
|
|
@@ -47,28 +42,23 @@ def validar_texto(texto):
|
|
| 47 |
raise gr.Error("鈿狅笍 Caracteres no permitidos detectados")
|
| 48 |
|
| 49 |
async def procesar_audio(texto, voz, duracion_total, duracion_intro):
|
| 50 |
-
"""Genera TTS y mezcla con m煤sica (versi贸n corregida)"""
|
| 51 |
temp_files = []
|
| 52 |
try:
|
| 53 |
-
# Validar texto
|
| 54 |
validar_texto(texto)
|
| 55 |
-
|
| 56 |
-
# Generar TTS
|
| 57 |
communicate = edge_tts.Communicate(texto, voz)
|
|
|
|
| 58 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
|
| 59 |
await communicate.save(tmp.name)
|
| 60 |
tts_audio = AudioFileClip(tmp.name)
|
| 61 |
temp_files.append(tmp.name)
|
| 62 |
|
| 63 |
-
# Verificar audio v谩lido
|
| 64 |
if tts_audio.duration < 0.5:
|
| 65 |
raise RuntimeError(f"Audio TTS inv谩lido ({tts_audio.duration}s)")
|
| 66 |
|
| 67 |
-
# Procesar m煤sica de fondo
|
| 68 |
bg_music = AudioSegment.from_mp3(MUSIC_BG)
|
| 69 |
-
needed_ms = int(duracion_total * 1000)
|
| 70 |
repeticiones = needed_ms // len(bg_music) + 1
|
| 71 |
-
bg_music = bg_music * repeticiones
|
| 72 |
bg_music = bg_music[:needed_ms].fade_out(5000)
|
| 73 |
|
| 74 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
|
|
@@ -76,18 +66,18 @@ async def procesar_audio(texto, voz, duracion_total, duracion_intro):
|
|
| 76 |
bg_audio = AudioFileClip(tmp.name).volumex(0.15)
|
| 77 |
temp_files.append(tmp.name)
|
| 78 |
|
| 79 |
-
# Combinar audios
|
| 80 |
audio_final = CompositeAudioClip([
|
| 81 |
-
bg_audio,
|
| 82 |
tts_audio.volumex(0.85).set_start(duracion_intro)
|
|
|
|
| 83 |
])
|
| 84 |
|
| 85 |
return audio_final
|
|
|
|
| 86 |
except Exception as e:
|
| 87 |
logging.error(f" fallo en audio: {str(e)}")
|
| 88 |
raise
|
| 89 |
finally:
|
| 90 |
-
# Limpiar archivos
|
| 91 |
for file in temp_files:
|
| 92 |
try:
|
| 93 |
os.remove(file)
|
|
@@ -95,7 +85,6 @@ async def procesar_audio(texto, voz, duracion_total, duracion_intro):
|
|
| 95 |
logging.warning(f"Error limpiando {file}: {e}")
|
| 96 |
|
| 97 |
def agregar_transiciones(clips):
|
| 98 |
-
"""Transiciones profesionales cada 40s"""
|
| 99 |
try:
|
| 100 |
fx_audio = AudioFileClip(FX_SOUND).subclip(0, 0.5)
|
| 101 |
watermark = (ImageClip(WATERMARK)
|
|
@@ -107,7 +96,6 @@ def agregar_transiciones(clips):
|
|
| 107 |
for i, clip in enumerate(clips):
|
| 108 |
clip_watermarked = CompositeVideoClip([clip, watermark])
|
| 109 |
|
| 110 |
-
# Agregar transici贸n cada 40s
|
| 111 |
if i > 0 and i % 40 == 0:
|
| 112 |
transicion = CompositeVideoClip([watermark.set_duration(0.5)]).set_audio(fx_audio)
|
| 113 |
clips_finales.append(transicion)
|
|
@@ -121,45 +109,41 @@ def agregar_transiciones(clips):
|
|
| 121 |
|
| 122 |
async def procesar_video(video_input, texto_tts, voz_seleccionada, metodo_corte, duracion_corte):
|
| 123 |
try:
|
| 124 |
-
# Cargar video con audio original
|
| 125 |
video_original = VideoFileClip(video_input)
|
| 126 |
audio_original = video_original.audio.volumex(0.7) if video_original.audio else None
|
| 127 |
|
| 128 |
-
# Cortar video
|
| 129 |
clips = []
|
| 130 |
if metodo_corte == "manual":
|
| 131 |
for i in range(math.ceil(video_original.duration / duracion_corte)):
|
| 132 |
clips.append(video_original.subclip(i*duracion_corte, (i+1)*duracion_corte))
|
| 133 |
else:
|
| 134 |
-
clips = [video_original.subclip(i, i+40
|
|
|
|
| 135 |
|
| 136 |
-
# Procesar transiciones
|
| 137 |
video_editado = agregar_transiciones(clips)
|
| 138 |
-
|
| 139 |
-
# Combinar con intro/outro
|
| 140 |
intro = VideoFileClip(INTRO_VIDEO)
|
| 141 |
outro = VideoFileClip(OUTRO_VIDEO)
|
| 142 |
video_final = concatenate_videoclips([intro, video_editado, outro])
|
| 143 |
|
| 144 |
-
# Calcular duraciones
|
| 145 |
duracion_total = video_final.duration
|
| 146 |
-
|
|
|
|
|
|
|
| 147 |
|
| 148 |
-
# Combinar todos los audios
|
| 149 |
audios = [audio_tts_bg]
|
| 150 |
if audio_original:
|
| 151 |
audios.append(audio_original.set_duration(video_final.duration))
|
| 152 |
|
| 153 |
-
audio_final = CompositeAudioClip(audios)
|
| 154 |
|
| 155 |
-
# Renderizar video final
|
| 156 |
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
|
| 157 |
video_final.set_audio(audio_final).write_videofile(
|
| 158 |
tmp.name,
|
| 159 |
codec="libx264",
|
| 160 |
audio_codec="aac",
|
| 161 |
fps=24,
|
| 162 |
-
threads=4
|
|
|
|
| 163 |
)
|
| 164 |
eliminar_archivo_tiempo(tmp.name)
|
| 165 |
return tmp.name
|
|
@@ -167,7 +151,6 @@ async def procesar_video(video_input, texto_tts, voz_seleccionada, metodo_corte,
|
|
| 167 |
logging.error(f" fallo general: {str(e)}")
|
| 168 |
raise
|
| 169 |
|
| 170 |
-
# Interfaz Gradio
|
| 171 |
with gr.Blocks() as demo:
|
| 172 |
gr.Markdown("# Editor de Video con IA")
|
| 173 |
|
|
@@ -197,25 +180,16 @@ with gr.Blocks() as demo:
|
|
| 197 |
label="Segundos por corte (manual)"
|
| 198 |
)
|
| 199 |
|
| 200 |
-
# Ejemplos en footer
|
| 201 |
with gr.Accordion("Ejemplos de Uso", open=False):
|
| 202 |
gr.Examples(
|
| 203 |
-
examples=[
|
| 204 |
-
[EJEMPLO_VIDEO, "隆Hola! Esto es una prueba. Suscr铆bete al canal y activa la campanita."],
|
| 205 |
-
],
|
| 206 |
inputs=[video_input, texto_tts],
|
| 207 |
label="Ejemplos"
|
| 208 |
)
|
| 209 |
|
| 210 |
procesar_btn.click(
|
| 211 |
procesar_video,
|
| 212 |
-
inputs=[
|
| 213 |
-
video_input,
|
| 214 |
-
texto_tts,
|
| 215 |
-
voz_seleccionada,
|
| 216 |
-
metodo_corte,
|
| 217 |
-
duracion_corte
|
| 218 |
-
],
|
| 219 |
outputs=video_output
|
| 220 |
)
|
| 221 |
|
|
|
|
| 10 |
import gradio as gr
|
| 11 |
from pydub import AudioSegment
|
| 12 |
|
|
|
|
| 13 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 14 |
|
|
|
|
| 15 |
INTRO_VIDEO = "introvideo.mp4"
|
| 16 |
OUTRO_VIDEO = "outrovideo.mp4"
|
| 17 |
MUSIC_BG = "musicafondo.mp3"
|
|
|
|
| 19 |
WATERMARK = "watermark.png"
|
| 20 |
EJEMPLO_VIDEO = "ejemplo.mp4"
|
| 21 |
|
|
|
|
| 22 |
for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, FX_SOUND, WATERMARK, EJEMPLO_VIDEO]:
|
| 23 |
if not os.path.exists(file):
|
| 24 |
logging.error(f"Falta archivo necesario: {file}")
|
| 25 |
raise FileNotFoundError(f"Falta: {file}")
|
| 26 |
|
| 27 |
def eliminar_archivo_tiempo(ruta, delay=1800):
|
|
|
|
| 28 |
def eliminar():
|
| 29 |
try:
|
| 30 |
if os.path.exists(ruta):
|
|
|
|
| 35 |
Timer(delay, eliminar).start()
|
| 36 |
|
| 37 |
def validar_texto(texto):
|
|
|
|
| 38 |
texto_limpio = texto.strip()
|
| 39 |
if len(texto_limpio) < 3:
|
| 40 |
raise gr.Error("鈿狅笍 El texto debe tener al menos 3 caracteres")
|
|
|
|
| 42 |
raise gr.Error("鈿狅笍 Caracteres no permitidos detectados")
|
| 43 |
|
| 44 |
async def procesar_audio(texto, voz, duracion_total, duracion_intro):
|
|
|
|
| 45 |
temp_files = []
|
| 46 |
try:
|
|
|
|
| 47 |
validar_texto(texto)
|
|
|
|
|
|
|
| 48 |
communicate = edge_tts.Communicate(texto, voz)
|
| 49 |
+
|
| 50 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
|
| 51 |
await communicate.save(tmp.name)
|
| 52 |
tts_audio = AudioFileClip(tmp.name)
|
| 53 |
temp_files.append(tmp.name)
|
| 54 |
|
|
|
|
| 55 |
if tts_audio.duration < 0.5:
|
| 56 |
raise RuntimeError(f"Audio TTS inv谩lido ({tts_audio.duration}s)")
|
| 57 |
|
|
|
|
| 58 |
bg_music = AudioSegment.from_mp3(MUSIC_BG)
|
| 59 |
+
needed_ms = int(duracion_total * 1000)
|
| 60 |
repeticiones = needed_ms // len(bg_music) + 1
|
| 61 |
+
bg_music = bg_music * repeticiones
|
| 62 |
bg_music = bg_music[:needed_ms].fade_out(5000)
|
| 63 |
|
| 64 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
|
|
|
|
| 66 |
bg_audio = AudioFileClip(tmp.name).volumex(0.15)
|
| 67 |
temp_files.append(tmp.name)
|
| 68 |
|
|
|
|
| 69 |
audio_final = CompositeAudioClip([
|
| 70 |
+
bg_audio.set_duration(duracion_total),
|
| 71 |
tts_audio.volumex(0.85).set_start(duracion_intro)
|
| 72 |
+
.set_duration(duracion_total - duracion_intro)
|
| 73 |
])
|
| 74 |
|
| 75 |
return audio_final
|
| 76 |
+
|
| 77 |
except Exception as e:
|
| 78 |
logging.error(f" fallo en audio: {str(e)}")
|
| 79 |
raise
|
| 80 |
finally:
|
|
|
|
| 81 |
for file in temp_files:
|
| 82 |
try:
|
| 83 |
os.remove(file)
|
|
|
|
| 85 |
logging.warning(f"Error limpiando {file}: {e}")
|
| 86 |
|
| 87 |
def agregar_transiciones(clips):
|
|
|
|
| 88 |
try:
|
| 89 |
fx_audio = AudioFileClip(FX_SOUND).subclip(0, 0.5)
|
| 90 |
watermark = (ImageClip(WATERMARK)
|
|
|
|
| 96 |
for i, clip in enumerate(clips):
|
| 97 |
clip_watermarked = CompositeVideoClip([clip, watermark])
|
| 98 |
|
|
|
|
| 99 |
if i > 0 and i % 40 == 0:
|
| 100 |
transicion = CompositeVideoClip([watermark.set_duration(0.5)]).set_audio(fx_audio)
|
| 101 |
clips_finales.append(transicion)
|
|
|
|
| 109 |
|
| 110 |
async def procesar_video(video_input, texto_tts, voz_seleccionada, metodo_corte, duracion_corte):
|
| 111 |
try:
|
|
|
|
| 112 |
video_original = VideoFileClip(video_input)
|
| 113 |
audio_original = video_original.audio.volumex(0.7) if video_original.audio else None
|
| 114 |
|
|
|
|
| 115 |
clips = []
|
| 116 |
if metodo_corte == "manual":
|
| 117 |
for i in range(math.ceil(video_original.duration / duracion_corte)):
|
| 118 |
clips.append(video_original.subclip(i*duracion_corte, (i+1)*duracion_corte))
|
| 119 |
else:
|
| 120 |
+
clips = [video_original.subclip(i, min(i+40, video_original.duration))
|
| 121 |
+
for i in range(0, math.ceil(video_original.duration), 40)]
|
| 122 |
|
|
|
|
| 123 |
video_editado = agregar_transiciones(clips)
|
|
|
|
|
|
|
| 124 |
intro = VideoFileClip(INTRO_VIDEO)
|
| 125 |
outro = VideoFileClip(OUTRO_VIDEO)
|
| 126 |
video_final = concatenate_videoclips([intro, video_editado, outro])
|
| 127 |
|
|
|
|
| 128 |
duracion_total = video_final.duration
|
| 129 |
+
duracion_intro = intro.duration
|
| 130 |
+
|
| 131 |
+
audio_tts_bg = await procesar_audio(texto_tts, voz_seleccionada, duracion_total, duracion_intro)
|
| 132 |
|
|
|
|
| 133 |
audios = [audio_tts_bg]
|
| 134 |
if audio_original:
|
| 135 |
audios.append(audio_original.set_duration(video_final.duration))
|
| 136 |
|
| 137 |
+
audio_final = CompositeAudioClip(audios).set_duration(video_final.duration)
|
| 138 |
|
|
|
|
| 139 |
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
|
| 140 |
video_final.set_audio(audio_final).write_videofile(
|
| 141 |
tmp.name,
|
| 142 |
codec="libx264",
|
| 143 |
audio_codec="aac",
|
| 144 |
fps=24,
|
| 145 |
+
threads=4,
|
| 146 |
+
verbose=False
|
| 147 |
)
|
| 148 |
eliminar_archivo_tiempo(tmp.name)
|
| 149 |
return tmp.name
|
|
|
|
| 151 |
logging.error(f" fallo general: {str(e)}")
|
| 152 |
raise
|
| 153 |
|
|
|
|
| 154 |
with gr.Blocks() as demo:
|
| 155 |
gr.Markdown("# Editor de Video con IA")
|
| 156 |
|
|
|
|
| 180 |
label="Segundos por corte (manual)"
|
| 181 |
)
|
| 182 |
|
|
|
|
| 183 |
with gr.Accordion("Ejemplos de Uso", open=False):
|
| 184 |
gr.Examples(
|
| 185 |
+
examples=[[EJEMPLO_VIDEO, "隆Hola! Esto es una prueba. Suscr铆bete al canal y activa la campanita."]],
|
|
|
|
|
|
|
| 186 |
inputs=[video_input, texto_tts],
|
| 187 |
label="Ejemplos"
|
| 188 |
)
|
| 189 |
|
| 190 |
procesar_btn.click(
|
| 191 |
procesar_video,
|
| 192 |
+
inputs=[video_input, texto_tts, voz_seleccionada, metodo_corte, duracion_corte],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
outputs=video_output
|
| 194 |
)
|
| 195 |
|