Download src/python/anuvani_diar/api.py from CIIL/Anuvani-Diarization: direct link, hf CLI and curl.
- Browser
- Download file 7.09 kB
-
https://huggingface.co/CIIL/Anuvani-Diarization/resolve/main/src/python/anuvani_diar/api.py
- Command line
-
hf download hf://CIIL/Anuvani-Diarization/src/python/anuvani_diar/api.py
-
curl -L -o api.py https://huggingface.co/CIIL/Anuvani-Diarization/resolve/main/src/python/anuvani_diar/api.py
7.09 kB
| # 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 | |
| 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 | |
| def ok(self) -> bool: | |
| return self.error is None and self.rttm is not None | |
| 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 | |
| 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)) | |