import os import asyncio import logging import tempfile import time import shutil from datetime import datetime, timedelta from moviepy.editor import VideoFileClip, AudioFileClip, CompositeAudioClip, ColorClip, TextClip, CompositeVideoClip import edge_tts import gradio as gr # Configuración avanzada de logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("video_generator.log"), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) # Directorio temporal personalizado con limpieza automática TEMP_DIR = "temp_media" os.makedirs(TEMP_DIR, exist_ok=True) def clean_old_files(): """Elimina archivos temporales con más de 24 horas""" now = time.time() cutoff = now - (24 * 3600) for filename in os.listdir(TEMP_DIR): file_path = os.path.join(TEMP_DIR, filename) if os.path.isfile(file_path): file_time = os.path.getmtime(file_path) if file_time < cutoff: try: os.remove(file_path) logger.info(f"Eliminado archivo antiguo: {filename}") except Exception as e: logger.error(f"Error al eliminar {filename}: {e}") async def text_to_speech(text, voice="es-ES-ElviraNeural"): """Convierte texto a voz y guarda en archivo temporal""" clean_old_files() output_path = os.path.join(TEMP_DIR, f"tts_{datetime.now().strftime('%Y%m%d%H%M%S')}.mp3") try: logger.info(f"Generando TTS para texto de {len(text)} caracteres") communicate = edge_tts.Communicate(text, voice) await communicate.save(output_path) return output_path except Exception as e: logger.error(f"Error en TTS: {e}") raise def create_audio_loop(audio_path, target_duration): """Crea un loop de audio hasta alcanzar la duración objetivo""" try: audio = AudioFileClip(audio_path) if audio.duration >= target_duration: return audio.subclip(0, target_duration) loops_needed = int(target_duration // audio.duration) + 1 clips = [audio] * loops_needed looped_audio = concatenate_audioclips(clips).subclip(0, target_duration) return looped_audio except Exception as e: logger.error(f"Error al crear loop de audio: {e}") raise def create_video_with_text(text, duration, size=(1280, 720)): """Crea un video simple con texto centrado""" try: # Fondo del video bg_clip = ColorClip(size, color=(30, 30, 30), duration=duration) # Texto con ajuste automático de tamaño text_clip = TextClip( text, fontsize=28, color='white', font='Arial-Bold', size=(size[0]-100, size[1]-100), method='caption', align='center' ).set_position('center').set_duration(duration) return CompositeVideoClip([bg_clip, text_clip]) except Exception as e: logger.error(f"Error al crear video con texto: {e}") raise async def generate_video_content(text, background_music=None, use_tts=True): """Función principal que genera el contenido del video""" try: clean_old_files() # 1. Procesar audio principal if use_tts: voice_path = await text_to_speech(text) main_audio = AudioFileClip(voice_path) else: # Si no usamos TTS, creamos un audio silencioso de la duración estimada estimated_duration = max(5, len(text.split()) / 3) # Estimación basada en palabras main_audio = AudioFileClip(lambda t: 0, duration=estimated_duration) duration = main_audio.duration # 2. Procesar música de fondo final_audio = main_audio if background_music: try: bg_music_loop = create_audio_loop(background_music, duration).volumex(0.2) final_audio = CompositeAudioClip([bg_music_loop, main_audio]) except Exception as e: logger.error(f"Error al procesar música de fondo, continuando sin ella: {e}") # 3. Crear video video_clip = create_video_with_text(text, duration) video_clip = video_clip.set_audio(final_audio) # 4. Guardar resultado output_path = os.path.join(TEMP_DIR, f"video_{datetime.now().strftime('%Y%m%d%H%M%S')}.mp4") video_clip.write_videofile( output_path, fps=24, threads=4, codec='libx264', audio_codec='aac', preset='fast', logger=None ) return output_path except Exception as e: logger.error(f"Error en generate_video_content: {e}") raise finally: # Cerrar todos los clips para liberar recursos if 'main_audio' in locals(): main_audio.close() if 'bg_music_loop' in locals(): bg_music_loop.close() if 'video_clip' in locals(): video_clip.close() # Interfaz Gradio mejorada with gr.Blocks(title="Generador de Videos Avanzado", theme="soft") as app: gr.Markdown(""" # 🎥 Generador de Videos Automático Crea videos con voz sintetizada y música de fondo """) with gr.Tab("Configuración Principal"): with gr.Row(): with gr.Column(): text_input = gr.Textbox( label="Texto del Video", placeholder="Escribe aquí el contenido de tu video...", lines=5, max_lines=20 ) with gr.Accordion("Opciones Avanzadas", open=False): use_tts = gr.Checkbox( label="Usar Texto a Voz (TTS)", value=True ) voice_selector = gr.Dropdown( label="Voz TTS", choices=["es-ES-ElviraNeural", "es-MX-DaliaNeural", "es-US-AlonsoNeural"], value="es-ES-ElviraNeural", visible=True ) bg_music = gr.Audio( label="Música de Fondo", type="filepath", sources=["upload"], format="mp3" ) generate_btn = gr.Button("Generar Video", variant="primary") with gr.Column(): video_output = gr.Video( label="Video Resultante", format="mp4", interactive=False ) status_output = gr.Textbox( label="Estado", interactive=False ) # Lógica para mostrar/ocultar selector de voz use_tts.change( fn=lambda x: gr.Dropdown(visible=x), inputs=use_tts, outputs=voice_selector ) # Función principal de generación def generate_video(text, use_tts, voice, bg_music): try: if not text.strip(): raise ValueError("Por favor ingresa un texto para el video") # Limpieza inicial clean_old_files() # Generación del video video_path = asyncio.run( generate_video_content( text=text, use_tts=use_tts, background_music=bg_music ) ) return video_path, "✅ Video generado con éxito" except Exception as e: logger.error(f"Error en la generación: {str(e)}") return None, f"❌ Error: {str(e)}" generate_btn.click( fn=generate_video, inputs=[text_input, use_tts, voice_selector, bg_music], outputs=[video_output, status_output] ) if __name__ == "__main__": # Limpieza inicial de archivos antiguos clean_old_files() # Configuración del servidor app.launch( server_name="0.0.0.0", server_port=7860, show_error=True, share=False, favicon_path=None )