File size: 4,236 Bytes
7e8e229
c6a2c55
7e8e229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6a2c55
 
 
 
 
 
 
 
7e8e229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6a2c55
 
 
 
 
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
"""
models.py — Descarga verificada de los modelos (DeepFilterNet3, DNSMOS y Silero VAD).

Cada fichero tiene una fuente principal (repositorio original en GitHub) y una
réplica en Hugging Face fijada a un commit concreto. Se comprueba el SHA-256
siempre que se conoce, así que da igual de dónde venga: o es el fichero bueno
o no se usa.
"""

from __future__ import annotations

import hashlib
import os
import shutil
import tempfile
import time
import urllib.request
import zipfile
from pathlib import Path

CACHE_DIR = Path(os.environ.get("AUDIO_DENOISE_CACHE", Path.home() / ".cache" / "audio-denoise"))

_HF_DFN = "https://huggingface.co/niobures/DeepFilterNet/resolve/563a7658343bc716f2bca304f16e54f403914ce1"
_HF_DNSMOS = "https://huggingface.co/anchor-flux/dnsmos-onnx/resolve/4323f201af8a33969025d4e39b409205f0881237"
_GH_DNSMOS = "https://github.com/microsoft/DNS-Challenge/raw/master/DNSMOS/DNSMOS"

DFN3_ZIP = {
    "name": "DeepFilterNet3.zip",
    "sha256": "49c52edc8947ae1f9bf50d81530beaf3a2c3245aeaf34b6f31ff535cd22284d2",
    "urls": [
        "https://github.com/Rikorose/DeepFilterNet/raw/main/models/DeepFilterNet3.zip",
        f"{_HF_DFN}/models/onnx/DeepFilterNet/DeepFilterNet3.zip",
    ],
}

DNSMOS_FILES = {
    "sig_bak_ovr.onnx": {
        "sha256": "269fbebdb513aa23cddfbb593542ecc540284a91849ac50516870e1ac78f6edd",
        "urls": [f"{_GH_DNSMOS}/sig_bak_ovr.onnx", f"{_HF_DNSMOS}/sig_bak_ovr.onnx"],
    },
}


_HF_SILERO = "https://huggingface.co/onnx-community/silero-vad/resolve/e71cae966052b992a7eca6b17738916ce0eca4ec"
SILERO_VAD = {
    "name": "silero_vad.onnx",
    "sha256": "a4a068cd6cf1ea8355b84327595838ca748ec29a25bc91fc82e6c299ccdc5808",
    "urls": [f"{_HF_SILERO}/onnx/model.onnx"],
}


def _sha256(path: Path) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for block in iter(lambda: f.read(1 << 20), b""):
            h.update(block)
    return h.hexdigest()


def _download(urls: list[str], dest: Path, sha256: str, retries: int = 3) -> Path:
    """Descarga `dest` probando cada URL; valida el hash. Lanza RuntimeError si todo falla."""
    if dest.exists() and _sha256(dest) == sha256:
        return dest
    dest.parent.mkdir(parents=True, exist_ok=True)
    errors = []
    for url in urls:
        for attempt in range(retries):
            try:
                with tempfile.NamedTemporaryFile(dir=dest.parent, delete=False) as tmp:
                    req = urllib.request.Request(url, headers={"User-Agent": "audio-denoise"})
                    with urllib.request.urlopen(req, timeout=60) as r:
                        shutil.copyfileobj(r, tmp)
                tmp_path = Path(tmp.name)
                digest = _sha256(tmp_path)
                if digest != sha256:
                    tmp_path.unlink(missing_ok=True)
                    errors.append(f"{url}: hash distinto ({digest[:12]}…)")
                    break  # reintentar la misma URL no arregla un hash distinto
                tmp_path.replace(dest)
                print(f"[MODELOS] {dest.name} ← {url}")
                return dest
            except Exception as e:  # red, 403, timeout…
                errors.append(f"{url}: {e}")
                time.sleep(2 ** attempt)
    raise RuntimeError(f"No se pudo descargar {dest.name}:\n" + "\n".join(errors))


def dfn3_dir() -> Path:
    """Devuelve la carpeta del modelo DeepFilterNet3 (config.ini + checkpoints/)."""
    model_dir = CACHE_DIR / "DeepFilterNet3"
    if (model_dir / "config.ini").is_file() and (model_dir / "checkpoints").is_dir():
        return model_dir
    zip_path = _download(DFN3_ZIP["urls"], CACHE_DIR / DFN3_ZIP["name"], DFN3_ZIP["sha256"])
    with zipfile.ZipFile(zip_path) as z:
        z.extractall(CACHE_DIR)
    return model_dir


def dnsmos_paths() -> dict[str, Path]:
    """Devuelve {nombre: ruta} de los modelos ONNX de DNSMOS."""
    return {
        name: _download(spec["urls"], CACHE_DIR / "dnsmos" / name, spec["sha256"])
        for name, spec in DNSMOS_FILES.items()
    }


def silero_vad_path() -> Path:
    """Detector de voz Silero VAD v5 (ONNX, licencia MIT)."""
    return _download(SILERO_VAD["urls"], CACHE_DIR / "silero" / SILERO_VAD["name"], SILERO_VAD["sha256"])