Spaces:
Sleeping
Sleeping
| 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 | |
| ) |