shangeth-anyreach commited on
Commit
7bf221f
Β·
verified Β·
1 Parent(s): 7260578

Upload README.md with huggingface_hub

Browse files
Files changed (1) hide show
  1. README.md +110 -99
README.md CHANGED
@@ -1,135 +1,115 @@
1
- # dualturn-endpointing
2
 
3
- Speech endpoint detection for two-channel (user + agent) audio.
4
 
5
- Answers one question in real-time every 80 ms:
6
 
7
- > **Has the user finished speaking?** β†’ `ST` (start talking β€” agent responds) or `CL` (continue listening β€” user is mid-sentence)
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  ---
10
 
11
- ## Models
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- | File | Size | Description |
14
- |------|------|-------------|
15
- | `best.pt` | 534 MB | Dualturn transformer backbone β€” predicts VAD/EOT/BOT per channel |
16
- | `endpoint_clf.pkl` | 370 KB | Logistic regression endpoint classifier β€” P(ST) from 10 signals |
 
 
 
 
 
 
 
 
17
 
18
  ---
19
 
20
- ## How it works
21
 
22
  ```
23
  Dual-channel audio (24 kHz stereo)
24
  β”‚
25
- β–Ό every 80 ms (Mimi encoder β†’ dualturn transformer)
26
  Per-frame signals:
27
- vad_user, vad_agent ← is each speaker currently talking?
28
- eot_user, eot_agent ← near end of their turn?
29
- bot_user, bot_agent ← beginning a new turn?
30
- fvad_user_short/long ← fast VAD (Silero), two smoothing windows
31
- fvad_agent_short/long
32
  β”‚
33
- β–Ό VAD edge detector (watches vad_user crossing 0.5)
34
  β”‚
35
- └── VAD offset detected (user stopped) + agent silent?
36
- β”‚
37
- β–Ό endpoint_clf.predict_proba([10 signal values])
38
- P(ST) >= 0.30 β†’ ST βœ“ agent should respond
39
- P(ST) < 0.30 β†’ CL wait, user paused mid-sentence
40
  ```
41
 
42
- **Threshold = 0.30** β€” tuned on held-out test set to maximise ST recall (99% recall, 90% precision on test set).
43
 
44
  ---
45
 
46
- ## Install
47
 
48
- ```bash
49
- pip install torch torchaudio joblib scikit-learn huggingface_hub silero-vad
50
- ```
 
 
 
 
 
 
51
 
52
  ---
53
 
54
- ## Usage
55
-
56
- ### From HuggingFace (recommended)
57
 
58
  ```python
59
  from endpointing import DualTurnEndpointing
60
 
61
- model = DualTurnEndpointing.from_pretrained("anyreach/dualturn-endpointing")
62
-
63
- # Offline: process a stereo WAV file
64
- frames, endpoints = model.process_file(
65
- "call.wav",
66
- user_channel=0, # which stereo channel is the user
67
- agent_channel=1,
68
- )
69
-
70
- for ep in endpoints:
71
- print(f"t={ep['t_s']:.2f}s action={ep['action']} P(ST)={ep['p_st']:.3f}")
72
- ```
73
-
74
- ### From local files
75
-
76
- ```python
77
- model = DualTurnEndpointing(
78
- backbone_path = "best.pt",
79
- classifier_path = "endpoint_clf.pkl",
80
- device = "cuda",
81
- )
82
- ```
83
-
84
- ### Streaming (real-time, 80 ms chunks)
85
-
86
- ```python
87
- model = DualTurnEndpointing.from_pretrained("anyreach/dualturn-endpointing")
88
  stream = model.stream(user_channel=0, agent_channel=1)
89
 
90
- for chunk in audio_source(): # chunk: np.ndarray (2, 1920) float32
 
 
91
  result = stream.push(chunk)
92
  if result:
93
  if result["action"] == "ST":
94
  agent.start_responding()
95
- else:
96
- agent.keep_waiting()
97
- ```
98
-
99
- ### CLI
100
-
101
- ```bash
102
- # From HuggingFace
103
- python endpointing.py --audio call.wav --from-hf anyreach/dualturn-endpointing
104
-
105
- # From local files
106
- python endpointing.py \
107
- --audio call.wav \
108
- --backbone best.pt \
109
- --classifier endpoint_clf.pkl \
110
- --user-channel 0 \
111
- --agent-channel 1 \
112
- --out-json results.json
113
- ```
114
-
115
- ---
116
-
117
- ## Output format
118
-
119
- Each endpoint decision:
120
- ```json
121
- {
122
- "t_s": 9.70,
123
- "action": "ST",
124
- "p_st": 0.984,
125
- "signals": {
126
- "vad_user": 0.02, "vad_agent": 0.01,
127
- "eot_user": 0.91, "eot_agent": 0.03,
128
- "bot_user": 0.01, "bot_agent": 0.05,
129
- "fvad_user_short": 0.03, "fvad_user_long": 0.12,
130
- "fvad_agent_short": 0.01, "fvad_agent_long": 0.02
131
- }
132
- }
133
  ```
134
 
135
  ---
@@ -140,8 +120,39 @@ Each endpoint decision:
140
  |--------|-------|
141
  | ST recall | 99% |
142
  | ST precision | 90% |
143
- | CL recall | 6% |
144
- | AUC | 0.853 |
145
  | Threshold | 0.30 |
146
 
147
- The model is tuned for **high ST recall** β€” it almost never misses a real turn end. CL recall is low by design (we'd rather respond slightly early than make the user repeat themselves).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # anyreach-ai/dualturn-endpointing
2
 
3
+ Real-time speech endpoint detector for two-channel (user + agent) audio.
4
 
5
+ Built on top of [DualTurn-Qwen2.5-Mimi-0.5B](https://huggingface.co/anyreach-ai/dualturn-qwen2.5-mimi-0.5B) with a trained endpoint classifier that answers one question at every VAD offset:
6
 
7
+ > **Has the user finished their turn?**
8
+ > `ST` β€” yes, agent should respond now
9
+ > `CL` β€” no, user just paused mid-sentence, keep waiting
10
+
11
+ | Output | Shape | Description |
12
+ |--------|-------|-------------|
13
+ | `vad_probs` | `[B, T, 2]` | P(speaking now) β€” `[:,0]`=user `[:,1]`=agent |
14
+ | `eot_probs` | `[B, T, 2]` | P(end of turn) per channel |
15
+ | `bot_probs` | `[B, T, 2]` | P(beginning of turn) per channel |
16
+ | `fvad_probs` | `[B, T, 4]` | Fast VAD (Silero) β€” user_short, user_long, agent_short, agent_long |
17
+ | `endpoints` | `list[dict]` | Sparse ST/CL decisions at VAD-offset anchors with P(ST) |
18
+
19
+ Frame rate: **12.5 Hz** (80 ms per frame). Audio resampled to 24 kHz internally.
20
 
21
  ---
22
 
23
+ ## Inference
24
+
25
+ ```bash
26
+ pip install transformers torch torchaudio joblib scikit-learn silero-vad huggingface_hub
27
+ ```
28
+
29
+ ```python
30
+ import torch, torchaudio
31
+ from transformers import AutoModel
32
+
33
+ model = AutoModel.from_pretrained(
34
+ "anyreach-ai/dualturn-endpointing",
35
+ trust_remote_code=True,
36
+ )
37
+ model.eval()
38
+
39
+ wav, sr = torchaudio.load("conversation.wav") # [2, T] CH0=user CH1=agent
40
+
41
+ with torch.no_grad():
42
+ out = model(wav, sr=sr)
43
 
44
+ # Per-frame signals at 12.5 Hz
45
+ print(out.vad_probs.shape) # [1, T, 2] P(speaking) β€” (user, agent)
46
+ print(out.eot_probs.shape) # [1, T, 2] P(end-of-turn)
47
+ print(out.bot_probs.shape) # [1, T, 2] P(begin-of-turn)
48
+ print(out.fvad_probs.shape) # [1, T, 4] fast VAD
49
+
50
+ # Sparse endpoint decisions β€” one per VAD offset (user stops speaking)
51
+ for ep in out.endpoints:
52
+ print(f"t={ep['t_s']:.2f}s action={ep['action']} P(ST)={ep['p_st']:.3f}")
53
+ # ep["action"] β†’ "ST" (agent should respond) or "CL" (keep waiting)
54
+ # ep["p_st"] β†’ P(user is done) threshold = 0.30
55
+ ```
56
 
57
  ---
58
 
59
+ ## Endpoint decision logic
60
 
61
  ```
62
  Dual-channel audio (24 kHz stereo)
63
  β”‚
64
+ β–Ό Mimi encoder + DualTurn backbone (every 80 ms)
65
  Per-frame signals:
66
+ vad_user, vad_agent, eot_user, eot_agent,
67
+ bot_user, bot_agent, fvad_*_short, fvad_*_long
 
 
 
68
  β”‚
69
+ β–Ό watch vad_user crossing 0.5
70
  β”‚
71
+ VAD offset detected (user stopped) + agent silent?
72
+ β”‚
73
+ β–Ό endpoint_clf.predict_proba(10 signal values)
74
+ P(ST) >= 0.30 β†’ ST βœ“ agent should respond now
75
+ P(ST) < 0.30 β†’ CL user paused mid-sentence, wait
76
  ```
77
 
78
+ Threshold **0.30** is tuned to maximise ST recall (99% recall on held-out test set).
79
 
80
  ---
81
 
82
+ ## Files
83
 
84
+ | File | Description |
85
+ |------|-------------|
86
+ | `best.pt` | DualTurn backbone weights (two-stream transformer, 34M params) |
87
+ | `endpoint_clf.pkl` | Endpoint classifier β€” sklearn bundle with trained model + recommended threshold |
88
+ | `modeling_dualturn.py` | `DualTurnModel(PreTrainedModel)` β€” AutoModel-compatible wrapper |
89
+ | `configuration_dualturn.py` | `DualTurnConfig` |
90
+ | `config.json` | `auto_map` for AutoModel/AutoConfig |
91
+ | `endpointing.py` | Higher-level `DualTurnEndpointing` class with streaming support |
92
+ | `src/` | Bundled dualturn + evaluation source code |
93
 
94
  ---
95
 
96
+ ## Streaming (real-time)
 
 
97
 
98
  ```python
99
  from endpointing import DualTurnEndpointing
100
 
101
+ model = DualTurnEndpointing.from_pretrained("anyreach-ai/dualturn-endpointing")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  stream = model.stream(user_channel=0, agent_channel=1)
103
 
104
+ # Feed 80 ms of 24 kHz stereo PCM float32 every tick
105
+ # chunk shape: (2, 1920)
106
+ for chunk in audio_source():
107
  result = stream.push(chunk)
108
  if result:
109
  if result["action"] == "ST":
110
  agent.start_responding()
111
+ # result["p_st"] β€” P(ST) from classifier
112
+ # result["signals"] β€” all 10 signal values at the anchor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  ```
114
 
115
  ---
 
120
  |--------|-------|
121
  | ST recall | 99% |
122
  | ST precision | 90% |
123
+ | AUC (ST vs CL) | 0.853 |
 
124
  | Threshold | 0.30 |
125
 
126
+ Tuned for **high ST recall** β€” almost never misses a real turn end. Accepts some false STs to avoid making the user repeat themselves.
127
+
128
+ ---
129
+
130
+ ## Training Data
131
+
132
+ Endpoint classifier trained on 70 real dual-channel calls (user + Gemini agent) automatically labelled by Gemini 2.5 Pro.
133
+
134
+ Backbone: [anyreach-ai/dualturn-qwen2.5-mimi-0.5B](https://huggingface.co/anyreach-ai/dualturn-qwen2.5-mimi-0.5B)
135
+
136
+ ---
137
+
138
+ ## Authors
139
+
140
+ * Shangeth Rajaa β€” Senior ML Research Scientist, Anyreach AI
141
+
142
+ ---
143
+
144
+ ## Citation
145
+
146
+ **Paper:** DualTurn: Learning Turn-Taking from Dual-Channel Generative Speech Pretraining
147
+
148
+ ```bibtex
149
+ @misc{rajaa2026dualturnlearningturntakingdualchannel,
150
+ title={DualTurn: Learning Turn-Taking from Dual-Channel Generative Speech Pretraining},
151
+ author={Shangeth Rajaa},
152
+ year={2026},
153
+ eprint={2603.08216},
154
+ archivePrefix={arXiv},
155
+ primaryClass={eess.AS},
156
+ url={https://arxiv.org/abs/2603.08216},
157
+ }
158
+ ```