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 |
|