File size: 8,307 Bytes
9e5d646
7e8e229
 
 
 
 
 
9e5d646
 
7e8e229
410a586
7e8e229
 
fc527e7
7e8e229
 
410a586
fc527e7
 
 
 
9e5d646
7e8e229
 
 
 
 
 
 
 
9e5d646
7e8e229
 
 
 
 
 
 
9e5d646
7e8e229
 
 
 
 
 
9e5d646
7e8e229
 
 
9e5d646
7e8e229
 
 
 
 
9e5d646
7e8e229
9e5d646
7e8e229
 
 
 
 
fc527e7
 
9e5d646
fc527e7
 
9e5d646
 
7e8e229
 
 
 
 
 
9e5d646
 
fc527e7
 
 
 
 
 
 
 
 
 
7e8e229
fc527e7
7e8e229
 
 
 
 
9e5d646
7e8e229
 
 
 
 
 
 
 
9e5d646
 
7e8e229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e5d646
 
7e8e229
 
 
 
 
 
 
 
 
 
 
9e5d646
 
7e8e229
 
9e5d646
 
7e8e229
 
 
9e5d646
 
fc527e7
 
 
 
 
7e8e229
 
9e5d646
 
7e8e229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fc527e7
 
 
7e8e229
 
 
 
 
 
 
 
 
 
fc527e7
 
 
7e8e229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e5d646
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
"""
denoise_core.py — Motor de reducción de ruido (DeepFilterNet3) con ZeroGPU y respaldo en CPU.

- El modelo trabaja a 48 kHz. Quien llama se encarga de remuestrear (ver dsp.py).
- El audio se procesa por bloques con contexto y fundido cruzado, para que cada
  llamada a la GPU sea corta y la memoria no dependa de la duración del fichero.
- Si la GPU no está disponible (cuota agotada, cola, error), se usa la CPU.
"""

from __future__ import annotations

import copy
import sys
import time
import types
import warnings

try:
    import spaces  # en ZeroGPU debe importarse antes que torch
except ImportError:  # versión de escritorio: sin ZeroGPU
    spaces = None

# Shim de compatibilidad: deepfilternet 0.5.6 importa torchaudio.backend.common (solo para df.io,
# que no usamos). Versiones recientes de torchaudio lo han eliminado: si falta, se crea uno mínimo.
try:
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        import torchaudio.backend.common  # noqa: F401
except ImportError:
    from dataclasses import dataclass

    @dataclass
    class AudioMetaData:
        sample_rate: int
        num_frames: int
        num_channels: int
        bits_per_sample: int
        encoding: str

    _common = types.ModuleType("torchaudio.backend.common")
    _common.AudioMetaData = AudioMetaData
    _backend = types.ModuleType("torchaudio.backend")
    _backend.common = _common
    sys.modules["torchaudio.backend"] = _backend
    sys.modules["torchaudio.backend.common"] = _common

import numpy as np
import torch
from torch.nn import functional as F

with warnings.catch_warnings():
    warnings.simplefilter("ignore")  # avisos de deprecación de torchaudio dentro de deepfilternet
    from df.enhance import df_features, init_df
    from df.model import ModelParams
    from df.utils import as_complex

from models import dfn3_dir

DF_SR = 48000
CHUNK_SEC = 60.0      # bloque útil por inferencia
CONTEXT_SEC = 5.0     # contexto extra a cada lado para que el modelo "se asiente"
XFADE_SEC = 0.05      # fundido entre bloques
GROUP_SEC = 300.0     # audio máximo por llamada a la GPU
GPU_MIN_SEC = 90.0    # por debajo, la espera de ZeroGPU (cola, asignación) cuesta más que hacerlo en CPU
GPU_COOLDOWN_SEC = 900.0  # tras un fallo de GPU (p. ej. cuota) no se reintenta en este tiempo

ON_ZEROGPU = bool(spaces is not None and spaces.config.Config.zero_gpu)
_gpu_blocked_until = 0.0


def _force_df_device(device: str) -> None:
    """DeepFilterNet decide el dispositivo con df.utils.get_device(); lo fijamos nosotros."""
    dev = torch.device(device)
    for name, mod in list(sys.modules.items()):
        if (name == "df" or name.startswith("df.")) and hasattr(mod, "get_device"):
            mod.get_device = lambda dev=dev: dev


def _disable_df_git() -> None:
    """DeepFilterNet consulta git al iniciar su log; sin git instalado (Windows) eso rompe el arranque."""
    import df.logger
    import df.utils

    for mod in (df.utils, df.logger):
        mod.get_commit_hash = lambda: None
        mod.get_branch_name = lambda: None


print("[MODELO] Cargando DeepFilterNet3…")
_disable_df_git()
_force_df_device("cpu")
_model_cpu, _df_state, _ = init_df(model_base_dir=str(dfn3_dir()), log_level="WARNING", log_file=None)
_model_cpu.eval()
_nb_df = getattr(_model_cpu, "nb_df", getattr(_model_cpu, "df_bins", ModelParams().nb_df))
assert _df_state.sr() == DF_SR

# En ZeroGPU el modelo se coloca en "cuda" a nivel de módulo (emulado fuera de @spaces.GPU).
_model_gpu = None
if ON_ZEROGPU or torch.cuda.is_available():
    try:
        _model_gpu = copy.deepcopy(_model_cpu).to("cuda").eval()
    except Exception as e:  # sin CUDA real ni emulada: solo CPU
        print(f"[MODELO] GPU no disponible: {e}")
print(f"[MODELO] Listo · ZeroGPU={ON_ZEROGPU} · GPU={'sí' if _model_gpu is not None else 'no'}")


@torch.no_grad()
def _enhance_block(model, audio: np.ndarray, atten_db: float | None, device: str) -> np.ndarray:
    """Equivalente a df.enhance.enhance() pero con dispositivo explícito. audio: (C, T) float32 a 48 kHz."""
    x = torch.from_numpy(np.ascontiguousarray(audio, dtype=np.float32))
    orig_len = x.shape[-1]
    n_fft, hop = _df_state.fft_size(), _df_state.hop_size()
    x = F.pad(x, (0, n_fft))  # compensa el retardo del STFT
    spec, erb_feat, spec_feat = df_features(x, _df_state, _nb_df, device=device)
    enhanced = as_complex(model(spec.clone(), erb_feat, spec_feat)[0].cpu().squeeze(1))
    if atten_db is not None and atten_db > 0:
        lim = 10 ** (-atten_db / 20)
        enhanced = as_complex(spec.squeeze(1).cpu()) * lim + enhanced * (1 - lim)
    out = _df_state.synthesis(enhanced.numpy())
    d = n_fft - hop
    return np.asarray(out[:, d : orig_len + d], dtype=np.float32)


def _plan_blocks(n: int) -> list[tuple[int, int, int, int]]:
    """Bloques (ini_ctx, fin_ctx, ini_util, fin_util) en muestras."""
    chunk, ctx = int(CHUNK_SEC * DF_SR), int(CONTEXT_SEC * DF_SR)
    blocks, s = [], 0
    while s < n:
        e = min(n, s + chunk)
        if n - e < chunk // 4:  # no dejar un último bloque diminuto
            e = n
        blocks.append((max(0, s - ctx), min(n, e + ctx), s, e))
        s = e
    return blocks


def _run_blocks(model, pieces: list[np.ndarray], atten_db, device) -> list[np.ndarray]:
    return [_enhance_block(model, p, atten_db, device) for p in pieces]


def _gpu_duration(pieces, atten_db) -> int:
    seconds = sum(p.shape[-1] for p in pieces) / DF_SR
    return int(min(120, 15 + seconds * 0.15))


def _gpu_decorator(fn):
    return spaces.GPU(duration=_gpu_duration)(fn) if spaces is not None else fn


@_gpu_decorator
def _run_blocks_gpu(pieces: list[np.ndarray], atten_db) -> list[np.ndarray]:
    return _run_blocks(_model_gpu, pieces, atten_db, "cuda")


def enhance_48k(audio: np.ndarray, atten_db: float | None, use_gpu: bool = True, progress=None):
    """
    Reduce el ruido de `audio` (C, T) float32 a 48 kHz.
    atten_db: reducción máxima en dB (None = sin límite).
    Devuelve (audio_procesado, info) donde info indica dispositivo y avisos.
    """
    n = audio.shape[-1]
    blocks = _plan_blocks(n)
    xf = int(XFADE_SEC * DF_SR)
    out = np.zeros_like(audio, dtype=np.float32)
    weight = np.zeros(n, dtype=np.float32)

    # Agrupa bloques para que cada llamada a la GPU sea corta.
    per_group = max(1, int(GROUP_SEC // CHUNK_SEC))
    groups = [blocks[i : i + per_group] for i in range(0, len(blocks), per_group)]

    global _gpu_blocked_until
    gpu_ok = (use_gpu and _model_gpu is not None and time.time() >= _gpu_blocked_until
              and (not ON_ZEROGPU or n / DF_SR >= GPU_MIN_SEC))
    devices, notes = set(), []
    for gi, group in enumerate(groups):
        pieces = [audio[:, a:b] for a, b, _, _ in group]
        results = None
        if gpu_ok:
            try:
                results = _run_blocks_gpu(pieces, atten_db)
                devices.add("GPU")
            except Exception as e:
                gpu_ok = False  # si falla una vez (p. ej. cuota), el resto va por CPU
                _gpu_blocked_until = time.time() + GPU_COOLDOWN_SEC
                print(f"[GPU] No disponible, se sigue en CPU: {str(e).splitlines()[0][:200]}")
                notes.append("La tarjeta gráfica no estaba libre: se ha hecho con el procesador (mismo resultado)")
        if results is None:
            results = _run_blocks(_model_cpu, pieces, atten_db, "cpu")
            devices.add("CPU")

        for (a, b, s, e), y in zip(group, results):
            lo, hi = max(0, s - xf), min(n, e + xf)
            w = np.ones(hi - lo, dtype=np.float32)
            if s > 0:  # rampa de entrada centrada en s
                w[: 2 * xf] = np.linspace(0, 1, 2 * xf, dtype=np.float32)[: min(2 * xf, hi - lo)]
            if e < n:  # rampa de salida centrada en e
                w[-2 * xf :] = np.minimum(w[-2 * xf :], np.linspace(1, 0, 2 * xf, dtype=np.float32))
            out[:, lo:hi] += y[:, lo - a : hi - a] * w
            weight[lo:hi] += w
        if progress is not None:
            progress((gi + 1) / len(groups))

    out /= np.maximum(weight, 1e-6)
    return out, {"dispositivo": "+".join(sorted(devices)) or "—", "avisos": notes}


def gpu_available() -> bool:
    return _model_gpu is not None