Spaces:
Sleeping
Sleeping
| import os | |
| import asyncio | |
| import logging | |
| import tempfile | |
| import requests | |
| from datetime import datetime | |
| import edge_tts | |
| import gradio as gr | |
| import torch | |
| from transformers import GPT2Tokenizer, GPT2LMHeadModel | |
| from keybert import KeyBERT | |
| from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip | |
| import re | |
| import math | |
| from pydub import AudioSegment | |
| from collections import Counter | |
| import shutil | |
| import json | |
| # Configuración de logging MEJORADA | |
| logging.basicConfig( | |
| level=logging.DEBUG, | |
| format='%(asctime)s - %(levelname)s - %(message)s', | |
| handlers=[ | |
| logging.StreamHandler(), | |
| logging.FileHandler('video_generator_full.log', encoding='utf-8') | |
| ] | |
| ) | |
| logger = logging.getLogger(__name__) | |
| logger.info("="*80) | |
| logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS") | |
| logger.info("="*80) | |
| # Clave API de Pexels (configuración segura) | |
| PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY") | |
| if not PEXELS_API_KEY: | |
| logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO") | |
| raise ValueError("API key de Pexels no configurada") | |
| else: | |
| logger.info("API key de Pexels configurada correctamente") | |
| # Inicialización de modelos CON LOGS DETALLADOS | |
| MODEL_NAME = "datificate/gpt2-small-spanish" | |
| logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}") | |
| try: | |
| tokenizer = GPTizer.from_pretrained(MODEL_NAME) | |
| model = GPT2LMHeadModel.from_pretrained(MODEL_NAME).eval() | |
| if tokenizer.pad_token is None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| logger.info(f"Modelo GPT-2 cargado | Vocabulario: {len(tokenizer)} tokens") | |
| except Exception as e: | |
| logger.error(f"FALLA CRÍTICA al cargar GPT-2: {str(e)}", exc_info=True) | |
| tokenizer = model = None | |
| logger.info("Cargando modelo KeyBERT...") | |
| try: | |
| kw_model = KeyBERT('distilbert-base-multilingual-cased') | |
| logger.info("KeyBERT inicializado correctamente") | |
| except Exception as e: | |
| logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True) | |
| kw_model = None | |
| # [FUNCIÓN BUSCAR_VIDEOS_PEXELS ORIGINAL CON LOGS AÑADIDOS] | |
| def buscar_videos_pexels(query, api_key, per_page=5): | |
| logger.debug(f"Buscando en Pexels: '{query}' | Resultados: {per_page}") | |
| headers = {"Authorization": api_key} | |
| try: | |
| params = { | |
| "query": query, | |
| "per_page": per_page, | |
| "orientation": "landscape", | |
| "size": "medium" | |
| } | |
| logger.debug(f"Params: {params}") | |
| response = requests.get( | |
| "https://api.pexels.com/videos/search", | |
| headers=headers, | |
| params=params, | |
| timeout=20 | |
| ) | |
| response.raise_for_status() | |
| try: | |
| data = response.json() | |
| logger.info(f"Pexels: {len(data.get('videos', []))} videos encontrados") | |
| return data.get('videos', []) | |
| except json.JSONDecodeError: | |
| logger.error(f"Pexels: JSON inválido | Status: {response.status_code} | Respuesta: {response.text[:200]}...") | |
| return [] | |
| except requests.exceptions.RequestException as e: | |
| logger.error(f"Error de conexión Pexels: {str(e)}") | |
| except Exception as e: | |
| logger.error(f"Error inesperado Pexels: {str(e)}", exc_info=True) | |
| return [] | |
| # [FUNCIÓN GENERATE_SCRIPT ORIGINAL CON LOGS] | |
| def generate_script(prompt, max_length=150): | |
| logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}") | |
| if not tokenizer or not model: | |
| logger.warning("Modelos no disponibles - Usando prompt original") | |
| return prompt | |
| try: | |
| enhanced_prompt = f"Escribe un guion corto y coherente sobre: {prompt}" | |
| inputs = tokenizer(enhanced_prompt, return_tensors="pt", truncation=True, max_length=512) | |
| logger.debug("Generando texto con GPT-2...") | |
| outputs = model.generate( | |
| **inputs, | |
| max_length=max_length, | |
| do_sample=True, | |
| top_p=0.9, | |
| top_k=40, | |
| temperature=0.7, | |
| repetition_penalty=1.5, | |
| pad_token_id=tokenizer.pad_token_id, | |
| eos_token_id=tokenizer.eos_token_id | |
| ) | |
| text = tokenizer.decode(outputs[0], skip_special_tokens=True) | |
| text = re.sub(r'<[^>]+>', '', text) | |
| sentences = text.split('.') | |
| if sentences: | |
| final_text = sentences[0] + '.' | |
| logger.info(f"Guion generado: '{final_text[:100]}...'") | |
| return final_text | |
| return text | |
| except Exception as e: | |
| logger.error(f"Error generando guion: {str(e)}", exc_info=True) | |
| return prompt | |
| # [FUNCIÓN TEXT_TO_SPEECH ORIGINAL CON LOGS] | |
| async def text_to_speech(text, output_path, voice="es-ES-ElviraNeural"): | |
| logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice}") | |
| try: | |
| communicate = edge_tts.Communicate(text, voice) | |
| await communicate.save(output_path) | |
| logger.info(f"Audio guardado en: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes") | |
| return True | |
| except Exception as e: | |
| logger.error(f"Error en TTS: {str(e)}", exc_info=True) | |
| return False | |
| # [FUNCIÓN DOWNLOAD_VIDEO_FILE ORIGINAL CON LOGS] | |
| def download_video_file(url, temp_dir): | |
| if not url: | |
| logger.warning("URL de video no proporcionada") | |
| return None | |
| try: | |
| logger.info(f"Descargando video desde: {url[:50]}...") | |
| file_name = f"video_{datetime.now().strftime('%H%M%S%f')}.mp4" | |
| output_path = os.path.join(temp_dir, file_name) | |
| with requests.get(url, stream=True, timeout=30) as r: | |
| r.raise_for_status() | |
| with open(output_path, 'wb') as f: | |
| for chunk in r.iter_content(chunk_size=8192): | |
| f.write(chunk) | |
| logger.info(f"Video descargado: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes") | |
| return output_path | |
| except Exception as e: | |
| logger.error(f"Error descargando video: {str(e)}", exc_info=True) | |
| return None | |
| # [FUNCIÓN LOOP_AUDIO_TO_LENGTH ORIGINAL CON LOGS] | |
| def loop_audio_to_length(audio_clip, target_duration): | |
| logger.debug(f"Ajustando audio | Duración actual: {audio_clip.duration:.2f}s | Objetivo: {target_duration:.2f}s") | |
| if audio_clip.duration >= target_duration: | |
| return audio_clip.subclip(0, target_duration) | |
| loops = math.ceil(target_duration / audio_clip.duration) | |
| logger.debug(f"Creando {loops} loops de audio") | |
| audios = [audio_clip] * loops | |
| return concatenate_videoclips(audios).subclip(0, target_duration) | |
| # [FUNCIÓN EXTRACT_VISUAL_KEYWORDS_FROM_SCRIPT ORIGINAL CON LOGS] | |
| def extract_visual_keywords_from_script(script_text): | |
| logger.info("Extrayendo palabras clave del guion") | |
| clean_text = re.sub(r'[^\w\sáéíóúñ]', '', script_text.lower()) | |
| if kw_model: | |
| try: | |
| keywords = kw_model.extract_keywords( | |
| clean_text, | |
| keyphrase_ngram_range=(1, 1), | |
| stop_words='spanish', | |
| top_n=3 | |
| ) | |
| if keywords: | |
| logger.debug(f"KeyBERT keywords: {keywords}") | |
| return [kw[0].replace(" ", "+") for kw in keywords] | |
| except Exception as e: | |
| logger.warning(f"KeyBERT falló: {str(e)}") | |
| words = clean_text.split() | |
| stop_words = {"el", "la", "los", "las", "de", "en", "y", "a", "que", "es", "un", "una", "con"} | |
| keywords = [word for word in words if len(word) > 3 and word not in stop_words] | |
| if not keywords: | |
| logger.warning("Usando palabras clave predeterminadas") | |
| return ["naturaleza", "ciudad", "paisaje"] | |
| word_counts = Counter(keywords) | |
| top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(3)] | |
| logger.info(f"Palabras clave finales: {top_keywords}") | |
| return top_keywords | |
| # [FUNCIÓN CREAR_VIDEO ORIGINAL CON LOGS] | |
| def crear_video(prompt_type, input_text, musica_file=None): | |
| logger.info("="*80) | |
| logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}") | |
| logger.debug(f"Input: '{input_text[:100]}...'") | |
| # 1. Generar o usar guion | |
| start_time = datetime.now() | |
| if prompt_type == "Generar Guion con IA": | |
| logger.info("Generando guion con IA...") | |
| guion = generate_script(input_text) | |
| else: | |
| logger.info("Usando guion proporcionado") | |
| guion = input_text | |
| logger.info(f"Guion final ({len(guion)} caracteres): '{guion[:100]}...'") | |
| if not guion.strip(): | |
| logger.error("El guion está vacío") | |
| raise ValueError("El guion está vacío") | |
| # Directorio temporal | |
| temp_dir = tempfile.mkdtemp() | |
| logger.info(f"Directorio temporal creado: {temp_dir}") | |
| temp_files = [] | |
| try: | |
| # 2. Generar audio de voz | |
| logger.info("Generando audio de voz...") | |
| voz_path = os.path.join(temp_dir, "voz.mp3") | |
| if not asyncio.run(text_to_speech(guion, voz_path)): | |
| logger.error("Fallo en generación de voz") | |
| raise ValueError("Error generando voz") | |
| temp_files.append(voz_path) | |
| audio_tts = AudioFileClip(voz_path) | |
| audio_duration = audio_tts.duration | |
| logger.info(f"Duración audio voz: {audio_duration:.2f} segundos") | |
| # 3. Extraer palabras clave | |
| logger.info("Extrayendo palabras clave...") | |
| try: | |
| keywords = extract_visual_keywords_from_script(guion) | |
| logger.info(f"Palabras clave identificadas: {keywords}") | |
| except Exception as e: | |
| logger.error(f"Error extrayendo keywords: {str(e)}") | |
| keywords = ["naturaleza", "paisaje"] | |
| # 4. Buscar y descargar videos | |
| logger.info("Buscando videos en Pexels...") | |
| videos_data = [] | |
| for keyword in keywords: | |
| try: | |
| videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=3) | |
| if videos: | |
| videos_data.extend(videos) | |
| logger.info(f"Encontrados {len(videos)} videos para '{keyword}'") | |
| except Exception as e: | |
| logger.warning(f"Error buscando videos para '{keyword}': {str(e)}") | |
| if not videos_data: | |
| logger.warning("No se encontraron videos - Usando palabras clave genéricas") | |
| for keyword in ["naturaleza", "ciudad", "paisaje"]: | |
| videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=3) | |
| if videos: | |
| videos_data.extend(videos) | |
| if not videos_data: | |
| logger.error("No se encontraron videos para ninguna palabra clave") | |
| raise ValueError("No se encontraron videos en Pexels para ninguna palabra clave") | |
| video_paths = [] | |
| for video in videos_data: | |
| if 'video_files' not in video or not video['video_files']: | |
| continue | |
| try: | |
| best_quality = max( | |
| video['video_files'], | |
| key=lambda x: x.get('width', 0) * x.get('height', 0) | |
| ) | |
| if 'link' in best_quality: | |
| path = download_video_file(best_quality['link'], temp_dir) | |
| if path: | |
| video_paths.append(path) | |
| temp_files.append(path) | |
| logger.info(f"Video descargado: {best_quality['link']}") | |
| except Exception as e: | |
| logger.warning(f"Error procesando video: {str(e)}") | |
| if not video_paths: | |
| logger.error("No se pudo descargar ningún video") | |
| raise ValueError("No se pudo descargar ningún video") | |
| # 5. Procesar videos | |
| logger.info("Procesando videos descargados...") | |
| clips = [] | |
| current_duration = 0 | |
| for path in video_paths: | |
| if current_duration >= audio_duration: | |
| break | |
| try: | |
| clip = VideoFileClip(path) | |
| usable_duration = min(clip.duration, 10) | |
| if usable_duration > 1: | |
| clips.append(clip.subclip(0, usable_duration)) | |
| current_duration += usable_duration | |
| logger.debug(f"Clip añadido: {usable_duration:.1f}s (total: {current_duration:.1f}/{audio_duration:.1f}s)") | |
| except Exception as e: | |
| logger.warning(f"Error procesando video {path}: {str(e)}") | |
| if not clips: | |
| logger.error("No hay clips válidos para crear el video") | |
| raise ValueError("No hay clips válidos para crear el video") | |
| video_base = concatenate_videoclips(clips, method="compose") | |
| logger.info(f"Duración base del video: {video_base.duration:.2f}s") | |
| if video_base.duration < audio_duration: | |
| num_repeats = math.ceil(audio_duration / video_base.duration) | |
| logger.info(f"Repitiendo video {num_repeats} veces para ajustar duración") | |
| video_base = concatenate_videoclips([video_base] * num_repeats).subclip(0, audio_duration) | |
| # 6. Manejar música de fondo | |
| logger.info("Procesando audio...") | |
| final_audio = audio_tts | |
| if musica_file: | |
| try: | |
| music_path = os.path.join(temp_dir, "musica.mp3") | |
| shutil.copyfile(musica_file, music_path) | |
| temp_files.append(music_path) | |
| logger.info(f"Música copiada a: {music_path}") | |
| musica_audio = AudioFileClip(music_path) | |
| logger.debug(f"Duración música original: {musica_audio.duration:.2f}s") | |
| if musica_audio.duration < audio_duration: | |
| musica_audio = loop_audio_to_length(musica_audio, audio_duration) | |
| logger.debug(f"Música looped: {musica_audio.duration:.2f}s") | |
| final_audio = CompositeAudioClip([ | |
| musica_audio.volumex(0.3), | |
| audio_tts.volumex(1.0) | |
| ]) | |
| logger.info("Mezcla de audio completada") | |
| except Exception as e: | |
| logger.warning(f"Error procesando música: {str(e)}") | |
| # 7. Crear video final | |
| logger.info("Renderizando video final...") | |
| video_final = video_base.set_audio(final_audio) | |
| output_path = os.path.join(temp_dir, "final_video.mp4") | |
| video_final.write_videofile( | |
| output_path, | |
| fps=24, | |
| threads=4, | |
| codec="libx264", | |
| audio_codec="aac", | |
| preset="medium", | |
| logger=None | |
| ) | |
| total_time = (datetime.now() - start_time).total_seconds() | |
| logger.info(f"VIDEO FINALIZADO: {output_path} | Tiempo total: {total_time:.2f}s") | |
| return output_path | |
| except Exception as e: | |
| logger.error(f"ERROR EN CREAR_VIDEO: {str(e)}", exc_info=True) | |
| raise | |
| finally: | |
| logger.info("Limpiando archivos temporales...") | |
| for path in temp_files: | |
| try: | |
| if os.path.isfile(path): | |
| os.remove(path) | |
| except Exception as e: | |
| logger.warning(f"No se pudo eliminar {path}: {str(e)}") | |
| try: | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| except Exception as e: | |
| logger.warning(f"No se pudo eliminar {temp_dir}: {str(e)}") | |
| # [FUNCIÓN RUN_APP ORIGINAL CON LOGS] | |
| def run_app(prompt_type, prompt_ia, prompt_manual, musica_file): | |
| logger.info("="*80) | |
| logger.info("SOLICITUD RECIBIDA EN INTERFAZ") | |
| input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual | |
| if not input_text.strip(): | |
| logger.warning("Texto de entrada vacío") | |
| return None, "Por favor ingresa texto" | |
| try: | |
| logger.info("Iniciando creación de video...") | |
| video_path = crear_video(prompt_type, input_text, musica_file) | |
| logger.info("Video creado exitosamente") | |
| return video_path, "✅ Video generado exitosamente" | |
| except ValueError as ve: | |
| logger.warning(f"Error de validación: {str(ve)}") | |
| return None, f"⚠️ {ve}" | |
| except Exception as e: | |
| logger.error(f"Error crítico: {str(e)}", exc_info=True) | |
| return None, f"❌ Error: {str(e)}" | |
| # [INTERFAZ DE GRADIO ORIGINAL COMPLETA] | |
| with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css=""" | |
| .gradio-container {max-width: 800px; margin: auto;} | |
| h1 {text-align: center;} | |
| """) as app: | |
| gr.Markdown("# 🎬 Generador Automático de Videos con IA") | |
| with gr.Row(): | |
| with gr.Column(): | |
| prompt_type = gr.Radio( | |
| ["Generar Guion con IA", "Usar Mi Guion"], | |
| label="Método de Entrada", | |
| value="Generar Guion con IA" | |
| ) | |
| with gr.Column(visible=True) as ia_guion_column: | |
| prompt_ia = gr.Textbox( | |
| label="Tema para IA", | |
| lines=2, | |
| placeholder="Ej: Un paisaje natural con montañas y ríos...", | |
| max_lines=4 | |
| ) | |
| with gr.Column(visible=False) as manual_guion_column: | |
| prompt_manual = gr.Textbox( | |
| label="Tu Guion Completo", | |
| lines=5, | |
| placeholder="Ej: En este video exploraremos los misterios del océano...", | |
| max_lines=10 | |
| ) | |
| musica_input = gr.Audio( | |
| label="Música de fondo (opcional)", | |
| type="filepath", | |
| interactive=True | |
| ) | |
| generate_btn = gr.Button("✨ Generar Video", variant="primary") | |
| with gr.Column(): | |
| video_output = gr.Video( | |
| label="Video Generado", | |
| interactive=False, | |
| height=400 | |
| ) | |
| status_output = gr.Textbox( | |
| label="Estado", | |
| interactive=False, | |
| show_label=False, | |
| placeholder="Esperando acción..." | |
| ) | |
| prompt_type.change( | |
| lambda x: (gr.update(visible=x == "Generar Guion con IA"), | |
| gr.update(visible=x == "Usar Mi Guion")), | |
| inputs=prompt_type, | |
| outputs=[ia_guion_column, manual_guion_column] | |
| ) | |
| generate_btn.click( | |
| lambda: (None, "⏳ Procesando... (esto puede tomar 2-5 minutos)"), | |
| outputs=[video_output, status_output], | |
| queue=False | |
| ).then( | |
| run_app, | |
| inputs=[prompt_type, prompt_ia, prompt_manual, musica_input], | |
| outputs=[video_output, status_output] | |
| ) | |
| gr.Markdown("### Instrucciones:") | |
| gr.Markdown(""" | |
| 1. **Selecciona el tipo de entrada**: | |
| - "Generar Guion con IA": Describe un tema | |
| - "Usar Mi Guion": Escribe tu guion completo | |
| 2. **Sube música** (opcional): Selecciona un archivo de audio | |
| 3. **Haz clic en Generar Video" | |
| 4. Espera a que se procese el video (puede tomar varios minutos) | |
| """) | |
| if __name__ == "__main__": | |
| logger.info("Iniciando aplicación Gradio...") | |
| try: | |
| app.launch(server_name="0.0.0.0", server_port=7860) | |
| except Exception as e: | |
| logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True) | |
| raise |