Spaces:
Running on Zero
Running on Zero
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
|