Spaces:
Sleeping
Sleeping
| import os | |
| import logging | |
| import tempfile | |
| import requests | |
| from datetime import datetime | |
| import gradio as gr | |
| import torch | |
| from transformers import GPT2Tokenizer, GPT2LMHeadModel | |
| from keybert import KeyBERT | |
| from TTS.api import TTS | |
| from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip | |
| import re | |
| import math | |
| import shutil | |
| import json | |
| from collections import Counter | |
| import threading | |
| import time | |
| from PIL import Image | |
| # *** CAMBIO 1 (CORRECCIÓN): Parche para la compatibilidad de Pillow >= 10.0 *** | |
| # Las versiones nuevas de Pillow eliminaron 'ANTIALIAS'. MoviePy aún lo usa. | |
| # Este código restaura la compatibilidad haciendo que ANTIALIAS apunte a LANCZOS. | |
| if not hasattr(Image, 'ANTIALIAS'): | |
| Image.ANTIALIAS = Image.LANCZOS | |
| # Variable global para TTS | |
| tts_model = None | |
| # Configuración de logging | |
| 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 | |
| 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") | |
| # Inicialización de modelos | |
| MODEL_NAME = "datificate/gpt2-small-spanish" | |
| logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}") | |
| tokenizer = None | |
| model = None | |
| try: | |
| tokenizer = GPT2Tokenizer.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...") | |
| kw_model = None | |
| 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 | |
| # *** CAMBIO 3 (AÑADIDO): Función para eliminar directorios temporalmente *** | |
| def schedule_deletion(directory_path, delay_seconds): | |
| """Programa la eliminación de un directorio después de un cierto tiempo.""" | |
| logger.info(f"PROGRAMADA eliminación del directorio '{directory_path}' en {delay_seconds / 3600:.1f} horas.") | |
| time.sleep(delay_seconds) | |
| try: | |
| if os.path.isdir(directory_path): | |
| shutil.rmtree(directory_path) | |
| logger.info(f"Directorio temporal '{directory_path}' eliminado exitosamente.") | |
| else: | |
| logger.warning(f"No se pudo eliminar: '{directory_path}' no es un directorio válido o ya fue eliminado.") | |
| except Exception as e: | |
| logger.error(f"Error durante la eliminación programada de '{directory_path}': {str(e)}") | |
| def buscar_videos_pexels(query, api_key, per_page=5): | |
| if not api_key: | |
| logger.warning("No se puede buscar en Pexels: API Key no configurada.") | |
| return [] | |
| 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" | |
| } | |
| response = requests.get( | |
| "https://api.pexels.com/videos/search", | |
| headers=headers, | |
| params=params, | |
| timeout=20 | |
| ) | |
| response.raise_for_status() | |
| data = response.json() | |
| videos = data.get('videos', []) | |
| logger.info(f"Pexels: {len(videos)} videos encontrados para '{query}'") | |
| return videos | |
| except requests.exceptions.RequestException as e: | |
| logger.error(f"Error de conexión Pexels para '{query}': {str(e)}") | |
| except json.JSONDecodeError: | |
| logger.error(f"Pexels: JSON inválido recibido | Status: {response.status_code} | Respuesta: {response.text[:200]}...") | |
| except Exception as e: | |
| logger.error(f"Error inesperado Pexels para '{query}': {str(e)}", exc_info=True) | |
| return [] | |
| 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 GPT-2 no disponibles - Usando prompt original como guion.") | |
| return prompt.strip() | |
| instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:" | |
| ai_prompt = f"{instruction_phrase_start} {prompt}" | |
| try: | |
| inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512) | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| model.to(device) | |
| inputs = {k: v.to(device) for k, v in inputs.items()} | |
| outputs = model.generate( | |
| **inputs, | |
| max_length=max_length + inputs[list(inputs.keys())[0]].size(1), | |
| do_sample=True, | |
| top_p=0.9, | |
| top_k=40, | |
| temperature=0.7, | |
| repetition_penalty=1.2, | |
| pad_token_id=tokenizer.pad_token_id, | |
| eos_token_id=tokenizer.eos_token_id, | |
| no_repeat_ngram_size=3 | |
| ) | |
| text = tokenizer.decode(outputs[0], skip_special_tokens=True) | |
| cleaned_text = text.strip() | |
| try: | |
| instruction_end_idx = text.find(instruction_phrase_start) | |
| if instruction_end_idx != -1: | |
| cleaned_text = text[instruction_end_idx + len(instruction_phrase_start):].strip() | |
| logger.debug("Instrucción inicial encontrada y eliminada del guión generado.") | |
| else: | |
| instruction_start_idx = text.find(instruction_phrase_start) | |
| if instruction_start_idx != -1: | |
| prompt_in_output_idx = text.find(prompt, instruction_start_idx) | |
| if prompt_in_output_idx != -1: | |
| cleaned_text = text[prompt_in_output_idx + len(prompt):].strip() | |
| logger.debug("Instrucción base y prompt encontrados y eliminados del guión generado.") | |
| else: | |
| cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip() | |
| logger.debug("Instrucción base encontrada, eliminada del guión generado (sin prompt detectado).") | |
| except Exception as e: | |
| logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.") | |
| cleaned_text = re.sub(r'<[^>]+>', '', text).strip() | |
| if not cleaned_text or len(cleaned_text) < 10: | |
| logger.warning("El guión generado parece muy corto o vacío después de la limpieza. Usando el texto generado original (sin limpieza heurística).") | |
| cleaned_text = re.sub(r'<[^>]+>', '', text).strip() | |
| cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip() | |
| cleaned_text = cleaned_text.lstrip(':').strip() | |
| cleaned_text = cleaned_text.lstrip('.').strip() | |
| sentences = cleaned_text.split('.') | |
| if sentences and sentences[0].strip(): | |
| final_text = sentences[0].strip() + '.' | |
| if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7: | |
| final_text += " " + sentences[1].strip() + "." | |
| final_text = final_text.replace("..", ".") | |
| logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'") | |
| return final_text.strip() | |
| logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'") | |
| return cleaned_text.strip() | |
| except Exception as e: | |
| logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True) | |
| logger.warning("Usando prompt original como guion debido al error de generación.") | |
| return prompt.strip() | |
| def text_to_speech(text, output_path, voice=None): | |
| logger.info(f"Convirtiendo texto a voz con Coqui TTS | Caracteres: {len(text)} | Salida: {output_path}") | |
| if not text or not text.strip(): | |
| logger.warning("Texto vacío para TTS") | |
| return False | |
| try: | |
| tts = TTS(model_name="tts_models/es/css10/vits", progress_bar=False, gpu=False) | |
| text = text.replace("na hora", "A la hora") | |
| text = re.sub(r'[^\w\s.,!?áéíóúñÁÉÍÓÚÑ]', '', text) | |
| if len(text) > 500: | |
| logger.warning("Texto demasiado largo, truncando a 500 caracteres") | |
| text = text[:500] | |
| tts.tts_to_file(text=text, file_path=output_path) | |
| if os.path.exists(output_path) and os.path.getsize(output_path) > 1000: | |
| logger.info(f"Audio creado: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes") | |
| return True | |
| else: | |
| logger.error("Archivo de audio vacío o no creado") | |
| return False | |
| except Exception as e: | |
| logger.error(f"Error TTS: {str(e)}", exc_info=True) | |
| return False | |
| def download_video_file(url, temp_dir): | |
| if not url: | |
| logger.warning("URL de video no proporcionada para descargar") | |
| return None | |
| try: | |
| logger.info(f"Descargando video desde: {url[:80]}...") | |
| os.makedirs(temp_dir, exist_ok=True) | |
| file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4" | |
| output_path = os.path.join(temp_dir, file_name) | |
| with requests.get(url, stream=True, timeout=60) 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) | |
| if os.path.exists(output_path) and os.path.getsize(output_path) > 1000: | |
| logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes") | |
| return output_path | |
| else: | |
| logger.warning(f"Descarga parece incompleta o vacía para {url[:80]}...") | |
| if os.path.exists(output_path): | |
| os.remove(output_path) | |
| return None | |
| except requests.exceptions.RequestException as e: | |
| logger.error(f"Error de descarga para {url[:80]}... : {str(e)}") | |
| except Exception as e: | |
| logger.error(f"Error inesperado descargando {url[:80]}... : {str(e)}", exc_info=True) | |
| return None | |
| 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 is None or audio_clip.duration is None or audio_clip.duration <= 0: | |
| logger.warning("Input audio clip is invalid (None or zero duration), cannot loop.") | |
| return None | |
| if audio_clip.duration >= target_duration: | |
| logger.debug("Audio clip already longer or equal to target. Trimming.") | |
| return audio_clip.subclip(0, target_duration) | |
| loops = math.ceil(target_duration / audio_clip.duration) | |
| logger.debug(f"Creando {loops} loops de audio") | |
| try: | |
| looped_audio = concatenate_audioclips([audio_clip] * loops) | |
| return looped_audio.subclip(0, target_duration) | |
| except Exception as e: | |
| logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True) | |
| return None | |
| def extract_visual_keywords_from_script(script_text): | |
| logger.info("Extrayendo palabras clave del guion") | |
| if not script_text or not script_text.strip(): | |
| logger.warning("Guion vacío, no se pueden extraer palabras clave.") | |
| return ["naturaleza", "ciudad", "paisaje"] | |
| clean_text = re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', '', script_text) | |
| keywords_list = [] | |
| if kw_model: | |
| try: | |
| keywords1 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(1, 1), stop_words='spanish', top_n=5) | |
| keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3) | |
| all_keywords = sorted(keywords1 + keywords2, key=lambda item: item[1], reverse=True) | |
| seen_keywords = set() | |
| for keyword, score in all_keywords: | |
| formatted_keyword = keyword.lower().replace(" ", "+") | |
| if formatted_keyword and formatted_keyword not in seen_keywords: | |
| keywords_list.append(formatted_keyword) | |
| seen_keywords.add(formatted_keyword) | |
| if len(keywords_list) >= 5: break | |
| if keywords_list: | |
| logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}") | |
| return keywords_list | |
| except Exception as e: | |
| logger.warning(f"KeyBERT falló: {str(e)}. Intentando método simple.") | |
| stop_words = set(["el", "la", "los", "las", "de", "en", "y", "a", "que", "es", "un", "una", "con", "para", "del", "al", "por", "su", "sus"]) | |
| words = [word for word in clean_text.lower().split() if len(word) > 3 and word not in stop_words] | |
| if not words: return ["naturaleza", "ciudad", "paisaje"] | |
| top_keywords = [word.replace(" ", "+") for word, _ in Counter(words).most_common(5)] | |
| logger.info(f"Palabras clave finales: {top_keywords}") | |
| return top_keywords | |
| 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]}...'") | |
| start_time = datetime.now() | |
| temp_dir_intermediate = None | |
| TARGET_RESOLUTION = (1280, 720) # *** CAMBIO 2 (AÑADIDO): Resolución 720p *** | |
| audio_tts_original = None | |
| musica_audio_original = None | |
| audio_tts = None | |
| musica_audio = None | |
| video_base = None | |
| video_final = None | |
| source_clips = [] | |
| clips_to_concatenate = [] | |
| try: | |
| # 1. Generar o usar guion | |
| guion = generate_script(input_text) if prompt_type == "Generar Guion con IA" else input_text.strip() | |
| if not guion.strip(): raise ValueError("El guion está vacío.") | |
| guion = guion.replace("na hora", "A la hora") | |
| temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_") | |
| temp_intermediate_files = [] | |
| # 2. Generar audio de voz | |
| voz_path = os.path.join(temp_dir_intermediate, "voz.mp3") | |
| if not text_to_speech(guion, voz_path) or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 1000: | |
| raise ValueError("Error generando voz a partir del guion (fallo de TTS).") | |
| temp_intermediate_files.append(voz_path) | |
| audio_tts_original = AudioFileClip(voz_path) | |
| if audio_tts_original.duration is None or audio_tts_original.duration < 1.0: | |
| raise ValueError("Generated voice audio is too short (min 1 second required).") | |
| audio_tts = audio_tts_original | |
| audio_duration = audio_tts_original.duration | |
| # 3. Extraer palabras clave y buscar videos | |
| keywords = extract_visual_keywords_from_script(guion) | |
| videos_data = [] | |
| for keyword in keywords + ["nature", "city", "background", "abstract"]: | |
| if len(videos_data) >= 10: break | |
| videos_data.extend(buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=3)) | |
| if not videos_data: raise ValueError("No se encontraron videos adecuados en Pexels.") | |
| # 4. Descargar videos | |
| video_paths = [] | |
| for video in videos_data: | |
| best_quality = next((vf for vf in sorted(video.get('video_files', []), key=lambda x: x.get('width', 0), reverse=True) if 'link' in vf), None) | |
| if best_quality: | |
| path = download_video_file(best_quality['link'], temp_dir_intermediate) | |
| if path: | |
| video_paths.append(path) | |
| temp_intermediate_files.append(path) | |
| if not video_paths: raise ValueError("No se pudo descargar ningún video utilizable de Pexels.") | |
| # 5. Procesar y concatenar clips de video | |
| current_duration = 0 | |
| for i, path in enumerate(video_paths): | |
| if current_duration >= audio_duration: break | |
| clip = None | |
| try: | |
| clip = VideoFileClip(path) | |
| source_clips.append(clip) | |
| if clip.duration is None or clip.duration <= 0.5: continue | |
| segment_duration = min(clip.duration, audio_duration - current_duration, 10.0) | |
| if segment_duration >= 0.5: | |
| sub_raw = clip.subclip(0, segment_duration) | |
| # *** CAMBIO 2 (AÑADIDO): Redimensionar y recortar CADA clip a 720p *** | |
| sub_resized = sub_raw.resize(height=TARGET_RESOLUTION[1]).crop(x_center='center', y_center='center', width=TARGET_RESOLUTION[0], height=TARGET_RESOLUTION[1]) | |
| sub_raw.close() # Liberar memoria del clip intermedio sin redimensionar | |
| if sub_resized.duration is not None and sub_resized.duration > 0: | |
| clips_to_concatenate.append(sub_resized) | |
| current_duration += sub_resized.duration | |
| logger.debug(f"Segmento añadido: {sub_resized.duration:.1f}s (total: {current_duration:.1f}/{audio_duration:.1f}s)") | |
| else: | |
| sub_resized.close() | |
| except Exception as e: | |
| logger.warning(f"Error procesando video {path}: {str(e)}") | |
| if not clips_to_concatenate: | |
| raise ValueError("No hay segmentos de video válidos disponibles para crear el video.") | |
| video_base = concatenate_videoclips(clips_to_concatenate, method="chain") | |
| for seg in clips_to_concatenate: seg.close() # Limpieza de los clips en la lista | |
| clips_to_concatenate = [] | |
| if video_base.duration < audio_duration: | |
| video_base = video_base.loop(duration=audio_duration) | |
| if video_base.duration > audio_duration: | |
| video_base = video_base.subclip(0, audio_duration) | |
| # 6. Manejar música de fondo | |
| final_audio = audio_tts | |
| if musica_file: | |
| try: | |
| music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3") | |
| shutil.copyfile(musica_file, music_path) | |
| temp_intermediate_files.append(music_path) | |
| musica_audio_original = AudioFileClip(music_path) | |
| if musica_audio_original.duration > 0: | |
| musica_audio = loop_audio_to_length(musica_audio_original, video_base.duration) | |
| if musica_audio: | |
| final_audio = CompositeAudioClip([musica_audio.volumex(0.2), audio_tts.volumex(1.0)]) | |
| except Exception as e: | |
| logger.warning(f"Error procesando música: {str(e)}") | |
| # 7. Crear video final | |
| video_final = video_base.set_audio(final_audio) | |
| output_path = os.path.join(temp_dir_intermediate, "final_video.mp4") | |
| video_final.write_videofile( | |
| filename=output_path, fps=24, threads=4, codec="libx264", | |
| audio_codec="aac", preset="medium", logger='bar' | |
| ) | |
| total_time = (datetime.now() - start_time).total_seconds() | |
| logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {output_path} | Tiempo total: {total_time:.2f}s") | |
| return output_path | |
| except Exception as e: | |
| logger.critical(f"ERROR CRÍTICO en crear_video: {str(e)}", exc_info=True) | |
| raise | |
| finally: | |
| # Limpieza de todos los recursos de MoviePy | |
| all_clips = [audio_tts_original, musica_audio_original, audio_tts, musica_audio, video_base, video_final] + source_clips + clips_to_concatenate | |
| for clip_resource in all_clips: | |
| if clip_resource: | |
| try: clip_resource.close() | |
| except Exception as close_e: logger.warning(f"Error menor cerrando un clip: {close_e}") | |
| # Limpieza de archivos temporales | |
| if temp_dir_intermediate and os.path.exists(temp_dir_intermediate): | |
| final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4") | |
| for path in temp_intermediate_files: | |
| if os.path.isfile(path) and path != final_output_in_temp: | |
| try: | |
| os.remove(path) | |
| logger.debug(f"Eliminando archivo temporal intermedio: {path}") | |
| except Exception as rm_e: logger.warning(f"No se pudo eliminar archivo temporal {path}: {rm_e}") | |
| logger.info(f"Directorio temporal {temp_dir_intermediate} persistirá para Gradio.") | |
| 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 | |
| output_video, output_file, status_msg = None, gr.update(value=None, visible=False), gr.update(value="⏳ Procesando...", interactive=False) | |
| if not input_text or not input_text.strip(): | |
| return output_video, output_file, gr.update(value="⚠️ Por favor, ingresa un guion o tema.", interactive=False) | |
| try: | |
| video_path = crear_video(prompt_type, input_text, musica_file) | |
| if video_path and os.path.exists(video_path): | |
| output_video = video_path | |
| output_file = gr.update(value=video_path, visible=True) | |
| status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False) | |
| # *** CAMBIO 3 (AÑADIDO): Programar la eliminación automática del directorio del video *** | |
| temp_dir_to_delete = os.path.dirname(video_path) | |
| deletion_thread = threading.Thread( | |
| target=schedule_deletion, | |
| args=(temp_dir_to_delete, 3 * 3600) # 3 horas en segundos | |
| ) | |
| deletion_thread.daemon = True # Permite que el programa principal termine aunque el hilo esté esperando | |
| deletion_thread.start() | |
| else: | |
| status_msg = gr.update(value="❌ Error: La generación del video falló.", interactive=False) | |
| except Exception as e: | |
| status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False) | |
| finally: | |
| logger.info("Fin del handler run_app.") | |
| return output_video, output_file, status_msg | |
| 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") | |
| gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.") | |
| 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...") | |
| 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...") | |
| musica_input = gr.Audio(label="Música de fondo (opcional)", type="filepath") | |
| generate_btn = gr.Button("✨ Generar Video", variant="primary") | |
| with gr.Column(): | |
| video_output = gr.Video(label="Previsualización del Video Generado", interactive=False, height=400) | |
| file_output = gr.File(label="Descargar Archivo de Video", interactive=False, visible=False) | |
| 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, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)), | |
| outputs=[video_output, file_output, status_output], queue=True | |
| ).then( | |
| run_app, inputs=[prompt_type, prompt_ia, prompt_manual, musica_input], outputs=[video_output, file_output, status_output] | |
| ) | |
| gr.Markdown("### Instrucciones:") | |
| gr.Markdown("1. **Clave API de Pexels:** Asegúrate de tener la variable de entorno `PEXELS_API_KEY`.\n" | |
| "2. **Selecciona el método** y escribe tu tema o guion.\n" | |
| "3. **Sube música** (opcional).\n" | |
| "4. Haz clic en **Generar Video** y espera.\n" | |
| "5. El video generado se eliminará automáticamente del servidor después de 3 horas.") | |
| gr.Markdown("---") | |
| gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]") | |
| if __name__ == "__main__": | |
| logger.info("Iniciando aplicación Gradio...") | |
| try: | |
| app.launch(server_name="0.0.0.0", server_port=7860, share=False) | |
| except Exception as e: | |
| logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True) | |
| raise |