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()