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 | |
) |