File size: 5,462 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 | # Copyright (c) 2026 The Anuvani-Diarization authors
# SPDX-License-Identifier: MIT OR Apache-2.0
"""Build the model bundle `anuvani_diar` loads (no pickles, the Indic model only) from the frozen release bundle.
python -s tools/export_bundle.py --out artifacts/bundle
Layout:
MANIFEST.json version, the model's clustering config and decoder (`modes.indic`), licences, the
sha256 of every file, and the sha256 of the reference checkpoints they were made from
segmentation/config.json SegmentationNet arguments
segmentation/model.safetensors the release's hub/pytorch_model.bin state dict, tensors unchanged
voiceprint/model.safetensors the release's embedding/pytorch_model.bin Lightning state dict, tensors unchanged
decoder.json the release's decoder, without its internal recipe label
Every tensor is checked equal (bit for bit, same dtype and shape) to the release checkpoint it came from, and each new
network is loaded with strict=True.
"""
import argparse
import hashlib
import json
import sys
from pathlib import Path
import torch
from safetensors.torch import load_file, save_file
PROJ = Path(__file__).resolve().parents[1]
REL = PROJ.parent / "anuvani_diar_v1" / "bundle"
# the published model's version (the release's checkpoints, unchanged)
MODEL_VERSION = "1.0.0"
SEG_ARGS = dict(wavlm_src="wavlm_large_s80_md", wavlm_layer_num=25, wavlm_feat_dim=1024, attention_in=256,
ffn_hidden=1024, num_head=4, num_layer=4, kernel_size=31, dropout=0.1, use_posi=False,
output_activate_function=False, num_classes=16)
def sha256(p):
h = hashlib.sha256()
with open(p, "rb") as f:
for b in iter(lambda: f.read(1 << 24), b""):
h.update(b)
return h.hexdigest()
def same(a, b):
return a.dtype == b.dtype and a.shape == b.shape and torch.equal(a.view(torch.uint8) if a.dtype == torch.bool else a, b)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", type=Path, required=True)
a = ap.parse_args()
sys.path.insert(0, str(PROJ / "python"))
from anuvani_diar.nn.seg import SegmentationNet
from anuvani_diar.nn.voiceprint import VoiceprintNet
rel = json.loads((REL / "MANIFEST.json").read_text())
for f, want in rel["files"].items():
if sha256(REL / f) != want:
raise SystemExit(f"release bundle file {f} does not match its MANIFEST sha256")
out = a.out
(out / "segmentation").mkdir(parents=True, exist_ok=True)
(out / "voiceprint").mkdir(parents=True, exist_ok=True)
# a plain state dict (599 tensors)
seg_state = torch.load(REL / "hub/pytorch_model.bin", map_location="cpu")
save_file({k: v.contiguous() for k, v in seg_state.items()}, str(out / "segmentation/model.safetensors"),
metadata={"format": "pt", "source": "reference/bundle/hub/pytorch_model.bin"})
(out / "segmentation/config.json").write_text(json.dumps(dict(architecture="SegmentationNet", model_args=SEG_ARGS), indent=1) + "\n")
ck = torch.load(REL / "embedding/pytorch_model.bin", map_location="cpu")
vp_state = {k: v for k, v in ck["state_dict"].items()}
save_file({k: v.contiguous() for k, v in vp_state.items()}, str(out / "voiceprint/model.safetensors"),
metadata={"format": "pt", "source": "reference/bundle/embedding/pytorch_model.bin (pyannote/wespeaker-voxceleb-resnet34-LM)"})
decoder = (REL / rel["modes"]["indic"]["decoder"]).read_text()
label = '{"recipe": "cd1", '
if not decoder.startswith(label):
raise SystemExit("the release's decoder does not start with its recipe label")
(out / "decoder.json").write_text("{" + decoder[len(label):])
if json.loads((out / "decoder.json").read_text()) != {k: v for k, v in json.loads(decoder).items() if k != "recipe"}:
raise SystemExit("decoder.json differs from the release's decoder beyond the recipe label")
# every tensor round-trips bit for bit, and both networks load strictly
for name, state in (("segmentation", seg_state), ("voiceprint", vp_state)):
back = load_file(str(out / name / "model.safetensors"))
assert back.keys() == state.keys(), name
bad = [k for k in state if not same(back[k], state[k])]
assert not bad, (name, bad[:5])
seg = SegmentationNet.from_pretrained(out / "segmentation")
vp = VoiceprintNet.from_pretrained(out / "voiceprint")
n_seg = sum(p.numel() for p in seg.parameters())
n_vp = sum(p.numel() for p in vp.parameters())
files = {}
for p in sorted(out.rglob("*")):
if p.is_file() and p.name != "MANIFEST.json":
files[str(p.relative_to(out))] = sha256(p)
manifest = dict(
version=MODEL_VERSION, model="Anuvani-Diarization",
modes={"indic": dict(clustering=rel["modes"]["indic"]["clustering"], decoder="decoder.json")},
networks=dict(segmentation=dict(dir="segmentation", parameters=n_seg, source_sha256=rel["files"]["hub/pytorch_model.bin"]),
voiceprint=dict(dir="voiceprint", parameters=n_vp, source_sha256=rel["files"]["embedding/pytorch_model.bin"])),
licence=rel["licence"], files=files)
(out / "MANIFEST.json").write_text(json.dumps(manifest, indent=1) + "\n")
print(json.dumps(dict(out=str(out), segmentation_parameters=n_seg, voiceprint_parameters=n_vp, files=files), indent=1))
if __name__ == "__main__":
main()
|