Spaces:
Running on Zero
Running on Zero
Download profiles.py from Azuredraco/audio-denoise: direct link, hf CLI and curl.
- Browser
- Download file 9.3 kB
-
https://huggingface.co/spaces/Azuredraco/audio-denoise/resolve/main/profiles.py
- Command line
-
hf download hf://spaces/Azuredraco/audio-denoise/profiles.py
-
curl -L -o profiles.py https://huggingface.co/spaces/Azuredraco/audio-denoise/resolve/main/profiles.py
9.3 kB
| """ | |
| profiles.py — Perfiles de especificación (perfiles/*.yaml) y comprobaciones. | |
| Cada comprobación devuelve una fila: parámetro, valor medido, requisito y resultado | |
| (True = cumple, False = no cumple, None = solo informativo). | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import re | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| import yaml | |
| from audio_io import SUBTYPE_LABEL, AudioMeta, fmt_duration | |
| PROFILES_DIR = Path(__file__).parent / "perfiles" | |
| class Check: | |
| parametro: str | |
| medido: str | |
| requisito: str | |
| ok: bool | None | |
| def __post_init__(self): | |
| # Las medidas son floats de numpy y sus comparaciones dan numpy.bool_, que no es True/False: | |
| # `numpy.False_ is False` es falso. Se normaliza aquí para que «cumple / no cumple» sea fiable. | |
| if self.ok is not None: | |
| self.ok = bool(self.ok) | |
| def load_profiles(directory: Path = PROFILES_DIR) -> dict[str, dict]: | |
| profiles = {} | |
| for path in sorted(directory.glob("*.yaml")): | |
| with open(path, encoding="utf-8") as f: | |
| data = yaml.safe_load(f) or {} | |
| data["_id"] = path.stem | |
| profiles[path.stem] = data | |
| return dict(sorted(profiles.items(), key=lambda kv: (kv[1].get("orden", 99), kv[1].get("nombre", kv[0])))) | |
| def _sec(section) -> dict: | |
| return section if isinstance(section, dict) else {} | |
| def fdb(v: float, unit: str = "dBFS") -> str: | |
| if v is None or (isinstance(v, float) and math.isnan(v)): | |
| return "—" | |
| if math.isinf(v): | |
| return f"{'−' if v < 0 else ''}∞ {unit}" | |
| return f"{v:.1f} {unit}".replace("-", "−") | |
| def fsec(v: float) -> str: | |
| return "—" if v is None or math.isnan(v) else f"{v:.2f} s" | |
| def check(meta: AudioMeta, m: dict, prof: dict) -> list[Check]: | |
| fmt, lvl, nz, sil = _sec(prof.get("formato")), _sec(prof.get("niveles")), _sec(prof.get("ruido")), _sec(prof.get("silencios")) | |
| rows: list[Check] = [] | |
| if fmt.get("contenedor"): | |
| rows.append(Check("Formato", meta.container, fmt["contenedor"], meta.container == fmt["contenedor"].upper())) | |
| if fmt.get("pcm_entero") or fmt.get("bits") or fmt.get("bits_min"): | |
| bits = meta.bits or 0 | |
| ok = True | |
| req = [] | |
| if fmt.get("pcm_entero"): | |
| ok &= meta.is_integer_pcm | |
| req.append("PCM entero") | |
| if fmt.get("bits"): | |
| ok &= bits == fmt["bits"] | |
| req.append(f"{fmt['bits']} bits") | |
| elif fmt.get("bits_min"): | |
| ok &= bits >= fmt["bits_min"] | |
| req.append(f"≥ {fmt['bits_min']} bits") | |
| rows.append(Check("Codificación", SUBTYPE_LABEL.get(meta.subtype, meta.subtype), " · ".join(req), ok)) | |
| if fmt.get("frecuencia"): | |
| rows.append(Check("Frecuencia", f"{meta.samplerate} Hz", f"{fmt['frecuencia']} Hz", meta.samplerate == fmt["frecuencia"])) | |
| if fmt.get("canales"): | |
| rows.append(Check("Canales", str(meta.channels), "mono (1)" if fmt["canales"] == 1 else str(fmt["canales"]), | |
| meta.channels == fmt["canales"])) | |
| if lvl.get("pico_max_dbfs") is not None: | |
| rows.append(Check("Pico máximo", fdb(m["pico_dbfs"]), f"≤ {fdb(lvl['pico_max_dbfs'])}", m["pico_dbfs"] <= lvl["pico_max_dbfs"] + 1e-6)) | |
| rv = _sec(lvl.get("rms_voz")) | |
| if rv: | |
| rows.append(Check("RMS de voz", fdb(m["rms_voz_dbfs"]), f"{fdb(rv['min'])} a {fdb(rv['max'])}", | |
| rv["min"] <= m["rms_voz_dbfs"] <= rv["max"])) | |
| if lvl.get("amplitud_voz_min") is not None: | |
| rows.append(Check("Amplitud de la voz", f"{m['pico_voz_muestras']} (de 32767)", | |
| f"> {lvl['amplitud_voz_min']} ({fdb(20 * math.log10(lvl['amplitud_voz_min'] / 32767))})", | |
| m["pico_voz_muestras"] > lvl["amplitud_voz_min"])) | |
| if nz.get("ruido_fondo_max_dbfs") is not None: | |
| rows.append(Check("Ruido de fondo", fdb(m["ruido_fondo_dbfs"]), f"≤ {fdb(nz['ruido_fondo_max_dbfs'])}", | |
| m["ruido_fondo_dbfs"] <= nz["ruido_fondo_max_dbfs"])) | |
| if fmt.get("ancho_banda_min_hz") and "ancho_banda_hz" in m: | |
| bw = m["ancho_banda_hz"] | |
| rows.append(Check("Espectro (agudos)", "—" if math.isnan(bw) else f"hasta {bw / 1000:.1f} kHz", | |
| f"más allá de {fmt['ancho_banda_min_hz'] / 1000:.0f} kHz", | |
| (not math.isnan(bw)) and bw >= fmt["ancho_banda_min_hz"])) | |
| zb = m.get("zumbido") | |
| ztxt = f"{zb['base']} Hz · destaca {zb['db']:.0f} dB" if zb else "no" | |
| if nz.get("zumbido_max_db") is not None: | |
| rows.append(Check("Zumbido eléctrico", ztxt, f"≤ {nz['zumbido_max_db']:.0f} dB (sin zumbido)", | |
| zb is None or zb["db"] <= nz["zumbido_max_db"])) | |
| elif "zumbido" in m: | |
| rows.append(Check("Zumbido eléctrico", ztxt, "informativo", None)) | |
| if nz.get("ruido_relativo_max_db") is not None: | |
| rows.append(Check("Ruido relativo (inicio/fin)", fdb(m["ruido_relativo_db"], "dB"), | |
| f"≤ {fdb(nz['ruido_relativo_max_db'], 'dB')} vs voz", m["ruido_relativo_db"] <= nz["ruido_relativo_max_db"])) | |
| if nz.get("snr_min_db") is not None: | |
| rows.append(Check("Relación señal/ruido (SNR)", fdb(m["snr_db"], "dB"), f"≥ {nz['snr_min_db']:.0f} dB", | |
| m["snr_db"] >= nz["snr_min_db"])) | |
| if sil: | |
| for key, label in (("silencio_ini_s", "Silencio inicial"), ("silencio_fin_s", "Silencio final")): | |
| v = m[key] | |
| rows.append(Check(label, fsec(v), f"{sil['min_s']:.2f} a {sil['max_s']:.2f} s", | |
| (not math.isnan(v)) and sil["min_s"] - 1e-3 <= v <= sil["max_s"] + 1e-3)) | |
| if prof.get("saturacion_max_muestras") is not None: | |
| rows.append(Check("Saturación", f"{m['muestras_saturadas']} muestras", f"≤ {prof['saturacion_max_muestras']}", | |
| m["muestras_saturadas"] <= prof["saturacion_max_muestras"])) | |
| # Informativo | |
| if "pausas_s" in m: | |
| max_p = pause_max(prof) | |
| largas = [p for p in m["pausas_s"] if p > max_p + 0.05] | |
| rows.append(Check(f"Pausas largas (> {max_p:.1f} s)", | |
| f"{len(largas)} · la más larga {max(largas):.1f} s" if largas else "ninguna", | |
| "informativo (se pueden acortar)", None)) | |
| if nz.get("ruido_fondo_max_dbfs") is None: | |
| rows.append(Check("Ruido de fondo", fdb(m["ruido_fondo_dbfs"]), "informativo", None)) | |
| if not nz.get("snr_min_db"): | |
| rows.append(Check("SNR", fdb(m["snr_db"], "dB"), "informativo", None)) | |
| if "lufs" in m: | |
| rows.append(Check("Sonoridad integrada", fdb(m["lufs"], "LUFS"), "informativo", None)) | |
| rows.append(Check("Pico real", fdb(m["pico_real_dbtp"], "dBTP"), "informativo", None)) | |
| if "dnsmos" in m and m["dnsmos"]: | |
| d = m["dnsmos"] | |
| rows.append(Check("Calidad percibida (DNSMOS)", f"{d['OVRL']:.2f} / 5", | |
| f"voz {d['SIG']:.2f} · fondo {d['BAK']:.2f}", None)) | |
| if not m.get("voz_detectada", True): | |
| rows.append(Check("Detección de voz", "no se detecta voz", "—", False)) | |
| return rows | |
| def pause_max(prof: dict) -> float: | |
| """Pausa interna máxima para informar y acortar: la elegida en la app o la del perfil (2 s por defecto).""" | |
| return float(prof.get("_pausa_max_s") or (prof.get("pausas") or {}).get("max_s") or 2.0) | |
| def long_pauses(m: dict, prof: dict) -> list[float]: | |
| return [p for p in m.get("pausas_s", []) if p > pause_max(prof) + 0.05] | |
| def passes(rows: list[Check]) -> bool: | |
| return all(r.ok is not False for r in rows) | |
| def failed(rows: list[Check]) -> list[str]: | |
| return [r.parametro for r in rows if r.ok is False] | |
| # ── Entregables ────────────────────────────────────────────────────────────── | |
| def _norm(name: str) -> str: | |
| stem = Path(name).stem.lower() | |
| return re.sub(r"[\s_\-]+", " ", stem).strip() | |
| def deliverables(prof: dict, files: list[tuple[str, float]]) -> dict | None: | |
| """files: [(nombre, duración_s)]. Devuelve resumen de entregables o None si el perfil no los define.""" | |
| ent = _sec(prof.get("entregables")) | |
| if not ent: | |
| return None | |
| total = sum(d for _, d in files) | |
| out = {"total_s": total, "objetivo_s": ent.get("objetivo_total_s"), "ficheros": []} | |
| by_name = {_norm(n): (n, d) for n, d in files} | |
| for spec in ent.get("ficheros") or []: | |
| found = by_name.get(_norm(spec["nombre"])) | |
| row = {"esperado": spec["nombre"], "fichero": found[0] if found else None, | |
| "duracion_s": found[1] if found else None, "ok": None, "nota": "", "estado": "recibido"} | |
| if not found: | |
| row["ok"], row["nota"], row["estado"] = False, "aún no subido", "pendiente" | |
| elif spec.get("duracion_min_s"): | |
| row["ok"] = found[1] >= spec["duracion_min_s"] - 0.05 | |
| row["nota"] = f"mín. {fmt_duration(spec['duracion_min_s'])}" | |
| if not row["ok"]: | |
| row["estado"] = "corto" | |
| elif spec.get("duracion_aprox_s"): | |
| row["nota"] = f"≈ {fmt_duration(spec['duracion_aprox_s'])}" | |
| out["ficheros"].append(row) | |
| return out | |