File size: 3,163 Bytes
7e8e229
fc527e7
7e8e229
 
 
 
 
 
fc527e7
 
7e8e229
 
 
 
 
fc527e7
7e8e229
 
 
 
 
 
 
 
fc527e7
7e8e229
 
 
 
 
 
 
 
 
 
 
 
 
 
fc527e7
 
 
 
7e8e229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fc527e7
7e8e229
fc527e7
7e8e229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fc527e7
7e8e229
 
 
fc527e7
7e8e229
 
 
 
 
 
 
 
 
 
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
"""
dnsmos.py — Calidad percibida de la voz con DNSMOS P.835 (Microsoft).

Estima en una escala 1–5 lo que puntuarían oyentes humanos:
  SIG  = calidad de la voz (¿suena natural, sin distorsión?)
  BAK  = fondo (¿se oye ruido?)
  OVRL = valoración global
Trabaja a 16 kHz: evalúa la voz, no los agudos extremos.
Para que sea rápido en CPUs modestas se evalúan como mucho MAX_WINDOWS ventanas de 9 s
repartidas por el audio (la media se estabiliza con pocas).
Basado en DNSMOS/dnsmos_local.py del repositorio microsoft/DNS-Challenge.
"""

from __future__ import annotations

import os
import threading

import numpy as np

from models import dnsmos_paths

SR = 16000
WIN_SEC = 9.01
MAX_WINDOWS = 10

_lock = threading.Lock()
_sessions = None


def _load():
    global _sessions
    with _lock:
        if _sessions is None:
            import onnxruntime as ort

            paths = dnsmos_paths()
            opts = ort.SessionOptions()
            opts.log_severity_level = 3
            # En contenedores, onnxruntime puede lanzar más hilos que núcleos reales y ralentizarse.
            opts.intra_op_num_threads = max(1, min(4, os.cpu_count() or 1))
            opts.inter_op_num_threads = 1
            _sessions = ort.InferenceSession(str(paths["sig_bak_ovr.onnx"]), opts, providers=["CPUExecutionProvider"])
    return _sessions


def available() -> bool:
    try:
        _load()
        return True
    except Exception as e:
        print(f"[DNSMOS] No disponible: {e}")
        return False


def _polyfit(sig, bak, ovr):
    p_ovr = np.poly1d([-0.06766283, 1.11546468, 0.04602535])
    p_sig = np.poly1d([-0.08397278, 1.22083953, 0.0052439])
    p_bak = np.poly1d([-0.13166888, 1.60915514, -0.39604546])
    return p_sig(sig), p_bak(bak), p_ovr(ovr)


def score(mono: np.ndarray, sr: int) -> dict[str, float] | None:
    """Devuelve {'SIG','BAK','OVRL'} o None si el modelo no está disponible."""
    try:
        sess = _load()
    except Exception as e:
        print(f"[DNSMOS] No disponible: {e}")
        return None
    import soxr

    audio = soxr.resample(np.asarray(mono, dtype=np.float32), sr, SR, quality="HQ") if sr != SR else mono
    audio = np.asarray(audio, dtype=np.float32)
    if not np.any(audio):
        return None
    win = int(WIN_SEC * SR)
    while len(audio) < win:
        audio = np.concatenate([audio, audio])
    n_hops = int(np.floor(len(audio) / SR) - WIN_SEC) + 1
    starts = np.arange(n_hops) * SR
    if len(starts) > MAX_WINDOWS:
        starts = starts[np.linspace(0, len(starts) - 1, MAX_WINDOWS).round().astype(int)]
    rows = []
    for s in starts:
        seg = audio[int(s) : int(s) + win]
        if len(seg) < win:
            continue
        sig, bak, ovr = sess.run(None, {"input_1": seg[None, :]})[0][0]
        rows.append(_polyfit(sig, bak, ovr))
    if not rows:
        return None
    m = np.mean(rows, axis=0)
    return {"SIG": float(m[0]), "BAK": float(m[1]), "OVRL": float(m[2])}


def label(ovrl: float) -> str:
    if ovrl >= 3.6:
        return "excelente"
    if ovrl >= 3.2:
        return "buena"
    if ovrl >= 2.7:
        return "aceptable"
    return "mejorable"