import warnings, json, sys
warnings.filterwarnings("ignore")
import numpy as np, soundfile as sf, torch
from sklearn.cluster import AgglomerativeClustering
from speechbrain.inference.speaker import EncoderClassifier
VP="<the desk's voiceprint folder>"  # local path removed for publication
enc=EncoderClassifier.from_hparams(source="speechbrain/spkrec-ecapa-voxceleb", run_opts={"device":"cpu"}, savedir=VP+"/.ecapa_model")
SR=16000
def emb_windows(path, win=6.0, hop=3.0):
    w,sr=sf.read(path,dtype="float32"); 
    if w.ndim>1: w=w.mean(1)
    W,H=int(win*SR),int(hop*SR); out=[]
    for s in range(0,max(1,len(w)-W+1),H):
        seg=w[s:s+W]
        if len(seg)<3*SR or np.abs(seg).mean()<0.002: continue
        with torch.no_grad(): e=enc.encode_batch(torch.tensor(seg).unsqueeze(0)).squeeze().cpu().numpy()
        out.append(e/(np.linalg.norm(e)+1e-9))
    return np.stack(out) if out else np.zeros((0,192))
def dominant(E, thresh=0.55):
    if len(E)<3: return E.mean(0)/ (np.linalg.norm(E.mean(0))+1e-9), len(E), len(E)
    cl=AgglomerativeClustering(n_clusters=None, distance_threshold=1-thresh, metric="cosine", linkage="average").fit(E)
    labs=cl.labels_; big=np.bincount(labs).argmax(); m=E[labs==big].mean(0); return m/(np.linalg.norm(m)+1e-9), int((labs==big).sum()), len(E)
def clusters(E, thresh=0.55, min_n=3):
    if len(E)<3: return [(E.mean(0)/(np.linalg.norm(E.mean(0))+1e-9), len(E))]
    cl=AgglomerativeClustering(n_clusters=None, distance_threshold=1-thresh, metric="cosine", linkage="average").fit(E)
    res=[]
    for l in np.unique(cl.labels_):
        n=int((cl.labels_==l).sum())
        if n>=min_n:
            m=E[cl.labels_==l].mean(0); res.append((m/(np.linalg.norm(m)+1e-9), n))
    return sorted(res, key=lambda x:-x[1])
db=dict(np.load(VP+"/voiceprints.npz")); ben=db.get("ben_schneider")
refs={}
for name,f in [("najar_call_oct11",'ref_najar_call_4600-5020.wav'),("najar_1336",'ref_najar_1336-1406.wav'),("cockroach_narration",'ref_cockroach_narration_0030-0430.wav')]:
    E=emb_windows(f); m,n,t=dominant(E); refs[name]=m; print(f"ref {name}: {t} windows, dominant cluster {n}")
if ben is not None: refs["ben_schneider_db"]=ben/np.linalg.norm(ben)
print("\nref-vs-ref cosines:")
names=list(refs)
for i in range(len(names)):
    for j in range(i+1,len(names)):
        print(f"  {names[i]} vs {names[j]}: {float(refs[names[i]]@refs[names[j]]):.3f}")
import glob
for q in sorted(glob.glob('q_*.wav')):
    E=emb_windows(q); cs=clusters(E)
    print(f"\nquery {q}: {len(E)} windows, {len(cs)} clusters (>=3 windows)")
    for k,(m,n) in enumerate(cs[:4]):
        sims=" ".join(f"{r}={float(m@refs[r]):.3f}" for r in refs)
        print(f"  cluster {k} ({n} win): {sims}")
