Spaces:
Runtime error
Runtime error
| # App de Gradio para despliegue en Hugging Face Spaces | |
| # Super-resolución de audio con Attention Res-UNet 2D | |
| import os | |
| import torch | |
| import torchaudio | |
| import gradio as gr | |
| import tempfile | |
| import numpy as np | |
| import matplotlib | |
| matplotlib.use('Agg') | |
| import matplotlib.pyplot as plt | |
| from model import UNetAudio2D | |
| # ── Constantes ────────────────────────────────────────────── | |
| MODEL_PATH = "unet2D_superres.pt" | |
| TARGET_SR = 44100 | |
| POOL_FACTOR = 16 | |
| N_FFT = 2048 | |
| HOP_LENGTH = 512 | |
| FRAGMENT_LENGTH = 65536 | |
| # ── Carga del modelo ──────────────────────────────────────── | |
| device = torch.device("cpu") | |
| model = UNetAudio2D().to(device) | |
| model.load_state_dict(torch.load(MODEL_PATH, map_location=device, weights_only=True)) | |
| model.eval() | |
| print("✅ Modelo cargado correctamente.") | |
| # ═══════════════════════════════════════════════════════════ | |
| # Funciones de procesamiento (pipeline idéntico a inference.py) | |
| # ═══════════════════════════════════════════════════════════ | |
| def waveform_to_stft(waveform, n_fft=N_FFT, hop_length=HOP_LENGTH): | |
| """Convierte una forma de onda a un STFT con canales real e imaginario.""" | |
| if waveform.ndim == 2: | |
| waveform = waveform.squeeze(0) | |
| window = torch.hann_window(n_fft, device='cpu') | |
| stft = torch.stft( | |
| waveform, n_fft=n_fft, hop_length=hop_length, | |
| win_length=n_fft, window=window, return_complex=True, | |
| ) | |
| return torch.stack([stft.real, stft.imag], dim=0) | |
| def stft_to_waveform(stft, n_fft=N_FFT, hop_length=HOP_LENGTH, length=None): | |
| """Convierte un STFT con canales real e imaginario a forma de onda.""" | |
| stft_complex = torch.complex(stft[0], stft[1]) | |
| window = torch.hann_window(n_fft) | |
| waveform = torch.istft( | |
| stft_complex, n_fft=n_fft, hop_length=hop_length, | |
| win_length=n_fft, window=window, length=length, | |
| ) | |
| return waveform.unsqueeze(0) | |
| def normalize_stft(stft): | |
| """Log-compresión de la magnitud STFT preservando la fase.""" | |
| real, imag = stft[0], stft[1] | |
| magnitude = torch.sqrt(real**2 + imag**2 + 1e-8) | |
| phase_cos = real / magnitude | |
| phase_sin = imag / magnitude | |
| mag_compressed = torch.log1p(magnitude) | |
| return torch.stack([mag_compressed * phase_cos, mag_compressed * phase_sin], dim=0) | |
| def denormalize_stft(stft): | |
| """Inversa de la log-compresión de la STFT (expm1 = exp(x)-1).""" | |
| real, imag = stft[0], stft[1] | |
| mag_compressed = torch.sqrt(real**2 + imag**2 + 1e-8) | |
| phase_cos = real / mag_compressed | |
| phase_sin = imag / mag_compressed | |
| magnitude = torch.expm1(mag_compressed) | |
| return torch.stack([magnitude * phase_cos, magnitude * phase_sin], dim=0) | |
| def pad_stft(stft, pool_factor=POOL_FACTOR): | |
| """Padding para que las dimensiones sean divisibles por pool_factor.""" | |
| _, freq_bins, time_frames = stft.shape | |
| pad_f = (pool_factor - (freq_bins % pool_factor)) % pool_factor | |
| pad_t = (pool_factor - (time_frames % pool_factor)) % pool_factor | |
| if pad_f > 0 or pad_t > 0: | |
| stft = torch.nn.functional.pad(stft, (0, pad_t, 0, pad_f), mode='reflect') | |
| return stft, freq_bins, time_frames | |
| def process_audio_in_chunks(mdl, stft, orig_f, orig_t, chunk_frames, overlap=64): | |
| """Procesa el STFT por chunks con overlap para evitar artefactos.""" | |
| _, F, T = stft.shape | |
| hop = chunk_frames - overlap | |
| output = torch.zeros_like(stft) | |
| weights = torch.zeros(T, device=stft.device) | |
| window = torch.ones(chunk_frames, device=stft.device) | |
| if overlap > 0: | |
| window[:overlap] = torch.linspace(0, 1, overlap, device=stft.device) | |
| window[-overlap:] = torch.linspace(1, 0, overlap, device=stft.device) | |
| start = 0 | |
| while start < T: | |
| end = min(start + chunk_frames, T) | |
| chunk = stft[:, :, start:end] | |
| pad_t = chunk_frames - chunk.shape[-1] | |
| if pad_t > 0: | |
| pad_mode = 'reflect' if pad_t < chunk.shape[-1] else 'replicate' | |
| chunk = torch.nn.functional.pad(chunk, (0, pad_t), mode=pad_mode) | |
| with torch.no_grad(): | |
| pred_chunk = mdl(chunk.unsqueeze(0)).squeeze(0) | |
| actual_len = end - start | |
| w = window[:actual_len].clone() | |
| if start == 0 and overlap > 0: | |
| w[:overlap] = 1.0 | |
| if end == T and overlap > 0: | |
| w[-overlap:] = 1.0 | |
| output[:, :, start:end] += pred_chunk[:, :, :actual_len] * w | |
| weights[start:end] += w | |
| start += hop | |
| output = output / weights.unsqueeze(0).unsqueeze(0) | |
| return output | |
| # ── Generación de espectrogramas ──────────────────────────── | |
| def generate_spectrogram(waveform, sample_rate, n_fft=N_FFT, hop_length=HOP_LENGTH): | |
| """Genera una imagen de espectrograma con estilo premium oscuro.""" | |
| spec_transform = torchaudio.transforms.Spectrogram( | |
| n_fft=n_fft, hop_length=hop_length, power=2 | |
| ) | |
| spec = spec_transform(waveform.cpu()).squeeze().numpy() | |
| spec_db = 10 * np.log10(spec + 1e-10) | |
| nyquist = sample_rate / 2 | |
| duration = (spec.shape[1] * hop_length) / sample_rate | |
| extent = [0, duration, 0, nyquist / 1000] # kHz | |
| fig, ax = plt.subplots(figsize=(10, 3.5)) | |
| fig.patch.set_facecolor('#0c0f1a') | |
| ax.set_facecolor('#0c0f1a') | |
| im = ax.imshow( | |
| spec_db, origin='lower', aspect='auto', cmap='magma', | |
| extent=extent, vmin=-80, vmax=spec_db.max(), | |
| ) | |
| ax.set_ylabel("Frecuencia (kHz)", color='#94a3b8', fontsize=10) | |
| ax.set_xlabel("Tiempo (s)", color='#94a3b8', fontsize=10) | |
| ax.tick_params(colors='#64748b', labelsize=8) | |
| for spine in ax.spines.values(): | |
| spine.set_color('#1e293b') | |
| cbar = fig.colorbar(im, ax=ax, pad=0.02) | |
| cbar.set_label("dB", color='#94a3b8', fontsize=9) | |
| cbar.ax.tick_params(colors='#64748b', labelsize=8) | |
| plt.tight_layout(pad=0.5) | |
| tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False) | |
| plt.savefig(tmp.name, dpi=150, facecolor='#0c0f1a', edgecolor='none') | |
| plt.close(fig) | |
| return tmp.name | |
| # ── Pipeline de inferencia ────────────────────────────────── | |
| def inference(audio_path): | |
| """Ejecuta la super-resolución sobre el audio de entrada.""" | |
| if audio_path is None: | |
| raise gr.Error("Por favor, sube un archivo de audio.") | |
| waveform, original_sr = torchaudio.load(audio_path) | |
| # Resampleo a TARGET_SR | |
| if original_sr != TARGET_SR: | |
| resampler = torchaudio.transforms.Resample(original_sr, TARGET_SR) | |
| waveform = resampler(waveform) | |
| # Mono | |
| if waveform.size(0) > 1: | |
| waveform = waveform.mean(dim=0, keepdim=True) | |
| # Normalización por amplitud máxima | |
| max_val = waveform.abs().max().item() + 1e-8 | |
| waveform_norm = waveform / max_val | |
| original_length = waveform_norm.size(1) | |
| input_for_plot = waveform_norm.clone() | |
| # STFT → Log-compresión → Padding | |
| stft_input = waveform_to_stft(waveform_norm) | |
| stft_input = normalize_stft(stft_input) | |
| stft_padded, orig_f, orig_t = pad_stft(stft_input) | |
| chunk_frames = FRAGMENT_LENGTH // HOP_LENGTH | |
| chunk_frames = chunk_frames + (POOL_FACTOR - (chunk_frames % POOL_FACTOR)) % POOL_FACTOR | |
| # Inferencia | |
| predicted_stft = process_audio_in_chunks( | |
| model, stft_padded, orig_f, orig_t, chunk_frames=chunk_frames | |
| ) | |
| predicted_stft = predicted_stft[:, :orig_f, :orig_t] | |
| predicted_stft = denormalize_stft(predicted_stft) | |
| # ISTFT | |
| predicted_waveform = stft_to_waveform(predicted_stft, length=original_length) | |
| predicted_waveform = torch.nan_to_num(predicted_waveform, nan=0.0, posinf=1.0, neginf=-1.0) | |
| predicted_waveform = torch.clamp(predicted_waveform, -1.0, 1.0) | |
| # Guardar WAV | |
| tmp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) | |
| torchaudio.save(tmp_wav.name, predicted_waveform, TARGET_SR, bits_per_sample=16, encoding="PCM_S") | |
| # Espectrogramas | |
| spec_in = generate_spectrogram(input_for_plot, TARGET_SR) | |
| spec_out = generate_spectrogram(predicted_waveform, TARGET_SR) | |
| return tmp_wav.name, spec_in, spec_out | |
| # ═══════════════════════════════════════════════════════════ | |
| # DISEÑO GRADIO | |
| # ═══════════════════════════════════════════════════════════ | |
| custom_theme = gr.themes.Base( | |
| primary_hue=gr.themes.colors.indigo, | |
| secondary_hue=gr.themes.colors.violet, | |
| neutral_hue=gr.themes.colors.slate, | |
| font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], | |
| font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"], | |
| ).set( | |
| body_background_fill="#0b0f1a", | |
| body_text_color="#e2e8f0", | |
| block_background_fill="rgba(15, 23, 42, 0.6)", | |
| block_border_color="rgba(99, 102, 241, 0.15)", | |
| block_border_width="1px", | |
| block_radius="16px", | |
| block_label_text_color="#a5b4fc", | |
| block_title_text_color="#c7d2fe", | |
| input_background_fill="rgba(15, 23, 42, 0.8)", | |
| input_border_color="rgba(99, 102, 241, 0.2)", | |
| input_border_width="1px", | |
| button_primary_background_fill="linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)", | |
| button_primary_background_fill_hover="linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #8b5cf6 100%)", | |
| button_primary_text_color="#ffffff", | |
| button_secondary_background_fill="rgba(99, 102, 241, 0.08)", | |
| button_secondary_border_color="rgba(99, 102, 241, 0.25)", | |
| button_secondary_text_color="#c7d2fe", | |
| shadow_drop="0 4px 24px rgba(0,0,0,0.3)", | |
| ) | |
| custom_css = """ | |
| /* ── Base ─────────────────────────────────────────── */ | |
| body, .gradio-container { | |
| background: #0b0f1a !important; | |
| background-image: | |
| radial-gradient(ellipse 80% 60% at 50% -10%, rgba(99,102,241,0.12) 0%, transparent 60%), | |
| radial-gradient(ellipse 60% 50% at 80% 50%, rgba(139,92,246,0.06) 0%, transparent 50%) !important; | |
| min-height: 100vh; | |
| } | |
| /* ── Header ───────────────────────────────────────── */ | |
| .app-header { | |
| text-align: center; | |
| padding: 2.5rem 1rem 1rem; | |
| animation: fadeDown 0.7s ease-out; | |
| } | |
| .app-header h1 { | |
| font-size: 2.8rem !important; | |
| font-weight: 800 !important; | |
| background: linear-gradient(135deg, #818cf8 0%, #c084fc 50%, #f0abfc 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| letter-spacing: -0.03em; | |
| margin-bottom: 0.25rem !important; | |
| line-height: 1.2; | |
| } | |
| .app-subtitle { | |
| color: #94a3b8 !important; | |
| font-size: 1.05rem; | |
| max-width: 640px; | |
| margin: 0.5rem auto 0; | |
| line-height: 1.65; | |
| text-align: center; | |
| animation: fadeIn 0.9s ease-out 0.25s both; | |
| } | |
| /* ── Badges ───────────────────────────────────────── */ | |
| .tech-badges { | |
| display: flex; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| margin-top: 1rem; | |
| flex-wrap: wrap; | |
| animation: fadeIn 1s ease-out 0.4s both; | |
| } | |
| .tech-badge { | |
| background: rgba(99,102,241,0.1); | |
| border: 1px solid rgba(99,102,241,0.2); | |
| color: #a5b4fc; | |
| padding: 0.3rem 0.85rem; | |
| border-radius: 999px; | |
| font-size: 0.78rem; | |
| font-weight: 500; | |
| letter-spacing: 0.02em; | |
| } | |
| /* ── Glass panels ─────────────────────────────────── */ | |
| .panel-glass { | |
| background: rgba(15, 23, 42, 0.55) !important; | |
| backdrop-filter: blur(16px) saturate(1.2) !important; | |
| -webkit-backdrop-filter: blur(16px) saturate(1.2) !important; | |
| border: 1px solid rgba(99,102,241,0.12) !important; | |
| border-radius: 20px !important; | |
| padding: 1.5rem !important; | |
| transition: border-color 0.35s ease, box-shadow 0.35s ease; | |
| } | |
| .panel-glass:hover { | |
| border-color: rgba(99,102,241,0.25) !important; | |
| box-shadow: 0 8px 32px rgba(99,102,241,0.08) !important; | |
| } | |
| .section-label { | |
| font-size: 0.8rem !important; | |
| font-weight: 600 !important; | |
| text-transform: uppercase !important; | |
| letter-spacing: 0.08em !important; | |
| color: #818cf8 !important; | |
| margin-bottom: 0.75rem !important; | |
| } | |
| /* ── Primary button ───────────────────────────────── */ | |
| .run-btn { | |
| margin-top: 0.75rem !important; | |
| font-weight: 700 !important; | |
| letter-spacing: 0.04em !important; | |
| text-transform: uppercase !important; | |
| border-radius: 12px !important; | |
| transition: all 0.3s cubic-bezier(0.4,0,0.2,1) !important; | |
| box-shadow: 0 4px 20px rgba(99,102,241,0.25) !important; | |
| } | |
| .run-btn:hover { | |
| transform: translateY(-2px) scale(1.01) !important; | |
| box-shadow: 0 8px 30px rgba(99,102,241,0.4) !important; | |
| } | |
| /* ── Spectrogram section ──────────────────────────── */ | |
| .spec-section { | |
| background: rgba(15, 23, 42, 0.45) !important; | |
| backdrop-filter: blur(12px) !important; | |
| border: 1px solid rgba(99,102,241,0.1) !important; | |
| border-radius: 20px !important; | |
| padding: 1.25rem !important; | |
| margin-top: 1rem !important; | |
| } | |
| .spec-label { | |
| text-align: center; | |
| font-weight: 600; | |
| color: #c7d2fe !important; | |
| font-size: 0.85rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| margin-bottom: 0.5rem; | |
| } | |
| /* ── Footer ───────────────────────────────────────── */ | |
| .app-footer { | |
| text-align: center; | |
| color: #475569 !important; | |
| font-size: 0.82rem; | |
| margin-top: 2.5rem; | |
| padding: 1.5rem 1rem; | |
| border-top: 1px solid rgba(99,102,241,0.08); | |
| line-height: 1.7; | |
| } | |
| .app-footer strong { color: #64748b; } | |
| .app-footer a { color: #818cf8; text-decoration: none; } | |
| .app-footer a:hover { text-decoration: underline; } | |
| /* ── Animations ───────────────────────────────────── */ | |
| @keyframes fadeDown { | |
| from { opacity: 0; transform: translateY(-18px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| /* ── Download button ──────────────────────────────── */ | |
| .dl-btn { | |
| border-radius: 10px !important; | |
| margin-top: 0.5rem !important; | |
| } | |
| /* ── Panel alignment: audio players at same height ── */ | |
| #left-panel, #right-panel { | |
| display: flex !important; | |
| flex-direction: column !important; | |
| justify-content: flex-start !important; | |
| align-items: stretch !important; | |
| } | |
| #left-panel > div, #right-panel > div { | |
| flex: 0 0 auto !important; | |
| } | |
| #audio-input, #audio-output { | |
| margin-top: 0 !important; | |
| } | |
| /* ── Responsive ───────────────────────────────────── */ | |
| @media (max-width: 768px) { | |
| .app-header h1 { font-size: 2rem !important; } | |
| .app-subtitle { font-size: 0.95rem; } | |
| } | |
| """ | |
| # ── Construcción de la interfaz ───────────────────────────── | |
| with gr.Blocks( | |
| title="Audio Super-Resolution · Attention Res-UNet 2D", | |
| fill_width=True, | |
| ) as demo: | |
| # Header | |
| gr.HTML(""" | |
| <div class="app-header"> | |
| <h1>🎧 Audio Super-Resolution</h1> | |
| <p style="text-align: center;"> | |
| Restaura las altas frecuencias de grabaciones de baja calidad | |
| utilizando un modelo <strong>Attention Res-UNet 2D</strong> entrenado | |
| mediante representaciones STFT. Sube tu audio y obtén calidad | |
| <strong>44.1 kHz</strong> en segundos. | |
| </p> | |
| <div class="tech-badges"> | |
| <span class="tech-badge">PyTorch</span> | |
| <span class="tech-badge">STFT / iSTFT</span> | |
| <span class="tech-badge">Attention Gates</span> | |
| <span class="tech-badge">Residual UNet</span> | |
| <span class="tech-badge">44.1 kHz</span> | |
| </div> | |
| </div> | |
| """) | |
| # ── Sección principal: entrada / salida ── | |
| with gr.Row(equal_height=True): | |
| # Panel izquierdo: entrada | |
| with gr.Column(scale=1, elem_classes="panel-glass", elem_id="left-panel"): | |
| gr.Markdown("#### <span class='section-label'>① Entrada de Audio</span>") | |
| audio_input = gr.Audio( | |
| label="Sube o graba tu audio", | |
| type="filepath", | |
| sources=["upload", "microphone"], | |
| elem_id="audio-input", | |
| ) | |
| btn = gr.Button( | |
| "⚡ Procesar Super-Resolución", | |
| variant="primary", | |
| size="lg", | |
| elem_classes="run-btn", | |
| elem_id="run-btn", | |
| ) | |
| # Panel derecho: salida | |
| with gr.Column(scale=1, elem_classes="panel-glass", elem_id="right-panel"): | |
| gr.Markdown("#### <span class='section-label'>② Audio Restaurado</span>") | |
| audio_output = gr.Audio( | |
| label="Resultado — Alta Resolución", | |
| type="filepath", | |
| interactive=False, | |
| elem_id="audio-output", | |
| ) | |
| download_btn = gr.DownloadButton( | |
| label="⬇ Descargar WAV", | |
| visible=False, | |
| variant="secondary", | |
| elem_classes="dl-btn", | |
| elem_id="download-btn", | |
| ) | |
| # ── Espectrogramas lado a lado ── | |
| with gr.Row(equal_height=True, elem_classes="spec-section"): | |
| with gr.Accordion("📉 Espectrograma · Entrada", open=False): | |
| spec_input = gr.Image( | |
| type="filepath", | |
| interactive=False, | |
| show_label=False, | |
| elem_id="spec-input", | |
| ) | |
| with gr.Accordion("📈 Espectrograma · Super-Resolución", open=False): | |
| spec_output = gr.Image( | |
| type="filepath", | |
| interactive=False, | |
| show_label=False, | |
| elem_id="spec-output", | |
| ) | |
| # ── Lógica de eventos ── | |
| def run_pipeline(audio_path): | |
| wav_path, spec_in_path, spec_out_path = inference(audio_path) | |
| return ( | |
| wav_path, | |
| spec_in_path, | |
| spec_out_path, | |
| gr.DownloadButton(visible=True, value=wav_path), | |
| ) | |
| btn.click( | |
| fn=run_pipeline, | |
| inputs=[audio_input], | |
| outputs=[audio_output, spec_input, spec_output, download_btn], | |
| ) | |
| # Footer | |
| gr.HTML(""" | |
| <div class="app-footer"> | |
| <strong>Proyecto:</strong> Sistema de Deep Learning para la extensión del ancho de banda basado en representaciones STFT<br> | |
| <strong>Tecnología:</strong> STFT · Attention Res-UNet 2D · PyTorch<br> | |
| Trabajo de Fin de Grado — Álvaro Roca Nacarino · | |
| <a href="https://huggingface.co/devAlvaro26/Unet2D_SuperRes" target="_blank">Hugging Face Spaces</a> | |
| </div> | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch(theme=custom_theme, css=custom_css) |