devAlvaro26 commited on
Commit
f589e4c
·
verified ·
1 Parent(s): c7c6638

Upload 5 files

Browse files
Files changed (5) hide show
  1. README.md +6 -6
  2. app.py +516 -0
  3. model.py +189 -0
  4. requirements.txt +7 -0
  5. unet2D_superres.pt +3 -0
README.md CHANGED
@@ -1,15 +1,15 @@
1
  ---
2
  title: AudioSuperRes
3
- emoji: 🌍
4
- colorFrom: red
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
- short_description: Inference for Audio SuperRes using Attention Res-UNet
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: AudioSuperRes
3
+ emoji:
4
+ colorFrom: yellow
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 6.16.0
8
+ python_version: '3.12.10'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
+ short_description: Inference for https://github.com/devAlvaro26/TFG
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,516 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # App de Gradio para despliegue en Hugging Face Spaces
2
+ # Super-resolución de audio con Attention Res-UNet 2D
3
+
4
+ import os
5
+ import torch
6
+ import torchaudio
7
+ import gradio as gr
8
+ import tempfile
9
+ import numpy as np
10
+ import matplotlib
11
+ matplotlib.use('Agg')
12
+ import matplotlib.pyplot as plt
13
+ from model import UNetAudio2D
14
+
15
+ # ── Constantes ──────────────────────────────────────────────
16
+ MODEL_PATH = "unet2D_superres.pt"
17
+ TARGET_SR = 44100
18
+ POOL_FACTOR = 16
19
+ N_FFT = 2048
20
+ HOP_LENGTH = 512
21
+ FRAGMENT_LENGTH = 65536
22
+
23
+ # ── Carga del modelo ────────────────────────────────────────
24
+ device = torch.device("cpu")
25
+ model = UNetAudio2D().to(device)
26
+ model.load_state_dict(torch.load(MODEL_PATH, map_location=device, weights_only=True))
27
+ model.eval()
28
+ print("✅ Modelo cargado correctamente.")
29
+
30
+
31
+ # ═══════════════════════════════════════════════════════════
32
+ # Funciones de procesamiento (pipeline idéntico a inference.py)
33
+ # ═══════════════════════════════════════════════════════════
34
+
35
+ def waveform_to_stft(waveform, n_fft=N_FFT, hop_length=HOP_LENGTH):
36
+ """Convierte una forma de onda a un STFT con canales real e imaginario."""
37
+ if waveform.ndim == 2:
38
+ waveform = waveform.squeeze(0)
39
+ window = torch.hann_window(n_fft, device='cpu')
40
+ stft = torch.stft(
41
+ waveform, n_fft=n_fft, hop_length=hop_length,
42
+ win_length=n_fft, window=window, return_complex=True,
43
+ )
44
+ return torch.stack([stft.real, stft.imag], dim=0)
45
+
46
+
47
+ def stft_to_waveform(stft, n_fft=N_FFT, hop_length=HOP_LENGTH, length=None):
48
+ """Convierte un STFT con canales real e imaginario a forma de onda."""
49
+ stft_complex = torch.complex(stft[0], stft[1])
50
+ window = torch.hann_window(n_fft)
51
+ waveform = torch.istft(
52
+ stft_complex, n_fft=n_fft, hop_length=hop_length,
53
+ win_length=n_fft, window=window, length=length,
54
+ )
55
+ return waveform.unsqueeze(0)
56
+
57
+
58
+ def normalize_stft(stft):
59
+ """Log-compresión de la magnitud STFT preservando la fase."""
60
+ real, imag = stft[0], stft[1]
61
+ magnitude = torch.sqrt(real**2 + imag**2 + 1e-8)
62
+ phase_cos = real / magnitude
63
+ phase_sin = imag / magnitude
64
+ mag_compressed = torch.log1p(magnitude)
65
+ return torch.stack([mag_compressed * phase_cos, mag_compressed * phase_sin], dim=0)
66
+
67
+
68
+ def denormalize_stft(stft):
69
+ """Inversa de la log-compresión de la STFT (expm1 = exp(x)-1)."""
70
+ real, imag = stft[0], stft[1]
71
+ mag_compressed = torch.sqrt(real**2 + imag**2 + 1e-8)
72
+ phase_cos = real / mag_compressed
73
+ phase_sin = imag / mag_compressed
74
+ magnitude = torch.expm1(mag_compressed)
75
+ return torch.stack([magnitude * phase_cos, magnitude * phase_sin], dim=0)
76
+
77
+
78
+ def pad_stft(stft, pool_factor=POOL_FACTOR):
79
+ """Padding para que las dimensiones sean divisibles por pool_factor."""
80
+ _, freq_bins, time_frames = stft.shape
81
+ pad_f = (pool_factor - (freq_bins % pool_factor)) % pool_factor
82
+ pad_t = (pool_factor - (time_frames % pool_factor)) % pool_factor
83
+ if pad_f > 0 or pad_t > 0:
84
+ stft = torch.nn.functional.pad(stft, (0, pad_t, 0, pad_f), mode='reflect')
85
+ return stft, freq_bins, time_frames
86
+
87
+
88
+ def process_audio_in_chunks(mdl, stft, orig_f, orig_t, chunk_frames, overlap=64):
89
+ """Procesa el STFT por chunks con overlap para evitar artefactos."""
90
+ _, F, T = stft.shape
91
+ hop = chunk_frames - overlap
92
+ output = torch.zeros_like(stft)
93
+ weights = torch.zeros(T, device=stft.device)
94
+
95
+ window = torch.ones(chunk_frames, device=stft.device)
96
+ if overlap > 0:
97
+ window[:overlap] = torch.linspace(0, 1, overlap, device=stft.device)
98
+ window[-overlap:] = torch.linspace(1, 0, overlap, device=stft.device)
99
+
100
+ start = 0
101
+ while start < T:
102
+ end = min(start + chunk_frames, T)
103
+ chunk = stft[:, :, start:end]
104
+
105
+ pad_t = chunk_frames - chunk.shape[-1]
106
+ if pad_t > 0:
107
+ pad_mode = 'reflect' if pad_t < chunk.shape[-1] else 'replicate'
108
+ chunk = torch.nn.functional.pad(chunk, (0, pad_t), mode=pad_mode)
109
+
110
+ with torch.no_grad():
111
+ pred_chunk = mdl(chunk.unsqueeze(0)).squeeze(0)
112
+
113
+ actual_len = end - start
114
+ w = window[:actual_len].clone()
115
+ if start == 0 and overlap > 0:
116
+ w[:overlap] = 1.0
117
+ if end == T and overlap > 0:
118
+ w[-overlap:] = 1.0
119
+
120
+ output[:, :, start:end] += pred_chunk[:, :, :actual_len] * w
121
+ weights[start:end] += w
122
+ start += hop
123
+
124
+ output = output / weights.unsqueeze(0).unsqueeze(0)
125
+ return output
126
+
127
+
128
+ # ── Generación de espectrogramas ──────────────────────���─────
129
+
130
+ def generate_spectrogram(waveform, sample_rate, n_fft=N_FFT, hop_length=HOP_LENGTH):
131
+ """Genera una imagen de espectrograma con estilo premium oscuro."""
132
+ spec_transform = torchaudio.transforms.Spectrogram(
133
+ n_fft=n_fft, hop_length=hop_length, power=2
134
+ )
135
+ spec = spec_transform(waveform.cpu()).squeeze().numpy()
136
+ spec_db = 10 * np.log10(spec + 1e-10)
137
+
138
+ nyquist = sample_rate / 2
139
+ duration = (spec.shape[1] * hop_length) / sample_rate
140
+ extent = [0, duration, 0, nyquist / 1000] # kHz
141
+
142
+ fig, ax = plt.subplots(figsize=(10, 3.5))
143
+ fig.patch.set_facecolor('#0c0f1a')
144
+ ax.set_facecolor('#0c0f1a')
145
+
146
+ im = ax.imshow(
147
+ spec_db, origin='lower', aspect='auto', cmap='magma',
148
+ extent=extent, vmin=-80, vmax=spec_db.max(),
149
+ )
150
+ ax.set_ylabel("Frecuencia (kHz)", color='#94a3b8', fontsize=10)
151
+ ax.set_xlabel("Tiempo (s)", color='#94a3b8', fontsize=10)
152
+ ax.tick_params(colors='#64748b', labelsize=8)
153
+ for spine in ax.spines.values():
154
+ spine.set_color('#1e293b')
155
+
156
+ cbar = fig.colorbar(im, ax=ax, pad=0.02)
157
+ cbar.set_label("dB", color='#94a3b8', fontsize=9)
158
+ cbar.ax.tick_params(colors='#64748b', labelsize=8)
159
+
160
+ plt.tight_layout(pad=0.5)
161
+ tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
162
+ plt.savefig(tmp.name, dpi=150, facecolor='#0c0f1a', edgecolor='none')
163
+ plt.close(fig)
164
+ return tmp.name
165
+
166
+
167
+ # ── Pipeline de inferencia ──────────────────────────────────
168
+
169
+ def inference(audio_path):
170
+ """Ejecuta la super-resolución sobre el audio de entrada."""
171
+ if audio_path is None:
172
+ raise gr.Error("Por favor, sube un archivo de audio.")
173
+
174
+ waveform, original_sr = torchaudio.load(audio_path)
175
+
176
+ # Resampleo a TARGET_SR
177
+ if original_sr != TARGET_SR:
178
+ resampler = torchaudio.transforms.Resample(original_sr, TARGET_SR)
179
+ waveform = resampler(waveform)
180
+
181
+ # Mono
182
+ if waveform.size(0) > 1:
183
+ waveform = waveform.mean(dim=0, keepdim=True)
184
+
185
+ # Normalización por amplitud máxima
186
+ max_val = waveform.abs().max().item() + 1e-8
187
+ waveform_norm = waveform / max_val
188
+ original_length = waveform_norm.size(1)
189
+ input_for_plot = waveform_norm.clone()
190
+
191
+ # STFT → Log-compresión → Padding
192
+ stft_input = waveform_to_stft(waveform_norm)
193
+ stft_input = normalize_stft(stft_input)
194
+ stft_padded, orig_f, orig_t = pad_stft(stft_input)
195
+
196
+ chunk_frames = FRAGMENT_LENGTH // HOP_LENGTH
197
+ chunk_frames = chunk_frames + (POOL_FACTOR - (chunk_frames % POOL_FACTOR)) % POOL_FACTOR
198
+
199
+ # Inferencia
200
+ predicted_stft = process_audio_in_chunks(
201
+ model, stft_padded, orig_f, orig_t, chunk_frames=chunk_frames
202
+ )
203
+ predicted_stft = predicted_stft[:, :orig_f, :orig_t]
204
+ predicted_stft = denormalize_stft(predicted_stft)
205
+
206
+ # ISTFT
207
+ predicted_waveform = stft_to_waveform(predicted_stft, length=original_length)
208
+ predicted_waveform = torch.nan_to_num(predicted_waveform, nan=0.0, posinf=1.0, neginf=-1.0)
209
+ predicted_waveform = torch.clamp(predicted_waveform, -1.0, 1.0)
210
+
211
+ # Guardar WAV
212
+ tmp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
213
+ torchaudio.save(tmp_wav.name, predicted_waveform, TARGET_SR, bits_per_sample=16, encoding="PCM_S")
214
+
215
+ # Espectrogramas
216
+ spec_in = generate_spectrogram(input_for_plot, TARGET_SR)
217
+ spec_out = generate_spectrogram(predicted_waveform, TARGET_SR)
218
+
219
+ return tmp_wav.name, spec_in, spec_out
220
+
221
+
222
+ # ═══════════════════════════════════════════════════════════
223
+ # DISEÑO GRADIO
224
+ # ═══════════════════════════════════════════════════════════
225
+
226
+ custom_theme = gr.themes.Base(
227
+ primary_hue=gr.themes.colors.indigo,
228
+ secondary_hue=gr.themes.colors.violet,
229
+ neutral_hue=gr.themes.colors.slate,
230
+ font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
231
+ font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
232
+ ).set(
233
+ body_background_fill="#0b0f1a",
234
+ body_text_color="#e2e8f0",
235
+ block_background_fill="rgba(15, 23, 42, 0.6)",
236
+ block_border_color="rgba(99, 102, 241, 0.15)",
237
+ block_border_width="1px",
238
+ block_radius="16px",
239
+ block_label_text_color="#a5b4fc",
240
+ block_title_text_color="#c7d2fe",
241
+ input_background_fill="rgba(15, 23, 42, 0.8)",
242
+ input_border_color="rgba(99, 102, 241, 0.2)",
243
+ input_border_width="1px",
244
+ button_primary_background_fill="linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)",
245
+ button_primary_background_fill_hover="linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #8b5cf6 100%)",
246
+ button_primary_text_color="#ffffff",
247
+ button_secondary_background_fill="rgba(99, 102, 241, 0.08)",
248
+ button_secondary_border_color="rgba(99, 102, 241, 0.25)",
249
+ button_secondary_text_color="#c7d2fe",
250
+ shadow_drop="0 4px 24px rgba(0,0,0,0.3)",
251
+ )
252
+
253
+ custom_css = """
254
+ /* ── Base ─────────────────────────────────────────── */
255
+ body, .gradio-container {
256
+ background: #0b0f1a !important;
257
+ background-image:
258
+ radial-gradient(ellipse 80% 60% at 50% -10%, rgba(99,102,241,0.12) 0%, transparent 60%),
259
+ radial-gradient(ellipse 60% 50% at 80% 50%, rgba(139,92,246,0.06) 0%, transparent 50%) !important;
260
+ min-height: 100vh;
261
+ }
262
+ /* ── Header ───────────────────────────────────────── */
263
+ .app-header {
264
+ text-align: center;
265
+ padding: 2.5rem 1rem 1rem;
266
+ animation: fadeDown 0.7s ease-out;
267
+ }
268
+ .app-header h1 {
269
+ font-size: 2.8rem !important;
270
+ font-weight: 800 !important;
271
+ background: linear-gradient(135deg, #818cf8 0%, #c084fc 50%, #f0abfc 100%);
272
+ -webkit-background-clip: text;
273
+ -webkit-text-fill-color: transparent;
274
+ background-clip: text;
275
+ letter-spacing: -0.03em;
276
+ margin-bottom: 0.25rem !important;
277
+ line-height: 1.2;
278
+ }
279
+ .app-subtitle {
280
+ color: #94a3b8 !important;
281
+ font-size: 1.05rem;
282
+ max-width: 640px;
283
+ margin: 0.5rem auto 0;
284
+ line-height: 1.65;
285
+ text-align: center;
286
+ animation: fadeIn 0.9s ease-out 0.25s both;
287
+ }
288
+ /* ── Badges ───────────────────────────────────────── */
289
+ .tech-badges {
290
+ display: flex;
291
+ justify-content: center;
292
+ gap: 0.5rem;
293
+ margin-top: 1rem;
294
+ flex-wrap: wrap;
295
+ animation: fadeIn 1s ease-out 0.4s both;
296
+ }
297
+ .tech-badge {
298
+ background: rgba(99,102,241,0.1);
299
+ border: 1px solid rgba(99,102,241,0.2);
300
+ color: #a5b4fc;
301
+ padding: 0.3rem 0.85rem;
302
+ border-radius: 999px;
303
+ font-size: 0.78rem;
304
+ font-weight: 500;
305
+ letter-spacing: 0.02em;
306
+ }
307
+ /* ── Glass panels ─────────────────────────────────── */
308
+ .panel-glass {
309
+ background: rgba(15, 23, 42, 0.55) !important;
310
+ backdrop-filter: blur(16px) saturate(1.2) !important;
311
+ -webkit-backdrop-filter: blur(16px) saturate(1.2) !important;
312
+ border: 1px solid rgba(99,102,241,0.12) !important;
313
+ border-radius: 20px !important;
314
+ padding: 1.5rem !important;
315
+ transition: border-color 0.35s ease, box-shadow 0.35s ease;
316
+ }
317
+ .panel-glass:hover {
318
+ border-color: rgba(99,102,241,0.25) !important;
319
+ box-shadow: 0 8px 32px rgba(99,102,241,0.08) !important;
320
+ }
321
+ .section-label {
322
+ font-size: 0.8rem !important;
323
+ font-weight: 600 !important;
324
+ text-transform: uppercase !important;
325
+ letter-spacing: 0.08em !important;
326
+ color: #818cf8 !important;
327
+ margin-bottom: 0.75rem !important;
328
+ }
329
+ /* ── Primary button ───────────────────────────────── */
330
+ .run-btn {
331
+ margin-top: 0.75rem !important;
332
+ font-weight: 700 !important;
333
+ letter-spacing: 0.04em !important;
334
+ text-transform: uppercase !important;
335
+ border-radius: 12px !important;
336
+ transition: all 0.3s cubic-bezier(0.4,0,0.2,1) !important;
337
+ box-shadow: 0 4px 20px rgba(99,102,241,0.25) !important;
338
+ }
339
+ .run-btn:hover {
340
+ transform: translateY(-2px) scale(1.01) !important;
341
+ box-shadow: 0 8px 30px rgba(99,102,241,0.4) !important;
342
+ }
343
+ /* ── Spectrogram section ──────────────────────────── */
344
+ .spec-section {
345
+ background: rgba(15, 23, 42, 0.45) !important;
346
+ backdrop-filter: blur(12px) !important;
347
+ border: 1px solid rgba(99,102,241,0.1) !important;
348
+ border-radius: 20px !important;
349
+ padding: 1.25rem !important;
350
+ margin-top: 1rem !important;
351
+ }
352
+ .spec-label {
353
+ text-align: center;
354
+ font-weight: 600;
355
+ color: #c7d2fe !important;
356
+ font-size: 0.85rem;
357
+ text-transform: uppercase;
358
+ letter-spacing: 0.06em;
359
+ margin-bottom: 0.5rem;
360
+ }
361
+ /* ── Footer ───────────────────────────────────────── */
362
+ .app-footer {
363
+ text-align: center;
364
+ color: #475569 !important;
365
+ font-size: 0.82rem;
366
+ margin-top: 2.5rem;
367
+ padding: 1.5rem 1rem;
368
+ border-top: 1px solid rgba(99,102,241,0.08);
369
+ line-height: 1.7;
370
+ }
371
+ .app-footer strong { color: #64748b; }
372
+ .app-footer a { color: #818cf8; text-decoration: none; }
373
+ .app-footer a:hover { text-decoration: underline; }
374
+ /* ── Animations ───────────────────────────────────── */
375
+ @keyframes fadeDown {
376
+ from { opacity: 0; transform: translateY(-18px); }
377
+ to { opacity: 1; transform: translateY(0); }
378
+ }
379
+ @keyframes fadeIn {
380
+ from { opacity: 0; }
381
+ to { opacity: 1; }
382
+ }
383
+ /* ── Download button ─────────────────────────────��── */
384
+ .dl-btn {
385
+ border-radius: 10px !important;
386
+ margin-top: 0.5rem !important;
387
+ }
388
+ /* ── Panel alignment: audio players at same height ── */
389
+ #left-panel, #right-panel {
390
+ display: flex !important;
391
+ flex-direction: column !important;
392
+ justify-content: flex-start !important;
393
+ align-items: stretch !important;
394
+ }
395
+ #left-panel > div, #right-panel > div {
396
+ flex: 0 0 auto !important;
397
+ }
398
+ #audio-input, #audio-output {
399
+ margin-top: 0 !important;
400
+ }
401
+ /* ── Responsive ───────────────────────────────────── */
402
+ @media (max-width: 768px) {
403
+ .app-header h1 { font-size: 2rem !important; }
404
+ .app-subtitle { font-size: 0.95rem; }
405
+ }
406
+ """
407
+
408
+ # ── Construcción de la interfaz ─────────────────────────────
409
+
410
+ with gr.Blocks(
411
+ title="Audio Super-Resolution · Attention Res-UNet 2D",
412
+ fill_width=True,
413
+ ) as demo:
414
+
415
+ # Header
416
+ gr.HTML("""
417
+ <div class="app-header">
418
+ <h1>🎧 Audio Super-Resolution</h1>
419
+ <p style="text-align: center;">
420
+ Restaura las altas frecuencias de grabaciones de baja calidad
421
+ utilizando un modelo <strong>Attention Res-UNet 2D</strong> entrenado
422
+ mediante representaciones STFT. Sube tu audio y obtén calidad
423
+ <strong>44.1 kHz</strong> en segundos.
424
+ </p>
425
+ <div class="tech-badges">
426
+ <span class="tech-badge">PyTorch</span>
427
+ <span class="tech-badge">STFT / iSTFT</span>
428
+ <span class="tech-badge">Attention Gates</span>
429
+ <span class="tech-badge">Residual UNet</span>
430
+ <span class="tech-badge">44.1 kHz</span>
431
+ </div>
432
+ </div>
433
+ """)
434
+
435
+ # ── Sección principal: entrada / salida ──
436
+ with gr.Row(equal_height=True):
437
+
438
+ # Panel izquierdo: entrada
439
+ with gr.Column(scale=1, elem_classes="panel-glass", elem_id="left-panel"):
440
+ gr.Markdown("#### <span class='section-label'>① Entrada de Audio</span>")
441
+ audio_input = gr.Audio(
442
+ label="Sube o graba tu audio",
443
+ type="filepath",
444
+ sources=["upload", "microphone"],
445
+ elem_id="audio-input",
446
+ )
447
+ btn = gr.Button(
448
+ "⚡ Procesar Super-Resolución",
449
+ variant="primary",
450
+ size="lg",
451
+ elem_classes="run-btn",
452
+ elem_id="run-btn",
453
+ )
454
+
455
+ # Panel derecho: salida
456
+ with gr.Column(scale=1, elem_classes="panel-glass", elem_id="right-panel"):
457
+ gr.Markdown("#### <span class='section-label'>② Audio Restaurado</span>")
458
+ audio_output = gr.Audio(
459
+ label="Resultado — Alta Resolución",
460
+ type="filepath",
461
+ interactive=False,
462
+ elem_id="audio-output",
463
+ )
464
+ download_btn = gr.DownloadButton(
465
+ label="⬇ Descargar WAV",
466
+ visible=False,
467
+ variant="secondary",
468
+ elem_classes="dl-btn",
469
+ elem_id="download-btn",
470
+ )
471
+
472
+ # ── Espectrogramas lado a lado ──
473
+ with gr.Row(equal_height=True, elem_classes="spec-section"):
474
+ with gr.Accordion("📉 Espectrograma · Entrada", open=False):
475
+ spec_input = gr.Image(
476
+ type="filepath",
477
+ interactive=False,
478
+ show_label=False,
479
+ elem_id="spec-input",
480
+ )
481
+ with gr.Accordion("📈 Espectrograma · Super-Resolución", open=False):
482
+ spec_output = gr.Image(
483
+ type="filepath",
484
+ interactive=False,
485
+ show_label=False,
486
+ elem_id="spec-output",
487
+ )
488
+
489
+ # ── Lógica de eventos ──
490
+ def run_pipeline(audio_path):
491
+ wav_path, spec_in_path, spec_out_path = inference(audio_path)
492
+ return (
493
+ wav_path,
494
+ spec_in_path,
495
+ spec_out_path,
496
+ gr.DownloadButton(visible=True, value=wav_path),
497
+ )
498
+
499
+ btn.click(
500
+ fn=run_pipeline,
501
+ inputs=[audio_input],
502
+ outputs=[audio_output, spec_input, spec_output, download_btn],
503
+ )
504
+
505
+ # Footer
506
+ gr.HTML("""
507
+ <div class="app-footer">
508
+ <strong>Proyecto:</strong> Sistema de Deep Learning para la extensión del ancho de banda basado en representaciones STFT<br>
509
+ <strong>Tecnología:</strong> STFT · Attention Res-UNet 2D · PyTorch<br>
510
+ Trabajo de Fin de Grado — Álvaro Roca Nacarino ·
511
+ <a href="https://huggingface.co/devAlvaro26/Unet2D_SuperRes" target="_blank">Hugging Face Spaces</a>
512
+ </div>
513
+ """)
514
+
515
+ if __name__ == "__main__":
516
+ demo.launch(theme=custom_theme, css=custom_css)
model.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Modelo Attention Res-UNet 2D para Audio Super Resolution
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+
7
+ class AttentionGate(nn.Module):
8
+ """Módulo de atención para ponderar skip connections."""
9
+ def __init__(self, F_g, F_l, F_int):
10
+ """
11
+ Inicializa el módulo de atención.
12
+ Args:
13
+ F_g (int): Número de canales del gating signal.
14
+ F_l (int): Número de canales de la skip connection.
15
+ F_int (int): Número de canales intermedios.
16
+ """
17
+ super().__init__()
18
+
19
+ # Uso de Attention Gates basado en el paper "https://arxiv.org/abs/1804.03999"
20
+ self.W_g = nn.Conv2d(F_g, F_int, kernel_size=1)
21
+ self.W_x = nn.Conv2d(F_l, F_int, kernel_size=1)
22
+ self.psi = nn.Conv2d(F_int, 1, kernel_size=1)
23
+ self.relu = nn.ReLU(inplace=True)
24
+ self.sigmoid = nn.Sigmoid()
25
+
26
+ def forward(self, g, x):
27
+ # Interpolar g para que tenga el mismo tamaño que x
28
+ if g.shape[-2:] != x.shape[-2:]:
29
+ g = F.interpolate(g, size=x.shape[-2:], mode="bilinear", align_corners=False)
30
+ # Calcular atención
31
+ att = self.sigmoid(self.psi(self.relu(self.W_g(g) + self.W_x(x))))
32
+ return x * att
33
+
34
+
35
+ class DilatedBlock(nn.Module):
36
+ """Bloque con capas convolucionales dilatadas para capturar contexto de largo alcance."""
37
+ def __init__(self, channels):
38
+ """
39
+ Inicializa el bloque dilatado.
40
+ Args:
41
+ channels (int): Número de canales de entrada y salida.
42
+ """
43
+ super().__init__()
44
+
45
+ self.net = nn.Sequential(
46
+ nn.Conv2d(channels, channels, kernel_size=(7,3), padding=(3,1), dilation=(1,1)),
47
+ nn.LeakyReLU(0.2, inplace=True),
48
+ nn.Conv2d(channels, channels, kernel_size=(7,3), padding=(6,2), dilation=(2,2)),
49
+ nn.LeakyReLU(0.2, inplace=True),
50
+ nn.Conv2d(channels, channels, kernel_size=(7,3), padding=(12,4), dilation=(4,4)),
51
+ nn.LeakyReLU(0.2, inplace=True),
52
+ )
53
+
54
+ def forward(self, x):
55
+ return self.net(x) + x
56
+
57
+
58
+ class ResBlock(nn.Module):
59
+ """Bloque Residual para Attention Res-UNet."""
60
+ def __init__(self, in_ch, out_ch):
61
+ """
62
+ Inicializa el bloque residual.
63
+ Args:
64
+ in_ch (int): Número de canales de entrada.
65
+ out_ch (int): Número de canales de salida.
66
+ """
67
+ super().__init__()
68
+
69
+ self.conv1 = nn.Conv2d(in_ch, out_ch, kernel_size=(7,3), padding=(3,1))
70
+ self.norm1 = nn.GroupNorm(out_ch//4, out_ch) # GroupNorm para batchs pequeños
71
+ self.relu1 = nn.LeakyReLU(0.2, inplace=True)
72
+
73
+ self.conv2 = nn.Conv2d(out_ch, out_ch, kernel_size=(7,3), padding=(3,1))
74
+ self.norm2 = nn.GroupNorm(out_ch//4, out_ch)
75
+ self.relu2 = nn.LeakyReLU(0.2, inplace=True)
76
+
77
+ # Skip connection
78
+ if in_ch != out_ch:
79
+ self.skip = nn.Sequential(
80
+ nn.Conv2d(in_ch, out_ch, kernel_size=1, bias=False),
81
+ nn.GroupNorm(out_ch//4, out_ch)
82
+ )
83
+ else:
84
+ self.skip = nn.Identity()
85
+
86
+ def forward(self, x):
87
+ identity = self.skip(x)
88
+
89
+ out = self.conv1(x)
90
+ out = self.norm1(out)
91
+ out = self.relu1(out)
92
+
93
+ out = self.conv2(out)
94
+ out = self.norm2(out)
95
+
96
+ out += identity
97
+ out = self.relu2(out)
98
+
99
+ return out
100
+
101
+
102
+ class UNetAudio2D(nn.Module):
103
+ """
104
+ Modelo Attention Res-UNet 2D para super resolución de audio.
105
+ Entrada y salida con shape (B, 2, F, T).
106
+ """
107
+ def __init__(self):
108
+ """Inicializa la arquitectura UNet con encoder, bottleneck y decoder."""
109
+ super().__init__()
110
+
111
+ # Encoder
112
+ # Entrada: (B, 2, F, T)
113
+ self.enc1 = ResBlock(2, 32)
114
+ self.down1 = nn.Conv2d(32, 32, kernel_size=(4,4), stride=(2,2), padding=(1,1)) # Strided conv
115
+
116
+ self.enc2 = ResBlock(32, 64)
117
+ self.down2 = nn.Conv2d(64, 64, kernel_size=(4,4), stride=(2,2), padding=(1,1))
118
+
119
+ self.enc3 = ResBlock(64, 128)
120
+ self.down3 = nn.Conv2d(128, 128, kernel_size=(4,4), stride=(2,2), padding=(1,1))
121
+
122
+ self.enc4 = ResBlock(128, 256)
123
+ self.down4 = nn.Conv2d(256, 256, kernel_size=(4,4), stride=(2,2), padding=(1,1))
124
+
125
+ # Bottleneck
126
+ self.bottleneck_conv = ResBlock(256, 512)
127
+ self.bottleneck_dilated = DilatedBlock(512)
128
+
129
+ # Decoder
130
+ self.up4 = self.up_block(512,256)
131
+ self.dec4 = ResBlock(512,256)
132
+
133
+ self.up3 = self.up_block(256,128)
134
+ self.dec3 = ResBlock(256,128)
135
+
136
+ self.up2 = self.up_block(128,64)
137
+ self.dec2 = ResBlock(128,64)
138
+
139
+ self.up1 = self.up_block(64,32)
140
+ self.dec1 = ResBlock(64,32)
141
+
142
+ # Attention gates
143
+ self.att4 = AttentionGate(256,256,128)
144
+ self.att3 = AttentionGate(128,128,64)
145
+ self.att2 = AttentionGate(64,64,32)
146
+ self.att1 = AttentionGate(32,32,16)
147
+
148
+ # Output
149
+ self.final = nn.Conv2d(32,2,kernel_size=1)
150
+
151
+ def up_block(self, in_ch, out_ch):
152
+ """
153
+ Crea un bloque de upsampling con ConvTranspose2d.
154
+ Args:
155
+ in_ch (int): Número de canales de entrada.
156
+ out_ch (int): Número de canales de salida.
157
+ Returns:
158
+ nn.Sequential: Bloque de upsampling en frecuencia y tiempo.
159
+ """
160
+ return nn.Sequential(
161
+ nn.ConvTranspose2d(in_ch, out_ch, kernel_size=(4,4), stride=(2,2), padding=(1,1)),
162
+ nn.LeakyReLU(0.2, inplace=True)
163
+ )
164
+
165
+ def forward(self, x):
166
+ # Encoder
167
+ e1 = self.enc1(x)
168
+ e2 = self.enc2(self.down1(e1))
169
+ e3 = self.enc3(self.down2(e2))
170
+ e4 = self.enc4(self.down3(e3))
171
+
172
+ # Bottleneck
173
+ b = self.bottleneck_conv(self.down4(e4))
174
+ b = self.bottleneck_dilated(b)
175
+
176
+ # Decoder con skip connections y attention gates
177
+ up4 = self.up4(b)
178
+ d4 = self.dec4(torch.cat([up4, self.att4(up4, e4)], dim=1))
179
+
180
+ up3 = self.up3(d4)
181
+ d3 = self.dec3(torch.cat([up3, self.att3(up3, e3)], dim=1))
182
+
183
+ up2 = self.up2(d3)
184
+ d2 = self.dec2(torch.cat([up2, self.att2(up2, e2)], dim=1))
185
+
186
+ up1 = self.up1(d2)
187
+ d1 = self.dec1(torch.cat([up1, self.att1(up1, e1)], dim=1))
188
+
189
+ return self.final(d1) + x
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cpu
2
+ torch==2.7.1
3
+ torchaudio==2.7.1
4
+ torchvision==0.22.1
5
+ soundfile==0.13.1
6
+ numpy==2.4.4
7
+ matplotlib==3.10.8
unet2D_superres.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:766188b767a72d4410407d1b42fdaf2623a3c8f47a7e61a5b938a383a32a3e7f
3
+ size 150518753