File size: 12,650 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
152
153
154
155
156
157
158
159
# The wheel: anuvani_diar as one self-contained manylinux package

`tools/build_wheel.sh` builds `anuvani_diar-<version>-cp310-abi3-manylinux_2_28_x86_64.whl`: one wheel for CPython
3.10 and later (abi3) on x86_64 Linux with glibc 2.28 or newer. It carries the Rust extension, the Python package, the
four native libraries the engine loads at run time, the licence texts and the clustering assets, so the CPU side
needs nothing from the build machine.

Release 1.0.0 was built on 10 October 2026 from commit 2ce08e60 (clean tree): 14,910,918 bytes, 245 files (189 of them
licence files in `.dist-info/licenses/`), sha256 `84d7b5e71eec083a3835f0cc3340f846f3e03562cf2ed6f054476f4107fdd3b1`;
the extension `_core.abi3.so` is 2,571,976 bytes after stripping. The first build (9 October, commit 7c648240, 36
files) was 13,657,026 bytes. Each build writes `<wheel>.build.json` (inputs, versions, checks) and each check
`<wheel>.check.json` beside the wheel.

## What is bundled

`anuvani_diar/_libs/`: unmodified copies of the binaries the release's own Python stack uses, sha256-checked when staged,
in the built wheel, and again by the engine before it loads them.

| File | What, from where | sha256 | Licence (text in `_libs/LICENSES/`) |
|---|---|---|---|
| `libopenblas64_p-r0-0cf96a72.3.23.dev.so` | OpenBLAS 0.3.23.dev, ILP64, with LAPACK; numpy 1.26.4 wheel (`numpy.libs/`) | `9254d085…` | BSD-3-Clause (LAPACK: BSD-3-Clause-Attribution); `numpy-1.26.4-LICENSE.txt` |
| `libgfortran-040039e1.so.5.0.0` | GCC Fortran runtime; same wheel | `14afb312…` | GPL-3.0 with the GCC Runtime Library Exception 3.1; same file (both full texts) |
| `libquadmath-96973f99.so.0.0.0` | GCC quad-precision maths; same wheel | `934c22de…` | LGPL-2.1-or-later; same file, full text in `libsndfile-1.2.2-COPYING.txt` |
| `libsndfile_x86_64.so` | libsndfile 1.2.2 with libogg 1.3, libvorbis 1.3.7, FLAC 1.4.3, opus 1.4, mpg123 1.32.3, LAME 3.100 linked in; soundfile 0.14.0 wheel (`_soundfile_data/`) | `c872fb84…` | LGPL-2.1 (`libsndfile-1.2.2-COPYING.txt`, soundfile's copy, identical to libsndfile's own); codecs: BSD-3-Clause (ogg, vorbis, FLAC, opus), LGPL-2.1 (mpg123), LGPL-2.0-or-later (LAME), one file each |

`_libs/LICENSES/README.txt` says which file covers which library and where each comes from. The numpy and soundfile
texts are copied from the reference environment; the codec texts (`tools/wheel_licences/`) were taken once from each
project's source at the version in the binary (FLAC, Vorbis, Opus and mpg123 report their versions in the binary;
LAME's is pinned by the libsndfile-binaries build script; libogg's COPYING is the same in 1.3.4, 1.3.5 and 1.3.6).

`anuvani_diar/assets/`: the PLDA and x-vector arrays (`*.bin`), `MANIFEST.json` (their sha256), `nn_golden.json`,
`long_golden.json`; about 270 KB.

**Staged, not committed.** Both folders are written by `tools/stage_wheel_files.py` at build time and are git-ignored.
The libraries are 41.7 MB of third-party binaries (and must not be in git); the assets already live in `assets/`, where
the Rust tests and the gateway read them, and a second committed copy could drift. The script copies the libraries from
the reference environment's site-packages and the codec licence texts from `tools/wheel_licences/`, checks every file
against `tools/wheel_files.json` (the committed sha256 manifest) and the arrays against `assets/MANIFEST.json`, and
leaves exactly the listed files. `make build`, `make test-rust` and `tools/build_wheel.sh` run it.

## How the engine finds them

`anuvani_diar/__init__.py` sets three variables to the package's own copies before `_core` loads, unless they are set
(non-empty) already:

| Variable | Default (set by the package) | Used by |
|---|---|---|
| `ANUVANI_DIAR_OPENBLAS_DIR` | `<package>/_libs` | `crates/diar-exact/src/blas.rs` |
| `ANUVANI_DIAR_SNDFILE` | `<package>/_libs/libsndfile_x86_64.so` | `crates/diar-audio/src/sndfile.rs` |
| `ANUVANI_DIAR_ASSETS` | `<package>/assets` | `crates/diar-core/src/assets.rs` |
| `ANUVANI_DIAR_SNDFILE_UNCHECKED` | unset | `1`: load the libsndfile named by `ANUVANI_DIAR_SNDFILE` without the sha256 and version checks |

No path is compiled into `_core`. Without the variables (`cargo test`, the gateway under `target/`, a source checkout)
the loaders fall back, for development only, to the source tree found at run time (from cargo's `CARGO_MANIFEST_DIR`,
the running executable, or the extension's own file): its staged `python/anuvani_diar/_libs` and its `assets/`.

**OpenBLAS.** The three files must have their sha256 before anything is loaded. OpenBLAS reads its settings from the
environment while it loads, so the loader sets `OPENBLAS_NUM_THREADS=1` and, if the CPU reports AVX2 and FMA,
`OPENBLAS_CORETYPE=Zen` just before the first `dlmopen`, restores both afterwards, and then requires 1 thread and core
"Zen". On a CPU without AVX2 or FMA the core is not forced and loading fails with "exact mode unavailable" (the Zen
kernels need both; other kernels would not reproduce the release bit for bit). The private namespace keeps its own
copy of the environment, so a later `setenv` elsewhere cannot leave it reading freed memory. If glibc has no static TLS
space left for the namespace ("cannot allocate memory in static TLS block"), the error says so and names the remedy:
start the process with `GLIBC_TUNABLES=glibc.rtld.optional_static_tls=4096`, or load the engine's OpenBLAS first
(`anuvani_diar._core.openblas_info()`). `_core.openblas_info()` reports the file, threads, core and whether the core
was forced; `_core.sndfile_info()` the libsndfile file, version and whether it passed the checks.

**libsndfile.** Checked by sha256 and version (`libsndfile-1.2.2`). The LGPL lets users replace it:
`ANUVANI_DIAR_SNDFILE=/path/to/libsndfile.so.1 ANUVANI_DIAR_SNDFILE_UNCHECKED=1` loads another build, any version,
and the engine says on stderr that decoding is no longer claimed to be identical to the release's.

## Building

```bash
# about 20 s once the cache is warm; the first build also fetches the toolchain and crates
tools/build_wheel.sh
```

Pinned: the image `quay.io/pypa/manylinux_2_28_x86_64@sha256:39df0042d5cc900b085aa25a0659368b42a0006c54c474299b785b44c1b4ff82`
(AlmaLinux 8, glibc 2.28; pulled 9 October 2026, created 3 October 2026), rustup-init 1.29.1 (by sha256), rustc 1.94.1
(minimal profile), maturin 1.15.0, the crates by `Cargo.lock` (`--locked`), and file times by `SOURCE_DATE_EPOCH`
(the commit's time). The build context is the tracked build files plus the staged ones, copied to
`artifacts/wheel_build/`; the target directory starts empty. maturin runs with `--strip`, every source path remapped
(`/build/anuvani_diar_rs` to `/anuvani_diar_rs`, the crate registry to `/cargo`), `--compatibility manylinux_2_28` and
`--auditwheel check` (it verifies the tag and changes nothing). `dlmopen` and `dlinfo` are declared with
`#[link(name = "dl")]` in `blas.rs`, so on glibc 2.28 they come from `libdl.so.2`; on newer glibc the link finds only
an empty `libdl.a` and nothing changes. Strip and remapping are set in the script, not in `pyproject.toml`, so that
`maturin develop` keeps its symbols. `pyproject.toml` names the staged folders in `include` (maturin leaves git-ignored
files out otherwise). Caches (rustup, cargo, maturin) stay in `artifacts/wheel_build/cache`.

**Reproducible:** two builds from empty target directories at the same commit gave the same wheel, byte for byte
(`acd8a022…` twice, 9 October 2026); a new commit changes only the file times.

**Checks made by the build** (all passed for the wheel above):

- the bundled files in the wheel have the sha256 of `tools/wheel_files.json` and `assets/MANIFEST.json`;
- `strings` on `_core.abi3.so`: no path of the build machine or of the build. What remains is the Rust toolchain's own
  (`/rustc/<hash>/library/...`, `/rust/deps/...`, `/checkout/src`), the remapped registry (`/cargo/registry/src/...`),
  the project's relative `crates/...` and system files the standard library reads (`/proc/self/...`, `/dev/urandom`);
- `_core` needs glibc symbols up to 2.28 and only `libdl.so.2`, `libgcc_s.so.1`, `libpthread.so.0`, `libm.so.6`,
  `libc.so.6`, `ld-linux-x86-64.so.2`;
- `auditwheel show` (never `repair`, which would rewrite the bundled libraries and break their sha256): the symbol
  versions allow `manylinux_2_28_x86_64`, but its overall verdict is `linux_x86_64`, for one reason: OpenBLAS needs
  `libgfortran-040039e1.so.5.0.0` and has no RPATH, so auditwheel cannot see that the file is in the wheel (the engine
  loads it first, by path, into the same namespace). Every other library the bundled files need is on the manylinux
  list (`libz.so.1`, `libmvec.so.1`, `libgcc_s.so.1`, glibc's). Making auditwheel agree would mean editing OpenBLAS.
  The SBOM maturin adds (`.dist-info/sboms/diar-py.cyclonedx.json`) names the build-container path
  `/build/anuvani_diar_rs`, not a host path.

## What was tested

`tools/wheel_check.py` (CPU only, `CUDA_VISIBLE_DEVICES` empty), on this wheel, 9 October 2026: **PASS in 32.6 s**
((a) and (b) side by side; 42.9 s on the first run, which also fetched Python 3.12 and torch).

(a) In `.venv` with the canonical environment, the existing tests that cover the bundled libraries and assets:
decoding against soundfile over every format and 2,750 damaged files, the release's `load_audio`, numpy's memory-error
text, the VBx row-parallel products against numpy's own (shapes up to 5,000 rows), the engine with every BLAS thread
variable at 64, and 40 cached records' RTTMs against the canonical oracle through `diarize_record` and through the
engine: **121 passed** (24 of the 20,774-row product shapes left to `make test-fast`). From a git worktree the main
checkout's untracked data are bound read-only over the worktree's in a private mount namespace.

(b) The wheel in a fresh venv: Python 3.12.11, torch 2.11.0+cpu, numpy 2.5.3, no variable of `env/canonical.sh` and
no `ANUVANI_DIAR_*` variable set; torch and numpy imported before `anuvani_diar`.

| Check | Result |
|---|---|
| private OpenBLAS | from the package's `_libs`; 1 thread, core Zen (forced), 0 threads started by the load (0.075 s) |
| libsndfile | from the package's `_libs`; `libsndfile-1.2.2`, checks passed |
| bengali_009.flac, telugu_020.flac (golden inputs, 3,200,512 samples each at 16 kHz) | sha256 of the decoded float32 samples identical to soundfile 0.14.0's in the reference environment |
| long_1h record, 2,241 chunks | RTTM sha256 `0f74418f…` = `assets/long_golden.json` (1.07 s) |
| long_3h record, 6,741 chunks | `982bbb38…` = golden (6.24 s) |
| long_6h record, 13,491 chunks | `651635aa…` = golden (19.30 s) |

Also checked while making the change (before and after it, on the build machine):

- the whole `make test-fast` from this worktree (data bound read-only as above), at commit 5ed8a2fd: 462 passed in
  7 min 23 s, the Rust tests included;
- the same test files without the 106 s channel-mean property test and the slow sets: 247 passed before the change
  and 247 after;
- `cargo test --workspace`: all pass, with new tests for the hash refusal (OpenBLAS and libsndfile), the static-TLS
  message, the core pin (forcing Haswell shows up as core "Haswell", so the pin reaches OpenBLAS), the namespace's own
  environment copy, and the source-tree fallback (also from a test binary run with an empty environment);
- loaded without any OpenBLAS variable set, the private OpenBLAS starts no thread and leaves both variables unset
  again; the same binary loaded without the pin (numpy importing it) starts 63 extra threads on this 64-CPU host;
- static TLS used up on purpose (`glibc.rtld.nns=1`, `optional_static_tls=0`, two libgomp copies loaded first): the
  readable error appears; with `optional_static_tls=4096` the same process loads;
- two gateway tests with the stand-in Triton (every RTTM against the release, the resample limit).

## Not covered here

- The networks: since 9 October 2026 the wheel declares torch, safetensors and huggingface_hub as well (the networks,
  their weights, the download).
- Licences: the code is MIT OR Apache-2.0 (decided 10 October 2026). The wheel carries `LICENSE-MIT`,
  `LICENSE-APACHE`, `THIRD_PARTY_NOTICES.md` and the licence files of every Rust crate compiled into `_core`
  (`LICENSES/crates/`, from `tools/third_party_notices.py`) in its `.dist-info/licenses/`, beside the bundled
  libraries' texts in `_libs/LICENSES/`. The source of the LGPL libraries is in the model repository's
  `third_party_sources/`.
- Other CPUs and older glibc: tested on one AVX2 CPU with glibc 2.35 only.