HY-2012's picture
Read speaker channel count from the model instead of assuming 10
fad0776 verified
Raw
History Blame Contribute Delete
5.71 kB
"""Streaming inference loop for the LS-EEND one-frame AXMODEL.
The graph is a single frame step with explicit recurrent state. Two things are
the caller's responsibility, and getting either wrong costs tens of DER points:
1. **FP32 host accumulation.** The graph returns the *bounded per-frame
increment* ``k_t v_t`` (``enc{i}_inc`` / ``dec{i}_inc``), not the updated
retention state. The host keeps the running mean in FP32 via
``mean += (inc - mean) / t``. Feeding a quantized state back instead lets
quantization error compound through the recurrence (DER 1.95% -> 7.4%).
2. **Warmup gating.** Upstream ``StreamingConv1d`` buffers 19 frames and emits
nothing until its 10th call, so the native loop never invokes the decoder for
the first ``CONV_DELAY`` frames. Real features are still fed (the encoder
state must advance), but ``pred`` is discarded and the decoder state is left
untouched. Skipping this diverges the trajectory permanently (DER -> 71.8%).
"""
from __future__ import annotations
import numpy as np
N_ENC_LAYERS = 4
N_DEC_LAYERS = 2
CONV_DELAY = 9 # StreamingConv1d emits from its (center+1)=10th call
# Output channel count is read from the model at load time: each LS-EEND
# release has a different max_speakers (simu 8 -> 10 channels, AMI 4 -> 6,
# CALLHOME 7 -> 9, DIHARD 10 -> 12).
INPUT_NAMES = (
['feat', 'inv_count', 'dec_inv_count', 'conv_cache']
+ [f'enc{i}_{x}' for i in range(N_ENC_LAYERS) for x in ('kv', 'conv')]
+ [f'dec{i}_kv' for i in range(N_DEC_LAYERS)]
)
OUTPUT_NAMES = (
['pred']
+ [f'enc{i}_{x}' for i in range(N_ENC_LAYERS) for x in ('inc', 'conv_out')]
+ ['conv_cache_out']
+ [f'dec{i}_inc' for i in range(N_DEC_LAYERS)]
)
ENC_MEAN_NAMES = [f'enc{i}_kv' for i in range(N_ENC_LAYERS)]
DEC_MEAN_NAMES = [f'dec{i}_kv' for i in range(N_DEC_LAYERS)]
class StreamingDiarizer:
"""Frame-synchronous LS-EEND runner over an AXMODEL (or ONNX) session."""
def __init__(self, model_path, providers=None):
self.model_path = str(model_path)
self._session, self._backend = self._open(self.model_path, providers)
shapes = {i.name: tuple(i.shape) for i in self._session.get_inputs()}
missing = [n for n in INPUT_NAMES if n not in shapes]
if missing:
raise RuntimeError(f'model is missing expected inputs: {missing}')
self.shapes = shapes
self.slots = int(np.prod([d for d in self._session.get_outputs()[0].shape]))
self.reset()
@staticmethod
def _open(model_path, providers):
if model_path.endswith('.axmodel'):
import axengine as axe
return axe.InferenceSession(
model_path, providers=providers or ['AxEngineExecutionProvider']
), 'axengine'
import onnxruntime as ort
return ort.InferenceSession(
model_path, providers=providers or ['CPUExecutionProvider']
), 'onnxruntime'
@property
def backend(self):
return self._backend
def reset(self):
"""Clear all recurrent state; call before each new recording."""
self._state = {n: np.zeros(self.shapes[n], dtype=np.float32) for n in INPUT_NAMES}
self._enc_t = 0 # frames seen by the encoder
self._dec_t = 0 # frames actually decoded
def step(self, frame):
"""Advance one feature frame.
Args:
frame: (345,) or (1,1,345) float32 log-mel frame.
Returns:
(slots,) float32 logits, or None during the conv warmup.
"""
state = self._state
state['feat'] = np.asarray(frame, dtype=np.float32).reshape(self.shapes['feat'])
enc_b = 1.0 / (self._enc_t + 1)
dec_b = 1.0 / max(self._dec_t + 1, 1)
state['inv_count'] = np.full(self.shapes['inv_count'], enc_b, dtype=np.float32)
state['dec_inv_count'] = np.full(self.shapes['dec_inv_count'], dec_b, dtype=np.float32)
feed = {n: np.ascontiguousarray(state[n], dtype=np.float32) for n in INPUT_NAMES}
out = dict(zip(OUTPUT_NAMES, self._session.run(None, feed)))
# Encoder state always advances. In-place to avoid per-frame allocations.
for i, name in enumerate(ENC_MEAN_NAMES):
mean = state[name]
mean *= (1.0 - enc_b)
mean += out[f'enc{i}_inc'] * enc_b
state[f'enc{i}_conv'] = out[f'enc{i}_conv_out']
state['conv_cache'] = out['conv_cache_out']
self._enc_t += 1
if self._enc_t <= CONV_DELAY:
return None # conv has not emitted yet; decoder stays frozen
for i, name in enumerate(DEC_MEAN_NAMES):
mean = state[name]
mean *= (1.0 - dec_b)
mean += out[f'dec{i}_inc'] * dec_b
self._dec_t += 1
return np.asarray(out['pred'], dtype=np.float32).reshape(-1)
def run(self, features, progress=None):
"""Run a whole recording.
Returns (T - CONV_DELAY, slots) float32 logits. The first
CONV_DELAY frames produce no output, and the trailing 0.9 s is not
emitted (the native flush pushes zero *embeddings* past the encoder,
which a fused one-frame graph cannot express).
"""
self.reset()
logits = []
total = len(features)
for t in range(total):
pred = self.step(features[t])
if pred is not None:
logits.append(pred)
if progress is not None and (t + 1) % progress == 0:
print(f' {t + 1}/{total} frames', flush=True)
if not logits:
raise RuntimeError(f'recording too short: need > {CONV_DELAY} frames, got {total}')
return np.stack(logits, axis=0)