Update app.py
Browse files
app.py
CHANGED
@@ -10,8 +10,10 @@ import edge_tts
|
|
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,12 +21,14 @@ FX_SOUND = "fxsound.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,42 +39,49 @@ def eliminar_archivo_tiempo(ruta, delay=1800):
|
|
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")
|
41 |
if any(c in texto_limpio for c in ["|", "\n", "\r"]):
|
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 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
|
|
|
|
54 |
|
55 |
-
|
56 |
-
|
|
|
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
|
65 |
-
bg_music.export(
|
66 |
-
bg_audio = AudioFileClip(
|
67 |
-
temp_files.append(
|
68 |
|
|
|
69 |
audio_final = CompositeAudioClip([
|
70 |
bg_audio.set_duration(duracion_total),
|
71 |
-
tts_audio.volumex(0.85)
|
72 |
-
|
73 |
-
|
|
|
74 |
|
75 |
return audio_final
|
76 |
|
@@ -85,6 +96,7 @@ async def procesar_audio(texto, voz, duracion_total, duracion_intro):
|
|
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)
|
@@ -109,9 +121,11 @@ def agregar_transiciones(clips):
|
|
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)):
|
@@ -120,22 +134,37 @@ async def procesar_video(video_input, texto_tts, voz_seleccionada, metodo_corte,
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
132 |
|
|
|
133 |
audios = [audio_tts_bg]
|
134 |
if audio_original:
|
135 |
-
audios.append(
|
|
|
|
|
|
|
|
|
136 |
|
137 |
-
audio_final = CompositeAudioClip(audios).set_duration(
|
138 |
|
|
|
139 |
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
|
140 |
video_final.set_audio(audio_final).write_videofile(
|
141 |
tmp.name,
|
@@ -151,6 +180,7 @@ async def procesar_video(video_input, texto_tts, voz_seleccionada, metodo_corte,
|
|
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 |
|
|
|
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 |
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 temporales despu茅s de 30 minutos"""
|
32 |
def eliminar():
|
33 |
try:
|
34 |
if os.path.exists(ruta):
|
|
|
39 |
Timer(delay, eliminar).start()
|
40 |
|
41 |
def validar_texto(texto):
|
42 |
+
"""Valida el texto para evitar errores en TTS"""
|
43 |
texto_limpio = texto.strip()
|
44 |
if len(texto_limpio) < 3:
|
45 |
raise gr.Error("鈿狅笍 El texto debe tener al menos 3 caracteres")
|
46 |
if any(c in texto_limpio for c in ["|", "\n", "\r"]):
|
47 |
raise gr.Error("鈿狅笍 Caracteres no permitidos detectados")
|
48 |
|
49 |
+
async def procesar_audio(texto, voz, duracion_total, duracion_intro, max_tts_time):
|
50 |
+
"""Genera y mezcla audio con protecci贸n de duraci贸n"""
|
51 |
temp_files = []
|
52 |
try:
|
53 |
validar_texto(texto)
|
|
|
54 |
|
55 |
+
# Generar TTS
|
56 |
+
communicate = edge_tts.Communicate(texto, voz)
|
57 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_tts:
|
58 |
+
await communicate.save(tmp_tts.name)
|
59 |
+
tts_audio = AudioFileClip(tmp_tts.name)
|
60 |
+
temp_files.append(tmp_tts.name)
|
61 |
|
62 |
+
# Asegurar TTS no exceda el tiempo disponible
|
63 |
+
if tts_audio.duration > max_tts_time:
|
64 |
+
tts_audio = tts_audio.subclip(0, max_tts_time)
|
65 |
|
66 |
+
# Procesar m煤sica de fondo
|
67 |
bg_music = AudioSegment.from_mp3(MUSIC_BG)
|
68 |
needed_ms = int(duracion_total * 1000)
|
69 |
repeticiones = needed_ms // len(bg_music) + 1
|
70 |
bg_music = bg_music * repeticiones
|
71 |
bg_music = bg_music[:needed_ms].fade_out(5000)
|
72 |
|
73 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_bg:
|
74 |
+
bg_music.export(tmp_bg.name, format="mp3")
|
75 |
+
bg_audio = AudioFileClip(tmp_bg.name).volumex(0.15)
|
76 |
+
temp_files.append(tmp_bg.name)
|
77 |
|
78 |
+
# Combinar audios con duraciones exactas
|
79 |
audio_final = CompositeAudioClip([
|
80 |
bg_audio.set_duration(duracion_total),
|
81 |
+
tts_audio.volumex(0.85)
|
82 |
+
.set_start(duracion_intro)
|
83 |
+
.set_duration(max_tts_time)
|
84 |
+
]).set_duration(duracion_total)
|
85 |
|
86 |
return audio_final
|
87 |
|
|
|
96 |
logging.warning(f"Error limpiando {file}: {e}")
|
97 |
|
98 |
def agregar_transiciones(clips):
|
99 |
+
"""Agrega transiciones visuales cada 40 segundos"""
|
100 |
try:
|
101 |
fx_audio = AudioFileClip(FX_SOUND).subclip(0, 0.5)
|
102 |
watermark = (ImageClip(WATERMARK)
|
|
|
121 |
|
122 |
async def procesar_video(video_input, texto_tts, voz_seleccionada, metodo_corte, duracion_corte):
|
123 |
try:
|
124 |
+
# Cargar video 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 seg煤n m茅todo
|
129 |
clips = []
|
130 |
if metodo_corte == "manual":
|
131 |
for i in range(math.ceil(video_original.duration / duracion_corte)):
|
|
|
134 |
clips = [video_original.subclip(i, min(i+40, video_original.duration))
|
135 |
for i in range(0, math.ceil(video_original.duration), 40)]
|
136 |
|
137 |
+
# Procesar transiciones visuales
|
138 |
video_editado = agregar_transiciones(clips)
|
139 |
+
video_editado_duration = video_editado.duration
|
140 |
+
|
141 |
+
# Combinar con intro/outro
|
142 |
intro = VideoFileClip(INTRO_VIDEO)
|
143 |
outro = VideoFileClip(OUTRO_VIDEO)
|
144 |
video_final = concatenate_videoclips([intro, video_editado, outro])
|
|
|
145 |
duracion_total = video_final.duration
|
|
|
146 |
|
147 |
+
# Procesar audio (recibe duraci贸n exacta para TTS)
|
148 |
+
audio_tts_bg = await procesar_audio(
|
149 |
+
texto_tts,
|
150 |
+
voz_seleccionada,
|
151 |
+
duracion_total,
|
152 |
+
intro.duration,
|
153 |
+
video_editado_duration
|
154 |
+
)
|
155 |
|
156 |
+
# Combinar todos los audios
|
157 |
audios = [audio_tts_bg]
|
158 |
if audio_original:
|
159 |
+
audios.append(
|
160 |
+
audio_original
|
161 |
+
.set_duration(video_editado_duration)
|
162 |
+
.set_start(intro.duration)
|
163 |
+
)
|
164 |
|
165 |
+
audio_final = CompositeAudioClip(audios).set_duration(duracion_total)
|
166 |
|
167 |
+
# Renderizar video final
|
168 |
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
|
169 |
video_final.set_audio(audio_final).write_videofile(
|
170 |
tmp.name,
|
|
|
180 |
logging.error(f" fallo general: {str(e)}")
|
181 |
raise
|
182 |
|
183 |
+
# Interfaz Gradio
|
184 |
with gr.Blocks() as demo:
|
185 |
gr.Markdown("# Editor de Video con IA")
|
186 |
|