viks66 commited on
Commit
a96f2f1
·
verified ·
1 Parent(s): a6c21ac

add predict.py

Browse files
Files changed (1) hide show
  1. predict.py +175 -0
predict.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Author: Sathvik Udupa (2026)
2
+ # Email: udupa@fit.vutbr.cz
3
+ # Paper: Streaming Endpointer for Spoken Dialogue using Neural Audio Codecs and Label-Delayed Training, https://arxiv.org/abs/2506.07081, ASRU 2025
4
+
5
+ """Mimi Endpointer — DiscriminativeModel for the TURN benchmark.
6
+
7
+ Two-stream LSTM over Mimi embeddings, streamed 20ms chunk at a time.
8
+ Mimi operates in 1920-sample (80ms) chunks → 2 LSTM frames per chunk.
9
+ Four harness steps are buffered before each Mimi run; floor bit is held
10
+ between updates. Inherent latency: ~80ms.
11
+
12
+ floor = 1 if P(user) > threshold else 0
13
+
14
+ subject is always fed as channel 0 (user); other as channel 1 (system).
15
+
16
+ Debug mode (MIMI_DEBUG=1): saves debug_pass{N}.npz per conversation pass;
17
+ run plot_debug.py afterwards to render PNGs.
18
+
19
+ Sweep mode (MIMI_SWEEP=1): runs the harness sweep over thresholds 0.05–0.95
20
+ in a single inference pass; threshold is reported per-step as a list[int].
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import atexit
25
+ import os
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ import numpy as np
30
+ import torch
31
+
32
+ _HERE = Path(__file__).resolve().parent
33
+ # model.py lives alongside predict.py in the HF flat layout, or one level up in the
34
+ # local nested layout (turn-bench-submission/ inside baselines/mimi_endpointer/)
35
+ sys.path.insert(0, str(_HERE))
36
+ sys.path.insert(0, str(_HERE.parent))
37
+
38
+ from model import ( # noqa: E402
39
+ AudioFeatureExtractor,
40
+ AUDIO_DEFAULTS,
41
+ load_model as load_mimi_model,
42
+ )
43
+
44
+ IDX_USER = 4 # from training config: {bos:0, system_end:1, user_end:2, system:3, user:4}
45
+
46
+ # checkpoint.pt is alongside predict.py (HF) or one level up (local)
47
+ CHECKPOINT = next(
48
+ p for p in (_HERE / "checkpoint.pt", _HERE.parent / "checkpoint.pt") if p.exists()
49
+ )
50
+
51
+ _CHUNK_STEPS = 4 # 4 × 20ms = 80ms = one Mimi frame_size (1920 samples at 24kHz)
52
+ _SR = 24_000
53
+ _FRAME_RATE = 50 # harness step rate (Hz)
54
+
55
+
56
+ class MimiEndpointerModel:
57
+ input_sample_rate = _SR # Mimi native rate; 24000 % 50 == 0 → 480 samples/step
58
+
59
+ def __init__(
60
+ self,
61
+ threshold: float = 0.5,
62
+ thresholds: list[float] | None = None,
63
+ debug: bool = False,
64
+ ) -> None:
65
+ # sweep mode: thresholds is a list; single mode: scalar threshold
66
+ if thresholds is not None:
67
+ self.thresholds = thresholds # harness detects sweep mode via hasattr
68
+ self._thresholds_arr = thresholds
69
+ else:
70
+ self.threshold = threshold # single operating point
71
+ self._thresholds_arr = [threshold]
72
+ self._sweep = thresholds is not None
73
+ self.debug = debug
74
+ device = "cuda" if torch.cuda.is_available() else "cpu"
75
+ self._device = device
76
+ self._model = load_mimi_model(str(CHECKPOINT), device=device)
77
+ self._extractor = AudioFeatureExtractor(**AUDIO_DEFAULTS, device=device)
78
+ self._ctx = None
79
+ self._debug_idx = 0
80
+ self._log_subj: list[np.ndarray] = []
81
+ self._log_other: list[np.ndarray] = []
82
+ self._log_floor: list[int] = []
83
+ self._log_probs: list[np.ndarray] = [] # all 5 class probs per step (T, 5)
84
+ if debug:
85
+ atexit.register(self._save_npz)
86
+ self.reset()
87
+
88
+ def reset(self) -> None:
89
+ if self.debug:
90
+ self._save_npz()
91
+ if self._ctx is not None:
92
+ self._ctx.__exit__(None, None, None)
93
+ self._ctx = self._extractor.mimi.streaming(batch_size=2)
94
+ self._ctx.__enter__()
95
+ self._h1, self._c1 = self._model.init_hidden(1, self._device)
96
+ self._h2, self._c2 = self._model.init_hidden(1, self._device)
97
+ self._buf_subj: list[np.ndarray] = []
98
+ self._buf_other: list[np.ndarray] = []
99
+ self._floor_bits: list[int] = [0] * len(self._thresholds_arr)
100
+ self._log_subj = []
101
+ self._log_other = []
102
+ self._log_floor = []
103
+ self._log_probs = []
104
+
105
+ def __del__(self) -> None:
106
+ if self._ctx is not None:
107
+ self._ctx.__exit__(None, None, None)
108
+
109
+ def _save_npz(self) -> None:
110
+ if not self._log_floor:
111
+ return
112
+ out = _HERE / f"debug_pass{self._debug_idx}.npz"
113
+ np.savez(
114
+ out,
115
+ subj=np.concatenate(self._log_subj),
116
+ other=np.concatenate(self._log_other),
117
+ floor=np.array(self._log_floor, dtype=np.int8),
118
+ probs=np.array(self._log_probs, dtype=np.float32), # (T, 5)
119
+ threshold=np.float32(self._thresholds_arr[0]),
120
+ sr=np.int32(_SR),
121
+ frame_rate=np.int32(_FRAME_RATE),
122
+ )
123
+ sys.stderr.write(f"[debug] saved → {out}\n")
124
+ self._debug_idx += 1
125
+
126
+ def step(self, subject_audio: np.ndarray, other_audio: np.ndarray):
127
+ self._buf_subj.append(subject_audio)
128
+ self._buf_other.append(other_audio)
129
+
130
+ if self.debug:
131
+ self._log_subj.append(subject_audio)
132
+ self._log_other.append(other_audio)
133
+
134
+ new_probs: np.ndarray | None = None
135
+
136
+ if len(self._buf_subj) == _CHUNK_STEPS:
137
+ chunk_s = torch.from_numpy(np.concatenate(self._buf_subj)).to(self._device)
138
+ chunk_o = torch.from_numpy(np.concatenate(self._buf_other)).to(self._device)
139
+ self._buf_subj.clear()
140
+ self._buf_other.clear()
141
+
142
+ # (2, 1, 1920) — subject=channel 0 (user), other=channel 1 (system)
143
+ chunk = torch.stack([chunk_s, chunk_o]).unsqueeze(1)
144
+ with torch.no_grad():
145
+ emb = self._extractor.mimi.encode_to_latent(chunk, quantize=True) # (2, feat, 1)
146
+ emb = self._extractor.mimi.upsample(emb) # (2, feat, 2)
147
+ logits = None
148
+ for t in range(emb.shape[-1]):
149
+ logits, self._h1, self._c1, self._h2, self._c2 = \
150
+ self._model.infer_ar_step(
151
+ emb[0:1, :, t], emb[1:2, :, t],
152
+ self._h1, self._c1, self._h2, self._c2,
153
+ )
154
+ new_probs = torch.softmax(logits[0], dim=-1).cpu().numpy() # (5,)
155
+ p_user = new_probs[IDX_USER]
156
+ self._floor_bits = [1 if p_user > t else 0 for t in self._thresholds_arr]
157
+
158
+ if self.debug:
159
+ p = new_probs if new_probs is not None else (
160
+ self._log_probs[-1] if self._log_probs else np.zeros(5, dtype=np.float32)
161
+ )
162
+ self._log_probs.append(p)
163
+ self._log_floor.append(self._floor_bits[0]) # first threshold for debug plot
164
+
165
+ return self._floor_bits if self._sweep else self._floor_bits[0]
166
+
167
+
168
+ def load_model() -> MimiEndpointerModel:
169
+ debug = os.environ.get("MIMI_DEBUG", "0") == "1"
170
+ sweep = os.environ.get("MIMI_SWEEP", "0") == "1"
171
+ if sweep:
172
+ thresholds = list(np.round(np.arange(0.05, 1.0, 0.05), 2).tolist())
173
+ return MimiEndpointerModel(thresholds=thresholds, debug=debug)
174
+ thr = float(os.environ.get("MIMI_THRESHOLD", "0.1"))
175
+ return MimiEndpointerModel(threshold=thr, debug=debug)