Spaces:
Running
Running
import os | |
import asyncio | |
import logging | |
import tempfile | |
import requests | |
import re | |
import math | |
import edge_tts | |
import gradio as gr | |
from pydub import AudioSegment | |
import subprocess | |
# Configuración básica de logging | |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
logger = logging.getLogger(__name__) | |
# Clave API de Pexels (configurar en Secrets de Hugging Face) | |
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY", "YOUR_API_KEY") | |
# --- Funciones optimizadas y corregidas --- | |
def extract_keywords(text, max_keywords=3): | |
"""Extrae palabras clave usando un método mejorado""" | |
# Limpieza de texto y tokenización | |
text = re.sub(r'[^\w\s]', '', text.lower()) | |
words = re.findall(r'\b\w+\b', text) | |
# Palabras comunes a excluir (lista ampliada) | |
stop_words = { | |
"el", "la", "los", "las", "de", "en", "y", "a", "que", "es", "por", | |
"un", "una", "con", "se", "del", "al", "lo", "como", "para", "su", "sus" | |
} | |
# Frecuencia de palabras y filtrado | |
word_freq = {} | |
for word in words: | |
if len(word) > 3 and word not in stop_words: | |
word_freq[word] = word_freq.get(word, 0) + 1 | |
# Ordenar por frecuencia y longitud | |
sorted_words = sorted(word_freq.items(), key=lambda x: (x[1], len(x[0])), reverse=True) | |
return [word for word, _ in sorted_words[:max_keywords]] | |
def search_pexels_videos(keywords, per_query=2): | |
"""Busca videos en Pexels con manejo de errores mejorado""" | |
if not PEXELS_API_KEY or not keywords: | |
return [] | |
headers = {"Authorization": PEXELS_API_KEY} | |
video_urls = [] | |
for query in keywords: | |
try: | |
logger.info(f"Buscando videos para: '{query}'") | |
params = { | |
"query": query, | |
"per_page": per_query, | |
"orientation": "landscape", | |
"size": "medium" | |
} | |
response = requests.get( | |
"https://api.pexels.com/videos/search", | |
headers=headers, | |
params=params, | |
timeout=20 | |
) | |
if response.status_code == 200: | |
data = response.json() | |
videos = data.get("videos", []) | |
for video in videos: | |
video_files = video.get("video_files", []) | |
if video_files: | |
# Seleccionar el video con la mejor resolución disponible | |
best_quality = max( | |
video_files, | |
key=lambda x: x.get("width", 0) * x.get("height", 0) | |
) | |
video_urls.append(best_quality["link"]) | |
logger.info(f"Video encontrado: {best_quality['link']}") | |
else: | |
logger.warning(f"Respuesta Pexels: {response.status_code}") | |
except Exception as e: | |
logger.error(f"Error buscando videos: {str(e)}") | |
return video_urls | |
async def generate_tts(text, output_path, voice="es-ES-ElviraNeural"): | |
"""Genera audio TTS con manejo de errores""" | |
try: | |
communicate = edge_tts.Communicate(text, voice) | |
await communicate.save(output_path) | |
logger.info("Audio TTS generado exitosamente") | |
return True | |
except Exception as e: | |
logger.error(f"Error en TTS: {str(e)}") | |
return False | |
def download_video(url, temp_dir): | |
"""Descarga videos con manejo robusto de errores""" | |
try: | |
logger.info(f"Descargando video: {url}") | |
response = requests.get(url, stream=True, timeout=40) | |
response.raise_for_status() | |
filename = f"video_{os.getpid()}_{datetime.now().strftime('%H%M%S%f')}.mp4" | |
filepath = os.path.join(temp_dir, filename) | |
with open(filepath, 'wb') as f: | |
for chunk in response.iter_content(chunk_size=8192): | |
f.write(chunk) | |
logger.info(f"Video descargado: {filepath}") | |
return filepath | |
except Exception as e: | |
logger.error(f"Error descargando video: {str(e)}") | |
return None | |
def create_video(audio_path, video_paths, output_path): | |
"""Crea el video final con FFmpeg - VERSIÓN CORREGIDA""" | |
try: | |
# 1. Crear archivo de lista para concatenación | |
list_file_path = os.path.join(os.path.dirname(video_paths[0]), "input.txt") | |
with open(list_file_path, "w") as f: | |
for path in video_paths: | |
f.write(f"file '{os.path.basename(path)}'\n") | |
# 2. Preparar comando FFmpeg | |
cmd = [ | |
"ffmpeg", "-y", | |
"-f", "concat", | |
"-safe", "0", | |
"-i", list_file_path, | |
"-i", audio_path, | |
"-c:v", "libx264", # Codificar video en lugar de copiar | |
"-preset", "fast", | |
"-crf", "23", | |
"-c:a", "aac", | |
"-b:a", "192k", | |
"-shortest", | |
"-movflags", "+faststart", | |
output_path | |
] | |
# 3. Ejecutar FFmpeg con logging detallado | |
logger.info("Ejecutando FFmpeg: " + " ".join(cmd)) | |
result = subprocess.run( | |
cmd, | |
cwd=os.path.dirname(video_paths[0]), | |
stdout=subprocess.PIPE, | |
stderr=subprocess.PIPE, | |
text=True | |
) | |
if result.returncode != 0: | |
logger.error(f"Error FFmpeg (code {result.returncode}): {result.stderr}") | |
return False | |
logger.info("Video creado exitosamente") | |
return True | |
except Exception as e: | |
logger.error(f"Error creando video: {str(e)}") | |
return False | |
finally: | |
try: | |
if os.path.exists(list_file_path): | |
os.remove(list_file_path) | |
except: | |
pass | |
async def generate_video(text, music_file=None): | |
"""Función principal con manejo mejorado de errores""" | |
temp_dir = tempfile.mkdtemp() | |
logger.info(f"Directorio temporal creado: {temp_dir}") | |
try: | |
# 1. Generar audio TTS | |
tts_path = os.path.join(temp_dir, "audio.mp3") | |
if not await generate_tts(text, tts_path): | |
return None, "❌ Error generando voz" | |
# 2. Extraer palabras clave | |
keywords = extract_keywords(text) | |
if not keywords: | |
return None, "❌ No se pudieron extraer palabras clave del texto" | |
logger.info(f"Palabras clave identificadas: {keywords}") | |
# 3. Buscar y descargar videos | |
video_urls = search_pexels_videos(keywords) | |
if not video_urls: | |
return None, "❌ No se encontraron videos para las palabras clave" | |
video_paths = [] | |
for url in video_urls: | |
path = download_video(url, temp_dir) | |
if path: | |
video_paths.append(path) | |
if not video_paths: | |
return None, "❌ Error descargando videos" | |
# 4. Crear video final | |
output_path = os.path.join(temp_dir, "final_video.mp4") | |
if not create_video(tts_path, video_paths, output_path): | |
return None, "❌ Error en la creación del video" | |
return output_path, "✅ Video creado exitosamente" | |
except Exception as e: | |
logger.exception("Error inesperado") | |
return None, f"❌ Error crítico: {str(e)}" | |
finally: | |
# Espacios maneja la limpieza automática | |
pass | |
# --- Interfaz de Gradio mejorada --- | |
with gr.Blocks(title="Generador Automático de Videos", theme=gr.themes.Soft(), css=".gradio-container {max-width: 800px}") as demo: | |
gr.Markdown(""" | |
# 🎬 Generador Automático de Videos con IA | |
Transforma texto en videos usando contenido de Pexels y voz sintetizada | |
""") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
text_input = gr.Textbox( | |
label="Texto para el video", | |
placeholder="Ej: Un hermoso paisaje montañoso con ríos cristalinos...", | |
lines=5, | |
max_lines=10 | |
) | |
generate_btn = gr.Button("✨ Generar Video", variant="primary") | |
with gr.Accordion("Configuración avanzada", open=False): | |
voice_select = gr.Dropdown( | |
["es-ES-ElviraNeural", "es-MX-DaliaNeural", "es-US-AlonsoNeural"], | |
label="Voz", | |
value="es-ES-ElviraNeural" | |
) | |
with gr.Column(scale=3): | |
video_output = gr.Video( | |
label="Video Generado", | |
interactive=False, | |
height=400 | |
) | |
status_output = gr.Textbox( | |
label="Estado", | |
interactive=False, | |
show_label=False, | |
container=False | |
) | |
generate_btn.click( | |
fn=lambda: (None, "⏳ Procesando... Esto puede tomar 1-2 minutos"), | |
outputs=[video_output, status_output], | |
queue=False | |
).then( | |
fn=generate_video, | |
inputs=[text_input], | |
outputs=[video_output, status_output] | |
) | |
gr.Markdown("### Instrucciones:") | |
gr.Markdown(""" | |
1. Describe el video que deseas crear (mínimo 20 palabras) | |
2. Haz clic en "Generar Video" | |
3. El sistema buscará videos relevantes en Pexels | |
4. Creará un video con narración automática | |
""") | |
gr.Markdown("### Ejemplos:") | |
examples = gr.Examples( | |
examples=[ | |
["Un atardecer en la playa con palmeras y olas suaves"], | |
["Un bosque otoñal con hojas de colores y senderos naturales"], | |
["La ciudad de noche con rascacielos iluminados y tráfico"] | |
], | |
inputs=[text_input], | |
label="Ejemplos para probar" | |
) | |
# Para Hugging Face Spaces | |
if __name__ == "__main__": | |
demo.launch( | |
server_name="0.0.0.0", | |
server_port=7860, | |
share=False, | |
show_error=True | |
) |