Download code/turn.py from Modularcomputing/dynamic-accent-duplex: direct link, hf CLI and curl.
- Browser
- Download file 4.26 kB
-
https://huggingface.co/Modularcomputing/dynamic-accent-duplex/resolve/ac573f1b218a7c549ca46573107d3833a92d0ed3/code/turn.py
- Command line
-
hf download hf://Modularcomputing/dynamic-accent-duplex@ac573f1b218a7c549ca46573107d3833a92d0ed3/code/turn.py
-
curl -L -o turn.py https://huggingface.co/Modularcomputing/dynamic-accent-duplex/resolve/ac573f1b218a7c549ca46573107d3833a92d0ed3/code/turn.py
4.26 kB
| """Duplex turn-taking: Silero VAD (MIT) per 32 ms frame, end-of-turn by trailing silence, barge-in while the agent | |
| speaks/thinks, and light backchannel cues at mid-turn pauses. Pure frame clock (16 kHz, 512-sample frames).""" | |
| import torch, numpy as np | |
| FR = 512; SR = 16000; FMS = FR / SR | |
| class TurnDetector: | |
| def __init__(self, eot_ms=600, start_ms=96, barge_ms=192, on=0.5, off=0.35, preroll_ms=300, | |
| bc_min_turn_s=4.5, bc_pause_ms=(288, 448), bc_gap_s=10.0, spec_ms=256): | |
| from silero_vad import load_silero_vad | |
| self.vad = load_silero_vad() | |
| self.eot_f = int(eot_ms / 1000 / FMS); self.start_f = max(1, int(start_ms / 1000 / FMS)); self.barge_f = int(barge_ms / 1000 / FMS) | |
| self.on, self.off = on, off; self.pre_f = int(preroll_ms / 1000 / FMS) | |
| self.spec_f = int(spec_ms / 1000 / FMS) if spec_ms else 0 | |
| self.bc_min, self.bc_pause, self.bc_gap = bc_min_turn_s, (int(bc_pause_ms[0] / 1000 / FMS), int(bc_pause_ms[1] / 1000 / FMS)), bc_gap_s | |
| self.reset() | |
| def reset(self): | |
| self.vad.reset_states(); self.t = 0.0; self.in_speech = False; self.on_count = 0; self.sil = 0 | |
| self.ring = []; self.buf = []; self.speech_frames = 0; self.onset_t = None; self.last_bc = -99; self.bc_done_this_pause = False | |
| self.barged = False; self.busy_speech = 0; self._was_busy = False | |
| def feed(self, frame, agent_busy=False): | |
| """frame: float32[512]. returns list of (event, info). events: start, barge, backchannel, eot""" | |
| ev = [] | |
| p = float(self.vad(torch.from_numpy(np.ascontiguousarray(frame, dtype=np.float32)), SR).item()) | |
| self.t += FMS | |
| sp = p > (self.off if self.in_speech else self.on) | |
| if agent_busy and not self._was_busy: | |
| self.busy_speech = 0; self.barged = False | |
| self._was_busy = agent_busy | |
| if agent_busy: | |
| self.busy_speech = self.busy_speech + 1 if p > self.on else max(0, self.busy_speech - 1) | |
| if not self.barged and self.busy_speech >= self.barge_f: | |
| self.barged = True; ev.append(("barge", dict(t=self.t, onset=self.onset_t))) | |
| if not self.in_speech: | |
| self.ring.append(frame); self.ring = self.ring[-self.pre_f:] | |
| self.on_count = self.on_count + 1 if sp else 0 | |
| if self.on_count >= self.start_f: | |
| self.in_speech = True; self.sil = 0; self.speech_frames = self.on_count | |
| self.onset_t = self.t - self.on_count * FMS; self.buf = list(self.ring); self.bc_done_this_pause = False | |
| ev.append(("start", dict(t=self.onset_t, p=p))) | |
| if agent_busy and self.on_count >= self.barge_f: | |
| pass | |
| else: | |
| self.buf.append(frame) | |
| if sp: | |
| if self.spec_f and self.sil >= self.spec_f: | |
| ev.append(("resume", dict(t=self.t))) # speculative response must be dropped | |
| self.sil = 0; self.speech_frames += 1; self.bc_done_this_pause = False | |
| else: | |
| self.sil += 1 | |
| if self.spec_f and self.sil == self.spec_f: | |
| keep = len(self.buf) - self.sil + int(0.2 / FMS) | |
| ev.append(("spec", dict(t=self.t, speech_end=self.t - self.sil * FMS, audio=np.concatenate(self.buf[:max(1, keep)])))) | |
| dur = self.speech_frames * FMS | |
| if (not agent_busy and dur >= self.bc_min and self.bc_pause[0] <= self.sil <= self.bc_pause[1] | |
| and not self.bc_done_this_pause and self.t - self.last_bc >= self.bc_gap): | |
| self.bc_done_this_pause = True; self.last_bc = self.t; ev.append(("backchannel", dict(t=self.t, turn_s=dur))) | |
| if self.sil >= self.eot_f: | |
| end_t = self.t - self.sil * FMS | |
| keep = len(self.buf) - self.sil + int(0.2 / FMS) | |
| audio = np.concatenate(self.buf[:max(1, keep)]) | |
| ev.append(("eot", dict(t=self.t, speech_end=end_t, onset=self.onset_t, audio=audio, speech_s=dur))) | |
| self.in_speech = False; self.on_count = 0; self.buf = []; self.ring = [] | |
| return ev | |
| def partial_audio(self): | |
| return np.concatenate(self.buf) if self.buf else np.zeros(0, np.float32) | |