Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
@@ -17,31 +17,33 @@ logger = logging.getLogger(__name__)
|
|
17 |
# Clave API de Pexels (configurar en Secrets de Hugging Face)
|
18 |
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY", "YOUR_API_KEY")
|
19 |
|
20 |
-
# --- Funciones optimizadas
|
21 |
|
22 |
def extract_keywords(text, max_keywords=3):
|
23 |
-
"""Extrae palabras clave usando un método
|
24 |
-
# Limpieza de texto
|
25 |
text = re.sub(r'[^\w\s]', '', text.lower())
|
26 |
-
words =
|
27 |
|
28 |
-
# Palabras comunes a excluir
|
29 |
-
stop_words = {
|
|
|
|
|
|
|
30 |
|
31 |
-
# Frecuencia de palabras
|
32 |
word_freq = {}
|
33 |
for word in words:
|
34 |
if len(word) > 3 and word not in stop_words:
|
35 |
word_freq[word] = word_freq.get(word, 0) + 1
|
36 |
|
37 |
-
# Ordenar por frecuencia
|
38 |
-
sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)
|
39 |
return [word for word, _ in sorted_words[:max_keywords]]
|
40 |
|
41 |
def search_pexels_videos(keywords, per_query=2):
|
42 |
-
"""Busca videos en Pexels
|
43 |
-
if not PEXELS_API_KEY:
|
44 |
-
logger.error("API_KEY de Pexels no configurada")
|
45 |
return []
|
46 |
|
47 |
headers = {"Authorization": PEXELS_API_KEY}
|
@@ -49,6 +51,7 @@ def search_pexels_videos(keywords, per_query=2):
|
|
49 |
|
50 |
for query in keywords:
|
51 |
try:
|
|
|
52 |
params = {
|
53 |
"query": query,
|
54 |
"per_page": per_query,
|
@@ -60,7 +63,7 @@ def search_pexels_videos(keywords, per_query=2):
|
|
60 |
"https://api.pexels.com/videos/search",
|
61 |
headers=headers,
|
62 |
params=params,
|
63 |
-
timeout=
|
64 |
)
|
65 |
|
66 |
if response.status_code == 200:
|
@@ -76,190 +79,213 @@ def search_pexels_videos(keywords, per_query=2):
|
|
76 |
key=lambda x: x.get("width", 0) * x.get("height", 0)
|
77 |
)
|
78 |
video_urls.append(best_quality["link"])
|
|
|
|
|
|
|
79 |
except Exception as e:
|
80 |
-
logger.error(f"Error buscando videos: {e}")
|
81 |
|
82 |
return video_urls
|
83 |
|
84 |
async def generate_tts(text, output_path, voice="es-ES-ElviraNeural"):
|
85 |
-
"""Genera audio TTS
|
86 |
try:
|
87 |
communicate = edge_tts.Communicate(text, voice)
|
88 |
await communicate.save(output_path)
|
|
|
89 |
return True
|
90 |
except Exception as e:
|
91 |
-
logger.error(f"Error en TTS: {e}")
|
92 |
return False
|
93 |
|
94 |
def download_video(url, temp_dir):
|
95 |
-
"""Descarga
|
96 |
try:
|
97 |
-
|
|
|
98 |
response.raise_for_status()
|
99 |
|
100 |
-
filename = f"video_{os.getpid()}.mp4"
|
101 |
filepath = os.path.join(temp_dir, filename)
|
102 |
|
103 |
with open(filepath, 'wb') as f:
|
104 |
for chunk in response.iter_content(chunk_size=8192):
|
105 |
f.write(chunk)
|
106 |
-
|
|
|
107 |
return filepath
|
108 |
except Exception as e:
|
109 |
-
logger.error(f"Error descargando video: {e}")
|
110 |
return None
|
111 |
|
112 |
def create_video(audio_path, video_paths, output_path):
|
113 |
-
"""Crea el video final
|
114 |
try:
|
115 |
-
# Crear archivo de lista para concatenación
|
116 |
-
|
117 |
-
with open(
|
118 |
for path in video_paths:
|
119 |
f.write(f"file '{os.path.basename(path)}'\n")
|
120 |
|
121 |
-
#
|
122 |
-
os.chdir(os.path.dirname(video_paths[0]))
|
123 |
-
|
124 |
-
# Comando FFmpeg para concatenar videos y añadir audio
|
125 |
cmd = [
|
126 |
"ffmpeg", "-y",
|
127 |
"-f", "concat",
|
128 |
"-safe", "0",
|
129 |
-
"-i",
|
130 |
"-i", audio_path,
|
131 |
-
"-c:v", "
|
|
|
|
|
132 |
"-c:a", "aac",
|
|
|
133 |
"-shortest",
|
|
|
134 |
output_path
|
135 |
]
|
136 |
|
137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
return True
|
139 |
except Exception as e:
|
140 |
-
logger.error(f"Error creando video: {e}")
|
141 |
return False
|
142 |
finally:
|
143 |
-
|
144 |
-
os.
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
try:
|
149 |
-
speech = AudioSegment.from_file(audio_path)
|
150 |
-
background = AudioSegment.from_file(music_path) - 20 # Reducir volumen
|
151 |
-
|
152 |
-
# Extender música si es necesario
|
153 |
-
if len(background) < len(speech):
|
154 |
-
loops = math.ceil(len(speech) / len(background))
|
155 |
-
background = background * loops
|
156 |
-
|
157 |
-
combined = speech.overlay(background[:len(speech)])
|
158 |
-
|
159 |
-
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
|
160 |
-
combined.export(tmp_file.name, format="mp3")
|
161 |
-
return tmp_file.name
|
162 |
-
except Exception as e:
|
163 |
-
logger.error(f"Error mezclando audio: {e}")
|
164 |
-
return audio_path
|
165 |
|
166 |
async def generate_video(text, music_file=None):
|
167 |
-
"""Función principal
|
168 |
temp_dir = tempfile.mkdtemp()
|
169 |
-
|
170 |
|
171 |
try:
|
172 |
# 1. Generar audio TTS
|
173 |
tts_path = os.path.join(temp_dir, "audio.mp3")
|
174 |
if not await generate_tts(text, tts_path):
|
175 |
-
return None, "Error generando voz"
|
176 |
-
output_files.append(tts_path)
|
177 |
-
|
178 |
-
# 2. Añadir música de fondo si está disponible
|
179 |
-
final_audio = tts_path
|
180 |
-
if music_file:
|
181 |
-
mixed_audio = add_background_music(tts_path, music_file)
|
182 |
-
if mixed_audio != tts_path:
|
183 |
-
final_audio = mixed_audio
|
184 |
-
output_files.append(mixed_audio)
|
185 |
|
186 |
-
#
|
187 |
keywords = extract_keywords(text)
|
188 |
-
logger.info(f"Palabras clave identificadas: {keywords}")
|
189 |
-
|
190 |
if not keywords:
|
191 |
-
return None, "No se pudieron extraer palabras clave del texto"
|
|
|
192 |
|
193 |
-
#
|
194 |
video_urls = search_pexels_videos(keywords)
|
195 |
if not video_urls:
|
196 |
-
return None, "No se encontraron videos para las palabras clave"
|
197 |
|
198 |
video_paths = []
|
199 |
for url in video_urls:
|
200 |
path = download_video(url, temp_dir)
|
201 |
if path:
|
202 |
video_paths.append(path)
|
203 |
-
output_files.append(path)
|
204 |
|
205 |
if not video_paths:
|
206 |
-
return None, "Error descargando videos"
|
207 |
|
208 |
-
#
|
209 |
output_path = os.path.join(temp_dir, "final_video.mp4")
|
210 |
-
if create_video(
|
211 |
-
return
|
212 |
-
|
213 |
-
|
214 |
|
215 |
except Exception as e:
|
216 |
logger.exception("Error inesperado")
|
217 |
-
return None, f"Error: {str(e)}"
|
218 |
finally:
|
219 |
-
#
|
220 |
pass
|
221 |
|
222 |
-
# --- Interfaz de Gradio
|
223 |
-
with gr.Blocks(title="Generador Automático de Videos
|
224 |
-
gr.Markdown("
|
225 |
-
|
|
|
|
|
226 |
|
227 |
with gr.Row():
|
228 |
-
with gr.Column():
|
229 |
text_input = gr.Textbox(
|
230 |
label="Texto para el video",
|
231 |
-
placeholder="
|
232 |
-
lines=5
|
|
|
233 |
)
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
|
|
|
|
|
|
239 |
|
240 |
-
with gr.Column():
|
241 |
-
video_output = gr.Video(
|
242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
243 |
|
244 |
generate_btn.click(
|
245 |
-
fn=lambda: (None, "Procesando...
|
246 |
outputs=[video_output, status_output],
|
247 |
queue=False
|
248 |
).then(
|
249 |
fn=generate_video,
|
250 |
-
inputs=[text_input
|
251 |
outputs=[video_output, status_output]
|
252 |
)
|
253 |
|
254 |
-
gr.Markdown("###
|
255 |
gr.Markdown("""
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
- **Procesamiento eficiente** con FFmpeg
|
261 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
262 |
|
263 |
# Para Hugging Face Spaces
|
264 |
if __name__ == "__main__":
|
265 |
-
demo.launch(
|
|
|
|
|
|
|
|
|
|
|
|
17 |
# Clave API de Pexels (configurar en Secrets de Hugging Face)
|
18 |
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY", "YOUR_API_KEY")
|
19 |
|
20 |
+
# --- Funciones optimizadas y corregidas ---
|
21 |
|
22 |
def extract_keywords(text, max_keywords=3):
|
23 |
+
"""Extrae palabras clave usando un método mejorado"""
|
24 |
+
# Limpieza de texto y tokenización
|
25 |
text = re.sub(r'[^\w\s]', '', text.lower())
|
26 |
+
words = re.findall(r'\b\w+\b', text)
|
27 |
|
28 |
+
# Palabras comunes a excluir (lista ampliada)
|
29 |
+
stop_words = {
|
30 |
+
"el", "la", "los", "las", "de", "en", "y", "a", "que", "es", "por",
|
31 |
+
"un", "una", "con", "se", "del", "al", "lo", "como", "para", "su", "sus"
|
32 |
+
}
|
33 |
|
34 |
+
# Frecuencia de palabras y filtrado
|
35 |
word_freq = {}
|
36 |
for word in words:
|
37 |
if len(word) > 3 and word not in stop_words:
|
38 |
word_freq[word] = word_freq.get(word, 0) + 1
|
39 |
|
40 |
+
# Ordenar por frecuencia y longitud
|
41 |
+
sorted_words = sorted(word_freq.items(), key=lambda x: (x[1], len(x[0])), reverse=True)
|
42 |
return [word for word, _ in sorted_words[:max_keywords]]
|
43 |
|
44 |
def search_pexels_videos(keywords, per_query=2):
|
45 |
+
"""Busca videos en Pexels con manejo de errores mejorado"""
|
46 |
+
if not PEXELS_API_KEY or not keywords:
|
|
|
47 |
return []
|
48 |
|
49 |
headers = {"Authorization": PEXELS_API_KEY}
|
|
|
51 |
|
52 |
for query in keywords:
|
53 |
try:
|
54 |
+
logger.info(f"Buscando videos para: '{query}'")
|
55 |
params = {
|
56 |
"query": query,
|
57 |
"per_page": per_query,
|
|
|
63 |
"https://api.pexels.com/videos/search",
|
64 |
headers=headers,
|
65 |
params=params,
|
66 |
+
timeout=20
|
67 |
)
|
68 |
|
69 |
if response.status_code == 200:
|
|
|
79 |
key=lambda x: x.get("width", 0) * x.get("height", 0)
|
80 |
)
|
81 |
video_urls.append(best_quality["link"])
|
82 |
+
logger.info(f"Video encontrado: {best_quality['link']}")
|
83 |
+
else:
|
84 |
+
logger.warning(f"Respuesta Pexels: {response.status_code}")
|
85 |
except Exception as e:
|
86 |
+
logger.error(f"Error buscando videos: {str(e)}")
|
87 |
|
88 |
return video_urls
|
89 |
|
90 |
async def generate_tts(text, output_path, voice="es-ES-ElviraNeural"):
|
91 |
+
"""Genera audio TTS con manejo de errores"""
|
92 |
try:
|
93 |
communicate = edge_tts.Communicate(text, voice)
|
94 |
await communicate.save(output_path)
|
95 |
+
logger.info("Audio TTS generado exitosamente")
|
96 |
return True
|
97 |
except Exception as e:
|
98 |
+
logger.error(f"Error en TTS: {str(e)}")
|
99 |
return False
|
100 |
|
101 |
def download_video(url, temp_dir):
|
102 |
+
"""Descarga videos con manejo robusto de errores"""
|
103 |
try:
|
104 |
+
logger.info(f"Descargando video: {url}")
|
105 |
+
response = requests.get(url, stream=True, timeout=40)
|
106 |
response.raise_for_status()
|
107 |
|
108 |
+
filename = f"video_{os.getpid()}_{datetime.now().strftime('%H%M%S%f')}.mp4"
|
109 |
filepath = os.path.join(temp_dir, filename)
|
110 |
|
111 |
with open(filepath, 'wb') as f:
|
112 |
for chunk in response.iter_content(chunk_size=8192):
|
113 |
f.write(chunk)
|
114 |
+
|
115 |
+
logger.info(f"Video descargado: {filepath}")
|
116 |
return filepath
|
117 |
except Exception as e:
|
118 |
+
logger.error(f"Error descargando video: {str(e)}")
|
119 |
return None
|
120 |
|
121 |
def create_video(audio_path, video_paths, output_path):
|
122 |
+
"""Crea el video final con FFmpeg - VERSIÓN CORREGIDA"""
|
123 |
try:
|
124 |
+
# 1. Crear archivo de lista para concatenación
|
125 |
+
list_file_path = os.path.join(os.path.dirname(video_paths[0]), "input.txt")
|
126 |
+
with open(list_file_path, "w") as f:
|
127 |
for path in video_paths:
|
128 |
f.write(f"file '{os.path.basename(path)}'\n")
|
129 |
|
130 |
+
# 2. Preparar comando FFmpeg
|
|
|
|
|
|
|
131 |
cmd = [
|
132 |
"ffmpeg", "-y",
|
133 |
"-f", "concat",
|
134 |
"-safe", "0",
|
135 |
+
"-i", list_file_path,
|
136 |
"-i", audio_path,
|
137 |
+
"-c:v", "libx264", # Codificar video en lugar de copiar
|
138 |
+
"-preset", "fast",
|
139 |
+
"-crf", "23",
|
140 |
"-c:a", "aac",
|
141 |
+
"-b:a", "192k",
|
142 |
"-shortest",
|
143 |
+
"-movflags", "+faststart",
|
144 |
output_path
|
145 |
]
|
146 |
|
147 |
+
# 3. Ejecutar FFmpeg con logging detallado
|
148 |
+
logger.info("Ejecutando FFmpeg: " + " ".join(cmd))
|
149 |
+
result = subprocess.run(
|
150 |
+
cmd,
|
151 |
+
cwd=os.path.dirname(video_paths[0]),
|
152 |
+
stdout=subprocess.PIPE,
|
153 |
+
stderr=subprocess.PIPE,
|
154 |
+
text=True
|
155 |
+
)
|
156 |
+
|
157 |
+
if result.returncode != 0:
|
158 |
+
logger.error(f"Error FFmpeg (code {result.returncode}): {result.stderr}")
|
159 |
+
return False
|
160 |
+
|
161 |
+
logger.info("Video creado exitosamente")
|
162 |
return True
|
163 |
except Exception as e:
|
164 |
+
logger.error(f"Error creando video: {str(e)}")
|
165 |
return False
|
166 |
finally:
|
167 |
+
try:
|
168 |
+
if os.path.exists(list_file_path):
|
169 |
+
os.remove(list_file_path)
|
170 |
+
except:
|
171 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
|
173 |
async def generate_video(text, music_file=None):
|
174 |
+
"""Función principal con manejo mejorado de errores"""
|
175 |
temp_dir = tempfile.mkdtemp()
|
176 |
+
logger.info(f"Directorio temporal creado: {temp_dir}")
|
177 |
|
178 |
try:
|
179 |
# 1. Generar audio TTS
|
180 |
tts_path = os.path.join(temp_dir, "audio.mp3")
|
181 |
if not await generate_tts(text, tts_path):
|
182 |
+
return None, "❌ Error generando voz"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
183 |
|
184 |
+
# 2. Extraer palabras clave
|
185 |
keywords = extract_keywords(text)
|
|
|
|
|
186 |
if not keywords:
|
187 |
+
return None, "❌ No se pudieron extraer palabras clave del texto"
|
188 |
+
logger.info(f"Palabras clave identificadas: {keywords}")
|
189 |
|
190 |
+
# 3. Buscar y descargar videos
|
191 |
video_urls = search_pexels_videos(keywords)
|
192 |
if not video_urls:
|
193 |
+
return None, "❌ No se encontraron videos para las palabras clave"
|
194 |
|
195 |
video_paths = []
|
196 |
for url in video_urls:
|
197 |
path = download_video(url, temp_dir)
|
198 |
if path:
|
199 |
video_paths.append(path)
|
|
|
200 |
|
201 |
if not video_paths:
|
202 |
+
return None, "❌ Error descargando videos"
|
203 |
|
204 |
+
# 4. Crear video final
|
205 |
output_path = os.path.join(temp_dir, "final_video.mp4")
|
206 |
+
if not create_video(tts_path, video_paths, output_path):
|
207 |
+
return None, "❌ Error en la creación del video"
|
208 |
+
|
209 |
+
return output_path, "✅ Video creado exitosamente"
|
210 |
|
211 |
except Exception as e:
|
212 |
logger.exception("Error inesperado")
|
213 |
+
return None, f"❌ Error crítico: {str(e)}"
|
214 |
finally:
|
215 |
+
# Espacios maneja la limpieza automática
|
216 |
pass
|
217 |
|
218 |
+
# --- Interfaz de Gradio mejorada ---
|
219 |
+
with gr.Blocks(title="Generador Automático de Videos", theme=gr.themes.Soft(), css=".gradio-container {max-width: 800px}") as demo:
|
220 |
+
gr.Markdown("""
|
221 |
+
# 🎬 Generador Automático de Videos con IA
|
222 |
+
Transforma texto en videos usando contenido de Pexels y voz sintetizada
|
223 |
+
""")
|
224 |
|
225 |
with gr.Row():
|
226 |
+
with gr.Column(scale=2):
|
227 |
text_input = gr.Textbox(
|
228 |
label="Texto para el video",
|
229 |
+
placeholder="Ej: Un hermoso paisaje montañoso con ríos cristalinos...",
|
230 |
+
lines=5,
|
231 |
+
max_lines=10
|
232 |
)
|
233 |
+
generate_btn = gr.Button("✨ Generar Video", variant="primary")
|
234 |
+
|
235 |
+
with gr.Accordion("Configuración avanzada", open=False):
|
236 |
+
voice_select = gr.Dropdown(
|
237 |
+
["es-ES-ElviraNeural", "es-MX-DaliaNeural", "es-US-AlonsoNeural"],
|
238 |
+
label="Voz",
|
239 |
+
value="es-ES-ElviraNeural"
|
240 |
+
)
|
241 |
|
242 |
+
with gr.Column(scale=3):
|
243 |
+
video_output = gr.Video(
|
244 |
+
label="Video Generado",
|
245 |
+
interactive=False,
|
246 |
+
height=400
|
247 |
+
)
|
248 |
+
status_output = gr.Textbox(
|
249 |
+
label="Estado",
|
250 |
+
interactive=False,
|
251 |
+
show_label=False,
|
252 |
+
container=False
|
253 |
+
)
|
254 |
|
255 |
generate_btn.click(
|
256 |
+
fn=lambda: (None, "⏳ Procesando... Esto puede tomar 1-2 minutos"),
|
257 |
outputs=[video_output, status_output],
|
258 |
queue=False
|
259 |
).then(
|
260 |
fn=generate_video,
|
261 |
+
inputs=[text_input],
|
262 |
outputs=[video_output, status_output]
|
263 |
)
|
264 |
|
265 |
+
gr.Markdown("### Instrucciones:")
|
266 |
gr.Markdown("""
|
267 |
+
1. Describe el video que deseas crear (mínimo 20 palabras)
|
268 |
+
2. Haz clic en "Generar Video"
|
269 |
+
3. El sistema buscará videos relevantes en Pexels
|
270 |
+
4. Creará un video con narración automática
|
|
|
271 |
""")
|
272 |
+
|
273 |
+
gr.Markdown("### Ejemplos:")
|
274 |
+
examples = gr.Examples(
|
275 |
+
examples=[
|
276 |
+
["Un atardecer en la playa con palmeras y olas suaves"],
|
277 |
+
["Un bosque otoñal con hojas de colores y senderos naturales"],
|
278 |
+
["La ciudad de noche con rascacielos iluminados y tráfico"]
|
279 |
+
],
|
280 |
+
inputs=[text_input],
|
281 |
+
label="Ejemplos para probar"
|
282 |
+
)
|
283 |
|
284 |
# Para Hugging Face Spaces
|
285 |
if __name__ == "__main__":
|
286 |
+
demo.launch(
|
287 |
+
server_name="0.0.0.0",
|
288 |
+
server_port=7860,
|
289 |
+
share=False,
|
290 |
+
show_error=True
|
291 |
+
)
|