File size: 7,089 Bytes
63dd946 | 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 | # Copyright (c) 2026 The Anuvani-Diarization authors
# SPDX-License-Identifier: MIT OR Apache-2.0
"""Diarization in one line, at full speed.
from anuvani_diar import Diarizer
diarize = Diarizer() # every visible GPU (or the CPU); Diarizer(devices=2): two GPUs
result = diarize("meeting.wav") # -> Diarization
result.save_rttm("meeting.rttm")
for r in diarize.run(["a.wav", "b.flac"]): # many files; results arrive as each one finishes
print(r.uri, r.speakers)
Everything except the two networks runs in the Rust engine (`anuvani_diar._core`), on its own threads: decoding,
windowing, batching across all GPUs, masks, clustering, the decoder and the RTTM text. The output is the release's
(the original program, `reference/diarize.py` in the model repository): byte-identical on the setup the release's
outputs were produced with, and near-identical, checked at start-up, on any other GPU or PyTorch version.
"""
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Iterable, Iterator, List, Optional, Tuple, Union
from . import _core
_HERE = Path(__file__).resolve().parent
def default_bundle() -> Path:
"""`$ANUVANI_DIAR_BUNDLE`; else, in a source tree, the bundle exported beside it (artifacts/bundle, made by
tools/export_bundle.py from the release's checkpoints: safetensors, the Indic model only); else the model folder of
the Hugging Face repository, downloaded once and sha256-checked (anuvani_diar.hub)."""
env = os.environ.get("ANUVANI_DIAR_BUNDLE")
if env:
return Path(env)
p = _HERE.parents[1] / "artifacts" / "bundle"
if (p / "MANIFEST.json").is_file():
return p
from .hub import bundle_dir
return bundle_dir()
def devices_arg(text: str) -> Union[str, int]:
"""A --devices value as `Diarizer(devices=...)` takes it: a bare number is a count of GPUs."""
text = text.strip()
return int(text) if text.isdigit() else text
def default_golden() -> Optional[Path]:
env = os.environ.get("ANUVANI_DIAR_GOLDEN")
if env:
return Path(env)
from .hub import assets_dir
p = assets_dir() / "nn_golden.json"
return p if p.is_file() else None
@dataclass
class Diarization:
"""One file's result. `rttm` is the exact text the release writes; `segments` are read back from it."""
uri: str
path: str
rttm: Optional[str]
error: Optional[str] = None
duration_s: Optional[float] = None
timing: dict = field(default_factory=dict)
record: Optional[dict] = None
index: int = -1
@property
def ok(self) -> bool:
return self.error is None and self.rttm is not None
@property
def segments(self) -> List[Tuple[float, float, str]]:
"""(start, end, speaker) per RTTM line, in the file's order."""
out = []
for line in (self.rttm or "").splitlines():
f = line.split()
out.append((float(f[3]), float(f[3]) + float(f[4]), f[7]))
return out
@property
def speakers(self) -> List[str]:
return sorted({s for _, _, s in self.segments})
def save_rttm(self, path: Union[str, Path]) -> Path:
if not self.ok:
raise RuntimeError(self.error or f"no output for {self.path}")
path = Path(path)
path.write_bytes(self.rttm.encode())
return path
class Diarizer:
"""The release's diarization.
devices: where the networks run. "auto" (default): every visible GPU, or the CPU when there is none; a number N:
the first N GPUs; a list of GPU indices or device names ([0, 3], ["cuda:1"], "0,3"); or "cpu". The engine
spreads every file's windows over all of them.
workers_per_device: kept at 1; one CUDA stream per GPU is what keeps the outputs deterministic.
precision: "exact" (default: the release's FP32 networks and settings), or a faster reduced-precision preset whose
outputs differ slightly: "ffn16", "tf32conv" and "mixed"; "fp32", "tf32" and "fp16" are for measurement only.
strict: byte-identical outputs need the setup the release's outputs were produced with. On any other GPU or
PyTorch version, with strict=False (default), the exact lane runs if its start-up self-test stays within
near-identical bounds of the release's outputs (a warning says so, and `info()["exactness"]` gives the numbers);
strict=True refuses to start instead.
"""
def __init__(self, bundle: Union[str, Path, None] = None, devices: Union[str, int, List, None] = "auto",
workers_per_device: int = 1, threads: int = 2, repack: bool = True, keep_records: bool = False,
decode_threads: int = 4, post_threads: Optional[int] = None, max_ready: Optional[int] = None, golden=None,
precision: str = "exact", strict: bool = False):
from .nn.torch_backend import TorchNN
self.bundle = Path(bundle) if bundle else default_bundle()
manifest = json.loads((self.bundle / "MANIFEST.json").read_text())
self.manifest = manifest
model = manifest["modes"]["indic"]
if golden is None:
golden = default_golden()
if golden is None:
raise FileNotFoundError(
"assets/nn_golden.json not found: every GPU checks the networks against the release's own outputs "
"before use. Set ANUVANI_DIAR_GOLDEN to the file (or pass golden=False to run without the check).")
elif golden is False:
golden = None
self.nn = TorchNN(self.bundle, devices, workers_per_device, threads, golden=golden, precision=precision,
strict=strict)
self.engine = _core.Engine(
self.nn, clustering=model["clustering"], decoder_path=str(self.bundle / model["decoder"]),
decode_threads=decode_threads, post_threads=post_threads or max(4, (os.cpu_count() or 8) // 2),
repack=repack, max_ready=max_ready, keep_records=keep_records)
def __call__(self, path: Union[str, Path]) -> Diarization:
return next(iter(self.run([path])))
def run(self, paths: Iterable[Union[str, Path]], out_dir: Union[str, Path, None] = None) -> Iterator[Diarization]:
"""Diarise many files; yields results in completion order. With `out_dir`, also writes `<stem>.rttm` there."""
paths = [Path(p) for p in paths]
jobs = [(p.stem, str(p), str(p)) for p in paths]
out = Path(out_dir) if out_dir else None
for r in self.engine.run(jobs):
d = Diarization(uri=r["uri"], path=r["name"], rttm=r["rttm"], error=r["error"], duration_s=r["duration_s"],
timing=r["timing"], record=r.get("record"), index=r["index"])
if out is not None and d.ok:
out.mkdir(parents=True, exist_ok=True)
d.save_rttm(out / f"{d.uri}.rttm")
yield d
def info(self) -> dict:
return dict(self.nn.info(), version=self.manifest["version"], bundle=str(self.bundle))
|