speaker-verification
audio
nemotron
tyger
experimental
ChristophSchuhmann's picture
Publish audited Robby source and completed Nemotron speaker-prediction experiments
b21cfae verified
Raw History Blame Contribute Delete
4.08 kB
"""Incremental speaker cue, deliberately separate from TYGER turn-taking.
Features are four new frozen Nemotron layer-15 vectors per 320-ms update.
Silence/overlap return no identity. Confidence is an activity/context indicator,
not an authentication probability. Do not persist private microphone features.
"""
from __future__ import annotations
from collections import deque
from pathlib import Path
import numpy as np
import torch
from .models import SpeakerPredictor,load_state
class Predictor:
def __init__(self, checkpoint, device="cpu", min_rms=0.003, accumulate=True):
state = torch.load(Path(checkpoint), map_location="cpu", weights_only=False)
cfg = state["config"]
self.model = SpeakerPredictor(cfg["architecture"], cfg["width"],centered=cfg.get("centered_target",False))
load_state(self.model,state["model"])
self.model.to(device).eval()
self.device = device
self.context = cfg["context"]
self.frames = deque(maxlen=self.context)
self.min_rms = min_rms
self.accumulate = accumulate
self.average = None
self.speech_updates = 0
self.voiced_frames = 0
self.threshold = float(state.get("activity_threshold", 0.6))
def reset(self):
self.frames.clear()
self.average = None
self.speech_updates = 0
self.voiced_frames = 0
@torch.inference_mode()
def update(self, features, energy=None):
"""Accept [new_frames,1024] numpy/tensor; energy is microphone RMS."""
values = torch.as_tensor(features, dtype=torch.float32).cpu()
if values.ndim != 2 or values.shape[1] != 1024:
raise ValueError("Expected new Nemotron layer-15 frames [T,1024]")
for frame in values:
self.frames.append(frame)
if energy is not None and energy < self.min_rms:
self.average = None
self.speech_updates = 0
self.voiced_frames = 0
return {"embedding": None, "confidence": 0., "kind": "silence",
"context_ms": len(self.frames) * 80}
if len(self.frames) < 4:
return {"embedding": None, "confidence": 0., "kind": "warming",
"context_ms": len(self.frames) * 80}
x = torch.stack(list(self.frames))
if len(x) < self.context:
x = torch.cat([x[:1].expand(self.context - len(x), -1), x])
emb, logits = self.model(x[None].to(self.device))
probs = logits.softmax(-1)[0].cpu().numpy()
kind = ("speech", "silence", "overlap")[int(probs.argmax())]
coverage = min(len(self.frames),self.voiced_frames+len(values)) / self.context
confidence = float(probs[0]) * min(1.,coverage)
if kind != "speech" or probs[0] < self.threshold:
self.average = None
self.speech_updates = 0
self.voiced_frames = 0
return {"embedding": None, "confidence": confidence, "kind": kind,
"context_ms": len(self.frames) * 80}
vector = emb[0].cpu().numpy()
self.voiced_frames = min(self.context,self.voiced_frames+len(values))
# Avoid blindly averaging across an obvious speaker change.
if self.average is None or np.dot(self.average, vector) < 0.3:
if self.average is not None:
self.voiced_frames = min(self.context,len(values))
confidence = float(probs[0])*self.voiced_frames/self.context
self.average = vector
self.speech_updates = 1
elif self.accumulate:
self.speech_updates = min(self.speech_updates + 1, 8)
alpha = 1 / self.speech_updates
self.average = (1 - alpha) * self.average + alpha * vector
self.average /= max(np.linalg.norm(self.average), 1e-9)
else:
self.average = vector
return {"embedding": self.average.copy(), "confidence": confidence,
"kind": "speech", "context_ms": len(self.frames) * 80,
"activity_probabilities": probs.tolist()}