StephennFernandes's picture
Anuvani-Diarization 1.0.0
63dd946
Raw History Blame Contribute Delete
3.12 kB
# Copyright (c) 2026 The Anuvani-Diarization authors
# SPDX-License-Identifier: MIT OR Apache-2.0
"""Cannot-link cluster repair for VBx (EEND-VC-style constraint; vendored verbatim)."""
# vendored from the research pipeline's cannotlink.py (sha256 e3cc5fee522f2d38): violations, split_cluster, repair
import numpy as np
def violations(labels, chunk):
"""count of (chunk, cluster) with >=2 train slots, per cluster."""
out = {}
key = chunk.astype(np.int64) * 1000 + labels
u, c = np.unique(key, return_counts=True)
for k, n in zip(u, c):
if n >= 2:
out[int(k % 1000)] = out.get(int(k % 1000), 0) + int(n - 1)
return out
def split_cluster(X, chunk, iters=10):
"""2-way constrained k-means on unit vectors X; members sharing a chunk must separate."""
n = len(X)
if n < 4:
return None
# seed: most dissimilar pair among co-occurring members
best, seed = 2.0, None
by = {}
for i, c in enumerate(chunk):
by.setdefault(int(c), []).append(i)
for idx in by.values():
if len(idx) >= 2:
s = X[idx] @ X[idx].T
a, b = np.unravel_index(np.argmin(s), s.shape)
if s[a, b] < best:
best, seed = s[a, b], (idx[a], idx[b])
if seed is None:
return None
c0, c1 = X[seed[0]].copy(), X[seed[1]].copy()
lab = np.zeros(n, dtype=int)
for _ in range(iters):
s0, s1 = X @ c0, X @ c1
lab = (s1 > s0).astype(int)
# enforce cannot-link inside each chunk: if two members share a chunk and a side, flip the weaker
for idx in by.values():
if len(idx) == 2 and lab[idx[0]] == lab[idx[1]]:
d = [(s1 - s0)[i] for i in idx]
j = idx[int(np.argmin(np.abs(d)))]
lab[j] = 1 - lab[j]
if lab.min() == lab.max():
return None
c0 = X[lab == 0].mean(0); c0 /= np.linalg.norm(c0)
c1 = X[lab == 1].mean(0); c1 /= np.linalg.norm(c1)
return lab, float(c0 @ c1)
def repair(train_labels, train_chunk, Xn, min_viol=3, min_frac=0.05, max_sim=0.6, keep=0.5, max_rounds=8):
"""Return repaired train labels (ints 0..K'-1)."""
lab = train_labels.copy()
for _ in range(max_rounds):
v = violations(lab, train_chunk)
cand = []
for k, nv in v.items():
m = lab == k
if nv >= min_viol and nv / m.sum() >= min_frac:
cand.append((nv, k))
changed = False
for nv, k in sorted(cand, reverse=True):
m = np.flatnonzero(lab == k)
res = split_cluster(Xn[m], train_chunk[m])
if res is None:
continue
sub, sim = res
if sim > max_sim:
continue
new = lab.copy(); new[m[sub == 1]] = lab.max() + 1
v2 = violations(new, train_chunk)
after = v2.get(k, 0) + v2.get(int(lab.max() + 1), 0)
if after <= keep * nv:
lab = new; changed = True
if not changed:
break
_, lab = np.unique(lab, return_inverse=True)
return lab