import gradio as gr import moviepy.editor as mp import numpy as np import librosa from PIL import Image, ImageDraw import tempfile import os import logging import threading import time # Configuración de logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[logging.StreamHandler()] ) logger = logging.getLogger("audio_to_video") def generate_video(audio_file, image_file): try: logger.info("Iniciando generación del video...") # 1. Cargar audio logger.info(f"Cargando audio: {audio_file}") y, sr = librosa.load(audio_file, sr=None, mono=True) # Carga completa del audio duration = librosa.get_duration(y=y, sr=sr) logger.info(f"Audio cargado: {duration:.1f} segundos") # 2. Cargar imagen img = Image.open(image_file).convert('RGB') img_w, img_h = img.size logger.info(f"Imagen cargada: {img_w}x{img_h}") # 3. Analizar audio audio_envelope = np.abs(y) / np.max(np.abs(y)) # Normalizar audio_envelope_zoom = audio_envelope * 0.2 + 0.9 # Para zoom audio_envelope_wave = audio_envelope * (img_h // 6) # Para waveform # 4. Generar frames con zoom y waveform def make_frame(t): # Calcular posición en el audio time_idx = int(t * sr) # --- Efecto de Zoom --- zoom_factor = audio_envelope_zoom[time_idx] if time_idx < len(audio_envelope_zoom) else 1.0 new_size = (int(img_w * zoom_factor), int(img_h * zoom_factor)) zoomed_img = img.resize(new_size, Image.LANCZOS) # Recortar al tamaño original x_offset = (new_size[0] - img_w) // 2 y_offset = (new_size[1] - img_h) // 2 cropped_img = zoomed_img.crop(( x_offset, y_offset, x_offset + img_w, y_offset + img_h )) # --- Dibujar Waveform --- frame = ImageDraw.Draw(cropped_img) start_y = int(img_h * 0.8) # 80% hacia abajo # Extraer slice de audio start = max(0, time_idx - sr//10) end = min(len(audio_envelope_wave), time_idx + sr//10) wave_slice = audio_envelope_wave[start:end] # Dibujar onda points = [] for i, val in enumerate(wave_slice): x = int((i / len(wave_slice)) * img_w) y_pos = start_y - int(val) y_neg = start_y + int(val) points.extend([(x, y_pos), (x, y_neg)]) if len(points) > 2: frame.polygon(points, fill=(255, 0, 0, 150)) # Rojo semitransparente return np.array(cropped_img) # 5. Crear video video = mp.VideoClip(make_frame, duration=duration) video.fps = 24 video = video.set_audio(mp.AudioFileClip(audio_file)) # 6. Guardar video en un directorio temporal persistente temp_dir = tempfile.mkdtemp() output_path = os.path.join(temp_dir, "output.mp4") logger.info(f"Exportando video a: {output_path}") video.write_videofile( output_path, codec="libx264", audio_codec="aac", fps=24, logger=None ) # Verificar que el archivo existe if not os.path.exists(output_path): raise Exception("Error: El archivo de video no se generó correctamente.") logger.info(f"Video guardado correctamente: {output_path}") # Programar eliminación del archivo después de 30 minutos def delete_file_after_delay(file_path, delay_minutes): time.sleep(delay_minutes * 60) # Convertir minutos a segundos try: if os.path.exists(file_path): os.remove(file_path) logger.info(f"Archivo eliminado: {file_path}") temp_dir = os.path.dirname(file_path) if os.path.exists(temp_dir): os.rmdir(temp_dir) logger.info(f"Directorio temporal eliminado: {temp_dir}") except Exception as e: logger.error(f"Error al eliminar archivo o directorio: {e}") threading.Thread(target=delete_file_after_delay, args=(output_path, 30)).start() return output_path # Retornar la ruta completa except Exception as e: logger.error(f"Error crítico: {str(e)}", exc_info=True) return None # Interfaz Gradio iface = gr.Interface( fn=generate_video, inputs=[ gr.Audio(type="filepath", label="Audio (WAV/MP3)"), gr.Image(type="filepath", label="Imagen de Fondo") ], outputs=gr.File(label="Descargar Video"), # Muestra el enlace de descarga title="Generador de Video Musical", description="Crea videos con zoom automático y efectos de audio sincronizados" ) if __name__ == "__main__": iface.queue(max_size=1).launch(show_error=True)