Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,29 +1,251 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
)
|
22 |
|
23 |
-
|
24 |
-
|
25 |
-
inputs=[video_input, texto_tts, voz_seleccionada],
|
26 |
-
outputs=video_output
|
27 |
-
)
|
28 |
-
if name == "main":
|
29 |
-
demo.queue().launch()
|
|
|
1 |
+
import tempfile
|
2 |
+
import logging
|
3 |
+
import os
|
4 |
+
import asyncio
|
5 |
+
from moviepy.editor import *
|
6 |
+
import edge_tts
|
7 |
+
import gradio as gr
|
8 |
+
from pydub import AudioSegment
|
9 |
+
|
10 |
+
# Configuración de Logs
|
11 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
12 |
+
|
13 |
+
# CONSTANTES DE ARCHIVOS
|
14 |
+
INTRO_VIDEO = "introvideo.mp4"
|
15 |
+
OUTRO_VIDEO = "outrovideo.mp4"
|
16 |
+
MUSIC_BG = "musicafondo.mp3"
|
17 |
+
GLITCH_SOUND = "fxsound.mp3" # Efecto de sonido para glitches
|
18 |
+
EJEMPLO_VIDEO = "ejemplo.mp4"
|
19 |
+
|
20 |
+
# Validar existencia de archivos
|
21 |
+
for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, GLITCH_SOUND, EJEMPLO_VIDEO]:
|
22 |
+
if not os.path.exists(file):
|
23 |
+
logging.error(f"Falta archivo necesario: {file}")
|
24 |
+
raise FileNotFoundError(f"Falta: {file}")
|
25 |
+
|
26 |
+
def eliminar_archivo_tiempo(ruta, delay=1800):
|
27 |
+
def eliminar():
|
28 |
+
try:
|
29 |
+
if os.path.exists(ruta):
|
30 |
+
os.remove(ruta)
|
31 |
+
logging.info(f"Archivo eliminado: {ruta}")
|
32 |
+
except Exception as e:
|
33 |
+
logging.error(f"Error al eliminar {ruta}: {e}")
|
34 |
+
from threading import Timer
|
35 |
+
Timer(delay, eliminar).start()
|
36 |
+
|
37 |
+
async def procesar_audio(texto, voz, duracion_video, audio_original):
|
38 |
+
temp_files = []
|
39 |
+
try:
|
40 |
+
# Validar texto
|
41 |
+
if not texto.strip():
|
42 |
+
raise ValueError("El texto para TTS no puede estar vacío.")
|
43 |
+
|
44 |
+
# Dividir el texto en fragmentos si es demasiado largo
|
45 |
+
def dividir_texto(texto, max_length=3000):
|
46 |
+
return [texto[i:i + max_length] for i in range(0, len(texto), max_length)]
|
47 |
+
|
48 |
+
fragmentos = dividir_texto(texto)
|
49 |
+
audios_tts = []
|
50 |
+
|
51 |
+
for fragmento in fragmentos:
|
52 |
+
# Generar TTS
|
53 |
+
communicate = edge_tts.Communicate(fragmento, voz)
|
54 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_tts:
|
55 |
+
try:
|
56 |
+
await communicate.save(tmp_tts.name)
|
57 |
+
except edge_tts.exceptions.NoAudioReceived as e:
|
58 |
+
logging.error(f"Error en TTS: {str(e)}")
|
59 |
+
raise ValueError("No se pudo generar el audio. Verifica tu conexión o los parámetros del TTS.")
|
60 |
+
|
61 |
+
tts_audio = AudioFileClip(tmp_tts.name)
|
62 |
+
temp_files.append(tmp_tts.name)
|
63 |
+
audios_tts.append(tts_audio)
|
64 |
+
|
65 |
+
# Combinar todos los fragmentos de TTS
|
66 |
+
tts_audio_final = concatenate_audioclips(audios_tts)
|
67 |
+
|
68 |
+
# Limitar TTS al video
|
69 |
+
if tts_audio_final.duration > duracion_video:
|
70 |
+
tts_audio_final = tts_audio_final.subclip(0, duracion_video)
|
71 |
+
|
72 |
+
# Preparar música de fondo en loop
|
73 |
+
bg_music = AudioSegment.from_mp3(MUSIC_BG)
|
74 |
+
needed_ms = int(duracion_video * 1000)
|
75 |
+
repeticiones = needed_ms // len(bg_music) + 1
|
76 |
+
bg_music = bg_music * repeticiones
|
77 |
+
bg_music = bg_music[:needed_ms].fade_out(1000)
|
78 |
+
|
79 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_bg:
|
80 |
+
bg_music.export(tmp_bg.name, format="mp3")
|
81 |
+
bg_audio = AudioFileClip(tmp_bg.name).volumex(0.15)
|
82 |
+
temp_files.append(tmp_bg.name)
|
83 |
+
|
84 |
+
# Combinar audios
|
85 |
+
audios = [bg_audio.set_duration(duracion_video)]
|
86 |
+
if audio_original:
|
87 |
+
audios.append(audio_original.volumex(0.7)) # Audio original al 70%
|
88 |
+
audios.append(tts_audio_final.volumex(0.85).set_start(0)) # TTS al 85%
|
89 |
+
|
90 |
+
audio_final = CompositeAudioClip(audios).set_duration(duracion_video)
|
91 |
+
return audio_final
|
92 |
+
|
93 |
+
except Exception as e:
|
94 |
+
logging.error(f" fallo en audio: {str(e)}")
|
95 |
+
raise
|
96 |
+
finally:
|
97 |
+
for file in temp_files:
|
98 |
+
try:
|
99 |
+
os.remove(file)
|
100 |
+
except Exception as e:
|
101 |
+
logging.warning(f"Error limpiando {file}: {e}")
|
102 |
+
|
103 |
+
def aplicar_glitch(video_clip):
|
104 |
+
"""Aplica un efecto de glitch al video."""
|
105 |
+
def glitch_effect(frame):
|
106 |
+
import numpy as np
|
107 |
+
height, width, _ = frame.shape
|
108 |
+
offset = np.random.randint(-10, 10)
|
109 |
+
if offset > 0:
|
110 |
+
offset = min(offset, height)
|
111 |
+
if offset < 0:
|
112 |
+
offset = max(offset, -height + 1)
|
113 |
+
if offset!= 0 and height > 0:
|
114 |
+
frame[offset:, :] = np.roll(frame[:-offset, :], -offset, axis=0)
|
115 |
+
return frame
|
116 |
+
|
117 |
+
return video_clip.fl_image(glitch_effect)
|
118 |
+
|
119 |
+
async def procesar_video(video_input, texto_tts, voz_seleccionada):
|
120 |
+
try:
|
121 |
+
# Cargar componentes
|
122 |
+
intro = VideoFileClip(INTRO_VIDEO)
|
123 |
+
outro = VideoFileClip(OUTRO_VIDEO)
|
124 |
+
video_original = VideoFileClip(video_input)
|
125 |
+
audio_original = video_original.audio
|
126 |
+
|
127 |
+
# Duración del video editado (sin intro/outro)
|
128 |
+
duracion_video = video_original.duration
|
129 |
+
|
130 |
+
# Procesar audio
|
131 |
+
audio_final = await procesar_audio(
|
132 |
+
texto_tts,
|
133 |
+
voz_seleccionada,
|
134 |
+
duracion_video,
|
135 |
+
audio_original
|
136 |
+
)
|
137 |
+
|
138 |
+
# Redimensionar todos los clips a 1920x1080
|
139 |
+
target_width = 1920
|
140 |
+
target_height = 1080
|
141 |
+
|
142 |
+
# Redimensionar intro
|
143 |
+
intro_resized = intro.resize((target_width, target_height))
|
144 |
+
|
145 |
+
# Redimensionar outro
|
146 |
+
outro_resized = outro.resize((target_width, target_height))
|
147 |
+
|
148 |
+
# Redimensionar video principal
|
149 |
+
video_resized = video_original.resize((target_width, target_height))
|
150 |
+
|
151 |
+
# Dividir el video en segmentos de 20 segundos y eliminar 2 segundos en cada corte
|
152 |
+
segment_duration = 20
|
153 |
+
overlap = 2 # Segundos a eliminar en cada corte
|
154 |
+
num_segments = int(duracion_video // (segment_duration - overlap)) + 1
|
155 |
+
segments = []
|
156 |
+
glitch_clips = []
|
157 |
+
glitch_sound = AudioFileClip(GLITCH_SOUND)
|
158 |
+
|
159 |
+
start_time = 0
|
160 |
+
for i in range(num_segments):
|
161 |
+
end_time = min(start_time + segment_duration, duracion_video)
|
162 |
+
if start_time >= duracion_video:
|
163 |
+
break
|
164 |
+
|
165 |
+
# Extraer el segmento de video y audio
|
166 |
+
segment = video_resized.subclip(start_time, end_time)
|
167 |
+
segment_audio = audio_original.subclip(start_time, end_time) # cortar el audio con el video
|
168 |
+
segment = segment.set_audio(segment_audio) # asignando el audio cortado al video
|
169 |
+
|
170 |
+
# Aplicar glitch al inicio del segmento (excepto el primero)
|
171 |
+
if i > 0:
|
172 |
+
glitch_segment = aplicar_glitch(segment.subclip(0, 0.5)) # Glitch de 0.5 segundos
|
173 |
+
glitch_sound_clip = glitch_sound.set_start(start_time).volumex(0.5)
|
174 |
+
glitch_clips.append(glitch_sound_clip)
|
175 |
+
segment = concatenate_videoclips([glitch_segment, segment.subclip(0.5)], method="compose")
|
176 |
+
|
177 |
+
segments.append(segment)
|
178 |
+
|
179 |
+
# Avanzar al siguiente segmento, eliminando 2 segundos
|
180 |
+
start_time += segment_duration - overlap
|
181 |
+
|
182 |
+
# Combinar los segmentos procesados
|
183 |
+
video_final = concatenate_videoclips(segments)
|
184 |
+
|
185 |
+
# Combinar audio con efectos de glitch
|
186 |
+
audio_final = CompositeAudioClip([audio_final] + glitch_clips).set_duration(video_final.duration)
|
187 |
+
|
188 |
+
# Combinar video con audio
|
189 |
+
video_con_audio = video_final.set_audio(audio_final)
|
190 |
+
|
191 |
+
# Concatenar intro + video + outro SIN alteraciones
|
192 |
+
video_final = concatenate_videoclips(
|
193 |
+
[intro_resized, video_con_audio, outro_resized],
|
194 |
+
method="compose", # Evitar problemas de grid
|
195 |
+
padding=0 # Sin espacio entre clips
|
196 |
+
)
|
197 |
+
|
198 |
+
# Renderizar video final con metadatos correctos
|
199 |
+
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
|
200 |
+
video_final.write_videofile(
|
201 |
+
tmp.name,
|
202 |
+
codec="libx264",
|
203 |
+
audio_codec="aac",
|
204 |
+
fps=video_original.fps, # Mantener FPS original
|
205 |
+
threads=4,
|
206 |
+
ffmpeg_params=[
|
207 |
+
"-aspect", "16:9", # Forzar relación de aspecto
|
208 |
+
"-vf", "scale=1920:1080" # Forzar escalado explícito
|
209 |
+
],
|
210 |
+
verbose=False
|
211 |
+
)
|
212 |
+
# eliminar_archivo_tiempo(tmp.name) # Comentar o eliminar esta línea
|
213 |
+
return tmp.name
|
214 |
+
except Exception as e:
|
215 |
+
logging.error(f" fallo general: {str(e)}")
|
216 |
+
raise
|
217 |
+
|
218 |
+
# Interfaz Gradio
|
219 |
+
with gr.Blocks() as demo:
|
220 |
+
gr.Markdown("# Editor de Video con IA")
|
221 |
+
|
222 |
+
with gr.Tab("Principal"):
|
223 |
+
video_input = gr.Video(label="Subir video")
|
224 |
+
texto_tts = gr.Textbox(
|
225 |
+
label="Texto para TTS",
|
226 |
+
lines=3,
|
227 |
+
placeholder="Escribe aquí tu texto..."
|
228 |
+
)
|
229 |
+
voz_seleccionada = gr.Dropdown(
|
230 |
+
label="Voz",
|
231 |
+
choices=["es-ES-AlvaroNeural", "es-MX-BeatrizNeural"],
|
232 |
+
value="es-ES-AlvaroNeural"
|
233 |
+
)
|
234 |
+
procesar_btn = gr.Button("Generar Video")
|
235 |
+
video_output = gr.Video(label="Video Procesado")
|
236 |
+
|
237 |
+
with gr.Accordion("Ejemplos de Uso", open=False):
|
238 |
+
gr.Examples(
|
239 |
+
examples=[[EJEMPLO_VIDEO, "¡Hola! Esto es una prueba. Suscríbete al canal."]],
|
240 |
+
inputs=[video_input, texto_tts],
|
241 |
+
label="Ejemplos"
|
242 |
+
)
|
243 |
+
|
244 |
+
procesar_btn.click(
|
245 |
+
procesar_video,
|
246 |
+
inputs=[video_input, texto_tts, voz_seleccionada],
|
247 |
+
outputs=video_output
|
248 |
)
|
249 |
|
250 |
+
if __name__ == "__main__":
|
251 |
+
demo.queue().launch()
|
|
|
|
|
|
|
|
|
|