Aduomas's picture
Wrap long decoder line
142a394 verified
Raw History Blame Contribute Delete
2.01 kB
"""Turn Nemotron 3 frame probabilities into speaker segments, replicating Omi's published decoding behaviour.
Omi's output differs from NVIDIA's plain 0.5 threshold by a slightly higher threshold and by bridging short
same-speaker pauses: always below `bridge_s`, and up to `bridge_clear_s` when no other speaker is active inside.
"""
from dataclasses import dataclass
import numpy as np
FRAME_S = 0.01
@dataclass(frozen=True)
class DecoderParams:
threshold: float = 0.5
bridge_s: float = 0.0
bridge_clear_s: float = 0.0
def _runs(active: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
edges = np.diff(np.r_[False, active, False].astype(np.int8))
return np.flatnonzero(edges == 1), np.flatnonzero(edges == -1)
def activity(probs: np.ndarray, params: DecoderParams) -> np.ndarray:
"""Boolean (frames, speakers) activity after thresholding and pause bridging."""
raw = probs > params.threshold
active = raw.copy()
bridge, bridge_clear = round(params.bridge_s / FRAME_S), round(params.bridge_clear_s / FRAME_S)
for speaker in range(raw.shape[1]):
others = np.delete(raw, speaker, axis=1).any(axis=1)
starts, ends = _runs(raw[:, speaker])
for gap_start, gap_end in zip(ends[:-1], starts[1:]):
length = gap_end - gap_start
if length < bridge or (length < bridge_clear and not others[gap_start:gap_end].any()):
active[gap_start:gap_end, speaker] = True
return active
def decode(probs: np.ndarray, params: DecoderParams) -> list[dict]:
"""Segments in seconds; speakers that never activate are dropped, so the count stays automatic."""
active = activity(probs, params)
segments = []
for speaker in range(active.shape[1]):
starts, ends = _runs(active[:, speaker])
segments.extend(
{"start": round(float(s * FRAME_S), 2), "end": round(float(e * FRAME_S), 2), "speaker": f"spk_{speaker}"}
for s, e in zip(starts, ends)
)
return segments