discussion-helper 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- discussion_helper-0.1.0/LICENSE +29 -0
- discussion_helper-0.1.0/PKG-INFO +251 -0
- discussion_helper-0.1.0/README.md +203 -0
- discussion_helper-0.1.0/discussion_helper/__init__.py +76 -0
- discussion_helper-0.1.0/discussion_helper/adapters.py +413 -0
- discussion_helper-0.1.0/discussion_helper/api.py +533 -0
- discussion_helper-0.1.0/discussion_helper/cli.py +231 -0
- discussion_helper-0.1.0/discussion_helper/cli_click.py +157 -0
- discussion_helper-0.1.0/discussion_helper/discussion.py +361 -0
- discussion_helper-0.1.0/discussion_helper/mcp.py +96 -0
- discussion_helper-0.1.0/discussion_helper/parts.py +149 -0
- discussion_helper-0.1.0/discussion_helper/remote.py +248 -0
- discussion_helper-0.1.0/discussion_helper/session.py +359 -0
- discussion_helper-0.1.0/discussion_helper/testing.py +163 -0
- discussion_helper-0.1.0/discussion_helper/types.py +231 -0
- discussion_helper-0.1.0/discussion_helper.egg-info/PKG-INFO +251 -0
- discussion_helper-0.1.0/discussion_helper.egg-info/SOURCES.txt +26 -0
- discussion_helper-0.1.0/discussion_helper.egg-info/dependency_links.txt +1 -0
- discussion_helper-0.1.0/discussion_helper.egg-info/entry_points.txt +4 -0
- discussion_helper-0.1.0/discussion_helper.egg-info/requires.txt +36 -0
- discussion_helper-0.1.0/discussion_helper.egg-info/top_level.txt +1 -0
- discussion_helper-0.1.0/pyproject.toml +97 -0
- discussion_helper-0.1.0/setup.cfg +4 -0
- discussion_helper-0.1.0/tests/test_a_bare_install_can_run_its_commands.py +124 -0
- discussion_helper-0.1.0/tests/test_readme_is_pypi_safe.py +97 -0
- discussion_helper-0.1.0/tests/test_the_docs_name_the_real_routes.py +100 -0
- discussion_helper-0.1.0/tests/test_the_floor.py +363 -0
- discussion_helper-0.1.0/tests/test_the_http_surface.py +404 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Warith HARCHAOUI
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: discussion-helper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hold a spoken conversation: listen, answer, speak โ and stop the moment the user cuts in. The loop over vocal-helper (hearing), a local language model (answering) and speaker-helper (speaking), with the floor, barge-in and the latency budget handled properly.
|
|
5
|
+
Author-email: Warith HARCHAOUI <warithmetics@deraison.ai>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/warith-harchaoui/discussion-helper
|
|
8
|
+
Project-URL: Issues, https://github.com/warith-harchaoui/discussion-helper/issues
|
|
9
|
+
Keywords: voice-agent,conversation,speech,barge-in,turn-taking,full-duplex,realtime,local-first,stt,tts
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: <3.14,>=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: os-helper>=1.7.2
|
|
20
|
+
Requires-Dist: click>=8.1
|
|
21
|
+
Provides-Extra: listen
|
|
22
|
+
Requires-Dist: vocal-helper>=0.4.4; extra == "listen"
|
|
23
|
+
Requires-Dist: capture-helper>=0.3.0; extra == "listen"
|
|
24
|
+
Provides-Extra: speak
|
|
25
|
+
Requires-Dist: speaker-helper[detect,kokoro]>=0.8.0; extra == "speak"
|
|
26
|
+
Provides-Extra: think
|
|
27
|
+
Requires-Dist: best-engine-ai-helper>=0.4.0; extra == "think"
|
|
28
|
+
Provides-Extra: play
|
|
29
|
+
Requires-Dist: sounddevice>=0.4; extra == "play"
|
|
30
|
+
Requires-Dist: numpy>=1.24; extra == "play"
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: discussion-helper[listen,play,speak,think]; extra == "all"
|
|
33
|
+
Provides-Extra: server
|
|
34
|
+
Requires-Dist: fastapi>=0.110; extra == "server"
|
|
35
|
+
Requires-Dist: uvicorn>=0.29; extra == "server"
|
|
36
|
+
Requires-Dist: pydantic>=2.6; extra == "server"
|
|
37
|
+
Requires-Dist: websockets>=12; extra == "server"
|
|
38
|
+
Provides-Extra: mcp
|
|
39
|
+
Requires-Dist: discussion-helper[server]; extra == "mcp"
|
|
40
|
+
Requires-Dist: fastapi-mcp>=0.3; extra == "mcp"
|
|
41
|
+
Requires-Dist: mcp<2.0; extra == "mcp"
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
45
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
46
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# Discussion Helper
|
|
50
|
+
|
|
51
|
+
[๐ซ๐ท](https://github.com/warith-harchaoui/discussion-helper/blob/main/LISEZMOI.md) ยท [๐ฌ๐ง](https://github.com/warith-harchaoui/discussion-helper/blob/main/README.md)
|
|
52
|
+
|
|
53
|
+
[](https://github.com/warith-harchaoui/discussion-helper/blob/main/LICENSE) [](#) [](#the-promise)
|
|
54
|
+
|
|
55
|
+
`Discussion Helper` belongs to a collection of libraries called `AI Helpers`
|
|
56
|
+
developed for building Artificial Intelligence.
|
|
57
|
+
|
|
58
|
+
[๐ AI Helpers](https://deraison.ai/ai-helpers)
|
|
59
|
+
|
|
60
|
+
**Hold a spoken conversation at the microphone: listen, answer, speak โ and
|
|
61
|
+
stop the moment the user cuts in.**
|
|
62
|
+
|
|
63
|
+
`vocal-helper` hears. `speaker-helper` speaks. A local language model answers.
|
|
64
|
+
None of them knows whose turn it is. This package is the loop between the
|
|
65
|
+
three, and the three questions it has to get right:
|
|
66
|
+
|
|
67
|
+
- **Who holds the floor.** Exactly one party at a time. Most voice-agent
|
|
68
|
+
failures are two parties believing they hold it โ the agent talking over the
|
|
69
|
+
user is the obvious one; both waiting in silence is the one nobody notices
|
|
70
|
+
until a recording is played back.
|
|
71
|
+
- **What happens when the user cuts in.** Under **150 ms** from their first
|
|
72
|
+
syllable to the agent's silence, and the audio already synthesised and not
|
|
73
|
+
yet heard is *discarded*, not drained. Playing it out because it exists is
|
|
74
|
+
what makes an agent feel deaf.
|
|
75
|
+
- **Where the latency went.** A single total says a conversation feels slow
|
|
76
|
+
and nothing about why. Every exchange reports its breakdown โ endpointing,
|
|
77
|
+
recognition, first token, first audio โ and names the stage to fix.
|
|
78
|
+
|
|
79
|
+
## The promise
|
|
80
|
+
|
|
81
|
+
**Local-first.** The microphone, the recogniser, the model and the voice all
|
|
82
|
+
run on your machine. No audio leaves it, no account, no per-minute billing.
|
|
83
|
+
|
|
84
|
+
**Testable without a room.** The three parts are protocols, so the whole loop
|
|
85
|
+
runs in CI with no microphone, no model and no sound card. That is not
|
|
86
|
+
tidiness: interruption handling is exactly the behaviour a real room makes
|
|
87
|
+
impossible to assert. You cannot *hear* whether the queued audio was
|
|
88
|
+
discarded.
|
|
89
|
+
|
|
90
|
+
## Install
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install 'discussion-helper[all]' # hearing, thinking, speaking, playing
|
|
94
|
+
speaker-helper install-voice --engine kokoro # the voice's weights, once
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
On Linux and macOS the voice also needs `espeak-ng`
|
|
98
|
+
(`apt install espeak-ng`, `brew install espeak-ng`).
|
|
99
|
+
|
|
100
|
+
Each half installs on its own if you only want some of it: `[listen]`,
|
|
101
|
+
`[think]`, `[speak]`, `[play]`.
|
|
102
|
+
|
|
103
|
+
## Try it with nothing installed
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install discussion-helper
|
|
107
|
+
discussion-helper rehearse "bonjour" "comment รงa va ?"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`rehearse` runs the **same loop** with the microphone, model and speaker
|
|
111
|
+
replaced by fakes. When a voice agent misbehaves the first question is always
|
|
112
|
+
"is it the loop or is it the room", and this answers it in a second:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# stage an interruption a quarter of a second into the reply
|
|
116
|
+
discussion-helper rehearse "dis-moi tout" --sentences 6 --interrupt-after 0.25 --grace 0
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
you: dis-moi tout
|
|
121
|
+
it: dis-moi tout dis-moi tout (2) (cut off)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Talk to it
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
discussion-helper talk --voice ff_siwis
|
|
128
|
+
discussion-helper talk --no-barge-in # strict half-duplex
|
|
129
|
+
discussion-helper talk --json # transcript + latency breakdown
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## As a library
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import asyncio
|
|
136
|
+
from discussion_helper import Discussion, Policy
|
|
137
|
+
from discussion_helper.adapters import VocalListener, LocalResponder, SpeakerVoice
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def main():
|
|
141
|
+
async with Discussion(
|
|
142
|
+
VocalListener(language="fr"),
|
|
143
|
+
LocalResponder(),
|
|
144
|
+
SpeakerVoice(voice="ff_siwis"),
|
|
145
|
+
Policy(barge_in_grace_s=0.35),
|
|
146
|
+
) as talk:
|
|
147
|
+
conversation = await talk.run()
|
|
148
|
+
|
|
149
|
+
for turn in conversation.utterances:
|
|
150
|
+
print(turn.speaker, turn.text)
|
|
151
|
+
print(conversation.timings[0].as_dict())
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
asyncio.run(main())
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Bring your own parts by implementing `Listener`, `Responder` or `Voice` โ a
|
|
158
|
+
cloud recogniser, a scripted model for a demo, a phone line instead of a
|
|
159
|
+
speaker. Each is one class, and the loop does not change.
|
|
160
|
+
|
|
161
|
+
## Over HTTP, and over a socket
|
|
162
|
+
|
|
163
|
+
A conversation does not fit a request โ it has no single answer and lasts
|
|
164
|
+
minutes โ so the HTTP surface is a session you create, a stream you follow and
|
|
165
|
+
a button you press:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pip install 'discussion-helper[server]'
|
|
169
|
+
discussion-helper-mcp # or: uvicorn discussion_helper.api:build --factory
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
| Route | What it is |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `GET /health` | whether each of the three parts could run here, and what is missing |
|
|
175
|
+
| `POST /sessions` | start one; returns an id immediately |
|
|
176
|
+
| `GET /sessions` | every conversation this process is holding |
|
|
177
|
+
| `GET /sessions/{id}` | the transcript, the floor and the timings, as they stand |
|
|
178
|
+
| `GET /sessions/{id}/events` | follow it โ floor, turns, latency, as they happen |
|
|
179
|
+
| `POST /sessions/{id}/say` | push a user turn in as text, for a remote session |
|
|
180
|
+
| `POST /sessions/{id}/interrupt` | stop the agent, through the same path a barge-in uses |
|
|
181
|
+
| `DELETE /sessions/{id}` | end it, and get what was said |
|
|
182
|
+
| `WS /sessions/{id}/audio` | **the microphone at the other end** |
|
|
183
|
+
| `POST /turn` | one exchange, no audio โ the shape an agent can use |
|
|
184
|
+
| `/mcp` | the same routes as agent tools |
|
|
185
|
+
|
|
186
|
+
The WebSocket carries real audio both ways: the client sends PCM and control
|
|
187
|
+
frames, the server sends events and WAV. That is how every hosted voice agent
|
|
188
|
+
works, and it means the browser can hold the microphone while this end holds
|
|
189
|
+
the model. Two duties fall on the client and are not optional โ send
|
|
190
|
+
`{"type":"speaking"}` on the **first syllable** so a barge-in can travel, and
|
|
191
|
+
**drop** buffered audio on `{"kind":"stop"}`. A client that plays its buffer
|
|
192
|
+
out reproduces, at the far end, exactly the deafness that stopping it here
|
|
193
|
+
was meant to prevent.
|
|
194
|
+
|
|
195
|
+
### In a container
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
docker compose up -d
|
|
199
|
+
curl -s localhost:8090/health
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The image serves the **remote** transport โ the caller owns the microphone and
|
|
203
|
+
sends frames โ and that is the honest half of this package a container can
|
|
204
|
+
run. It does **not** open a microphone of its own: `--transport local` reads
|
|
205
|
+
the machine's sound card, and a container has none. Passing one in is
|
|
206
|
+
`--device /dev/snd` on Linux and impossible on macOS, which is a per-machine
|
|
207
|
+
arrangement rather than an image, so the `listen` extra is deliberately left
|
|
208
|
+
out and the refusal happens at install time instead of at the first call.
|
|
209
|
+
|
|
210
|
+
The port is bound to the loopback, and the reason is in `discussion-helper-mcp
|
|
211
|
+
--help` as well: this server can open a microphone. One worker, also
|
|
212
|
+
deliberately โ the voice backend loads its ONNX graph once per process and a
|
|
213
|
+
second worker pays for its own copy, and sessions are stateful besides.
|
|
214
|
+
|
|
215
|
+
## Read this before tuning anything
|
|
216
|
+
|
|
217
|
+
**[KNOWLEDGE.md](https://github.com/warith-harchaoui/discussion-helper/blob/main/KNOWLEDGE.md)** is the design notebook: the 800 ms latency
|
|
218
|
+
budget and where it actually goes, why a silence threshold is not turn
|
|
219
|
+
detection, the two mistakes everyone makes with barge-in, why replies are
|
|
220
|
+
chunked at sentences and not tokens, and why acoustic echo is the problem that
|
|
221
|
+
decides your defaults.
|
|
222
|
+
|
|
223
|
+
The short version: a voice agent has about 800 ms before the conversation
|
|
224
|
+
feels broken, the largest line in that budget is endpointing rather than the
|
|
225
|
+
model, and the agent's own voice reaching its own microphone looks exactly
|
|
226
|
+
like the user speaking.
|
|
227
|
+
|
|
228
|
+
## How this differs from its neighbours
|
|
229
|
+
|
|
230
|
+
| You have | Reach for |
|
|
231
|
+
|---|---|
|
|
232
|
+
| A recording somebody needs as text | [`vocal-helper`](https://github.com/warith-harchaoui/vocal-helper) |
|
|
233
|
+
| Text somebody needs as speech | [`speaker-helper`](https://github.com/warith-harchaoui/speaker-helper) |
|
|
234
|
+
| A live camera or microphone as frames | [`capture-helper`](https://github.com/warith-harchaoui/capture-helper) |
|
|
235
|
+
| **A conversation to hold** | this one |
|
|
236
|
+
|
|
237
|
+
## What is deliberately not here
|
|
238
|
+
|
|
239
|
+
No wake word, no speaker diarization, no telephony, no acoustic echo
|
|
240
|
+
cancellation, and no turn detection โ that last one lives in `vocal-helper`,
|
|
241
|
+
which ships Silero VAD and a semantic end-of-turn stage. Reimplementing it
|
|
242
|
+
here would give it two owners. KNOWLEDGE.md ยง10 explains each.
|
|
243
|
+
|
|
244
|
+
## Author
|
|
245
|
+
|
|
246
|
+
[Warith HARCHAOUI](https://warith-harchaoui.github.io/) โ
|
|
247
|
+
warithmetics@deraison.ai
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
BSD-3-Clause. See [LICENSE](https://github.com/warith-harchaoui/discussion-helper/blob/main/LICENSE).
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Discussion Helper
|
|
2
|
+
|
|
3
|
+
[๐ซ๐ท](https://github.com/warith-harchaoui/discussion-helper/blob/main/LISEZMOI.md) ยท [๐ฌ๐ง](https://github.com/warith-harchaoui/discussion-helper/blob/main/README.md)
|
|
4
|
+
|
|
5
|
+
[](https://github.com/warith-harchaoui/discussion-helper/blob/main/LICENSE) [](#) [](#the-promise)
|
|
6
|
+
|
|
7
|
+
`Discussion Helper` belongs to a collection of libraries called `AI Helpers`
|
|
8
|
+
developed for building Artificial Intelligence.
|
|
9
|
+
|
|
10
|
+
[๐ AI Helpers](https://deraison.ai/ai-helpers)
|
|
11
|
+
|
|
12
|
+
**Hold a spoken conversation at the microphone: listen, answer, speak โ and
|
|
13
|
+
stop the moment the user cuts in.**
|
|
14
|
+
|
|
15
|
+
`vocal-helper` hears. `speaker-helper` speaks. A local language model answers.
|
|
16
|
+
None of them knows whose turn it is. This package is the loop between the
|
|
17
|
+
three, and the three questions it has to get right:
|
|
18
|
+
|
|
19
|
+
- **Who holds the floor.** Exactly one party at a time. Most voice-agent
|
|
20
|
+
failures are two parties believing they hold it โ the agent talking over the
|
|
21
|
+
user is the obvious one; both waiting in silence is the one nobody notices
|
|
22
|
+
until a recording is played back.
|
|
23
|
+
- **What happens when the user cuts in.** Under **150 ms** from their first
|
|
24
|
+
syllable to the agent's silence, and the audio already synthesised and not
|
|
25
|
+
yet heard is *discarded*, not drained. Playing it out because it exists is
|
|
26
|
+
what makes an agent feel deaf.
|
|
27
|
+
- **Where the latency went.** A single total says a conversation feels slow
|
|
28
|
+
and nothing about why. Every exchange reports its breakdown โ endpointing,
|
|
29
|
+
recognition, first token, first audio โ and names the stage to fix.
|
|
30
|
+
|
|
31
|
+
## The promise
|
|
32
|
+
|
|
33
|
+
**Local-first.** The microphone, the recogniser, the model and the voice all
|
|
34
|
+
run on your machine. No audio leaves it, no account, no per-minute billing.
|
|
35
|
+
|
|
36
|
+
**Testable without a room.** The three parts are protocols, so the whole loop
|
|
37
|
+
runs in CI with no microphone, no model and no sound card. That is not
|
|
38
|
+
tidiness: interruption handling is exactly the behaviour a real room makes
|
|
39
|
+
impossible to assert. You cannot *hear* whether the queued audio was
|
|
40
|
+
discarded.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install 'discussion-helper[all]' # hearing, thinking, speaking, playing
|
|
46
|
+
speaker-helper install-voice --engine kokoro # the voice's weights, once
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
On Linux and macOS the voice also needs `espeak-ng`
|
|
50
|
+
(`apt install espeak-ng`, `brew install espeak-ng`).
|
|
51
|
+
|
|
52
|
+
Each half installs on its own if you only want some of it: `[listen]`,
|
|
53
|
+
`[think]`, `[speak]`, `[play]`.
|
|
54
|
+
|
|
55
|
+
## Try it with nothing installed
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install discussion-helper
|
|
59
|
+
discussion-helper rehearse "bonjour" "comment รงa va ?"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`rehearse` runs the **same loop** with the microphone, model and speaker
|
|
63
|
+
replaced by fakes. When a voice agent misbehaves the first question is always
|
|
64
|
+
"is it the loop or is it the room", and this answers it in a second:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# stage an interruption a quarter of a second into the reply
|
|
68
|
+
discussion-helper rehearse "dis-moi tout" --sentences 6 --interrupt-after 0.25 --grace 0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
you: dis-moi tout
|
|
73
|
+
it: dis-moi tout dis-moi tout (2) (cut off)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Talk to it
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
discussion-helper talk --voice ff_siwis
|
|
80
|
+
discussion-helper talk --no-barge-in # strict half-duplex
|
|
81
|
+
discussion-helper talk --json # transcript + latency breakdown
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## As a library
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import asyncio
|
|
88
|
+
from discussion_helper import Discussion, Policy
|
|
89
|
+
from discussion_helper.adapters import VocalListener, LocalResponder, SpeakerVoice
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def main():
|
|
93
|
+
async with Discussion(
|
|
94
|
+
VocalListener(language="fr"),
|
|
95
|
+
LocalResponder(),
|
|
96
|
+
SpeakerVoice(voice="ff_siwis"),
|
|
97
|
+
Policy(barge_in_grace_s=0.35),
|
|
98
|
+
) as talk:
|
|
99
|
+
conversation = await talk.run()
|
|
100
|
+
|
|
101
|
+
for turn in conversation.utterances:
|
|
102
|
+
print(turn.speaker, turn.text)
|
|
103
|
+
print(conversation.timings[0].as_dict())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
asyncio.run(main())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Bring your own parts by implementing `Listener`, `Responder` or `Voice` โ a
|
|
110
|
+
cloud recogniser, a scripted model for a demo, a phone line instead of a
|
|
111
|
+
speaker. Each is one class, and the loop does not change.
|
|
112
|
+
|
|
113
|
+
## Over HTTP, and over a socket
|
|
114
|
+
|
|
115
|
+
A conversation does not fit a request โ it has no single answer and lasts
|
|
116
|
+
minutes โ so the HTTP surface is a session you create, a stream you follow and
|
|
117
|
+
a button you press:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install 'discussion-helper[server]'
|
|
121
|
+
discussion-helper-mcp # or: uvicorn discussion_helper.api:build --factory
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
| Route | What it is |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `GET /health` | whether each of the three parts could run here, and what is missing |
|
|
127
|
+
| `POST /sessions` | start one; returns an id immediately |
|
|
128
|
+
| `GET /sessions` | every conversation this process is holding |
|
|
129
|
+
| `GET /sessions/{id}` | the transcript, the floor and the timings, as they stand |
|
|
130
|
+
| `GET /sessions/{id}/events` | follow it โ floor, turns, latency, as they happen |
|
|
131
|
+
| `POST /sessions/{id}/say` | push a user turn in as text, for a remote session |
|
|
132
|
+
| `POST /sessions/{id}/interrupt` | stop the agent, through the same path a barge-in uses |
|
|
133
|
+
| `DELETE /sessions/{id}` | end it, and get what was said |
|
|
134
|
+
| `WS /sessions/{id}/audio` | **the microphone at the other end** |
|
|
135
|
+
| `POST /turn` | one exchange, no audio โ the shape an agent can use |
|
|
136
|
+
| `/mcp` | the same routes as agent tools |
|
|
137
|
+
|
|
138
|
+
The WebSocket carries real audio both ways: the client sends PCM and control
|
|
139
|
+
frames, the server sends events and WAV. That is how every hosted voice agent
|
|
140
|
+
works, and it means the browser can hold the microphone while this end holds
|
|
141
|
+
the model. Two duties fall on the client and are not optional โ send
|
|
142
|
+
`{"type":"speaking"}` on the **first syllable** so a barge-in can travel, and
|
|
143
|
+
**drop** buffered audio on `{"kind":"stop"}`. A client that plays its buffer
|
|
144
|
+
out reproduces, at the far end, exactly the deafness that stopping it here
|
|
145
|
+
was meant to prevent.
|
|
146
|
+
|
|
147
|
+
### In a container
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
docker compose up -d
|
|
151
|
+
curl -s localhost:8090/health
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The image serves the **remote** transport โ the caller owns the microphone and
|
|
155
|
+
sends frames โ and that is the honest half of this package a container can
|
|
156
|
+
run. It does **not** open a microphone of its own: `--transport local` reads
|
|
157
|
+
the machine's sound card, and a container has none. Passing one in is
|
|
158
|
+
`--device /dev/snd` on Linux and impossible on macOS, which is a per-machine
|
|
159
|
+
arrangement rather than an image, so the `listen` extra is deliberately left
|
|
160
|
+
out and the refusal happens at install time instead of at the first call.
|
|
161
|
+
|
|
162
|
+
The port is bound to the loopback, and the reason is in `discussion-helper-mcp
|
|
163
|
+
--help` as well: this server can open a microphone. One worker, also
|
|
164
|
+
deliberately โ the voice backend loads its ONNX graph once per process and a
|
|
165
|
+
second worker pays for its own copy, and sessions are stateful besides.
|
|
166
|
+
|
|
167
|
+
## Read this before tuning anything
|
|
168
|
+
|
|
169
|
+
**[KNOWLEDGE.md](https://github.com/warith-harchaoui/discussion-helper/blob/main/KNOWLEDGE.md)** is the design notebook: the 800 ms latency
|
|
170
|
+
budget and where it actually goes, why a silence threshold is not turn
|
|
171
|
+
detection, the two mistakes everyone makes with barge-in, why replies are
|
|
172
|
+
chunked at sentences and not tokens, and why acoustic echo is the problem that
|
|
173
|
+
decides your defaults.
|
|
174
|
+
|
|
175
|
+
The short version: a voice agent has about 800 ms before the conversation
|
|
176
|
+
feels broken, the largest line in that budget is endpointing rather than the
|
|
177
|
+
model, and the agent's own voice reaching its own microphone looks exactly
|
|
178
|
+
like the user speaking.
|
|
179
|
+
|
|
180
|
+
## How this differs from its neighbours
|
|
181
|
+
|
|
182
|
+
| You have | Reach for |
|
|
183
|
+
|---|---|
|
|
184
|
+
| A recording somebody needs as text | [`vocal-helper`](https://github.com/warith-harchaoui/vocal-helper) |
|
|
185
|
+
| Text somebody needs as speech | [`speaker-helper`](https://github.com/warith-harchaoui/speaker-helper) |
|
|
186
|
+
| A live camera or microphone as frames | [`capture-helper`](https://github.com/warith-harchaoui/capture-helper) |
|
|
187
|
+
| **A conversation to hold** | this one |
|
|
188
|
+
|
|
189
|
+
## What is deliberately not here
|
|
190
|
+
|
|
191
|
+
No wake word, no speaker diarization, no telephony, no acoustic echo
|
|
192
|
+
cancellation, and no turn detection โ that last one lives in `vocal-helper`,
|
|
193
|
+
which ships Silero VAD and a semantic end-of-turn stage. Reimplementing it
|
|
194
|
+
here would give it two owners. KNOWLEDGE.md ยง10 explains each.
|
|
195
|
+
|
|
196
|
+
## Author
|
|
197
|
+
|
|
198
|
+
[Warith HARCHAOUI](https://warith-harchaoui.github.io/) โ
|
|
199
|
+
warithmetics@deraison.ai
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
BSD-3-Clause. See [LICENSE](https://github.com/warith-harchaoui/discussion-helper/blob/main/LICENSE).
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
discussion-helper โ a spoken conversation, with the floor handled properly.
|
|
3
|
+
|
|
4
|
+
Module summary
|
|
5
|
+
--------------
|
|
6
|
+
``vocal-helper`` hears. ``speaker-helper`` speaks. A language model answers.
|
|
7
|
+
None of them knows whose turn it is, and that is what this package is: the
|
|
8
|
+
loop between the three, and the three questions it has to get right.
|
|
9
|
+
|
|
10
|
+
**Who holds the floor.** Exactly one party at a time. Most voice-agent
|
|
11
|
+
failures are two parties believing they hold it โ the agent talking over the
|
|
12
|
+
user is the obvious one, both waiting in silence is the one nobody notices.
|
|
13
|
+
|
|
14
|
+
**What happens when the user cuts in.** Under 150 ms from their first syllable
|
|
15
|
+
to the agent's silence, and the audio already synthesised and not yet heard is
|
|
16
|
+
*discarded*, not drained. Playing it out because it exists is what makes an
|
|
17
|
+
agent feel deaf.
|
|
18
|
+
|
|
19
|
+
**Where the latency went.** A single total says a conversation feels slow and
|
|
20
|
+
nothing about why. Every exchange reports its breakdown โ endpointing,
|
|
21
|
+
recognition, first token, first audio โ and names the stage to fix.
|
|
22
|
+
|
|
23
|
+
The three parts are protocols, so the whole loop runs in a test with no
|
|
24
|
+
microphone, no model and no sound card. That is not tidiness: interruption
|
|
25
|
+
handling is exactly the behaviour a real room makes impossible to assert.
|
|
26
|
+
|
|
27
|
+
Usage example
|
|
28
|
+
-------------
|
|
29
|
+
>>> import asyncio
|
|
30
|
+
>>> from discussion_helper import Discussion
|
|
31
|
+
>>> from discussion_helper.testing import ScriptedListener, EchoResponder, SilentVoice
|
|
32
|
+
>>> talk = Discussion(ScriptedListener(["bonjour"]), EchoResponder(), SilentVoice())
|
|
33
|
+
>>> [u.text for u in asyncio.run(talk.run()).utterances]
|
|
34
|
+
['bonjour', 'bonjour']
|
|
35
|
+
|
|
36
|
+
See ``KNOWLEDGE.md`` for the design notes: the latency budget, turn detection,
|
|
37
|
+
barge-in, and why acoustic echo is the problem that decides your defaults.
|
|
38
|
+
|
|
39
|
+
Author
|
|
40
|
+
------
|
|
41
|
+
Warith HARCHAOUI โ https://linkedin.com/in/warith-harchaoui
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
|
47
|
+
__author__ = "Warith HARCHAOUI"
|
|
48
|
+
__email__ = "warithmetics@deraison.ai"
|
|
49
|
+
__license__ = "BSD-3-Clause"
|
|
50
|
+
|
|
51
|
+
from discussion_helper.discussion import Discussion, Policy
|
|
52
|
+
from discussion_helper.parts import Listener, Responder, Voice
|
|
53
|
+
from discussion_helper.types import (
|
|
54
|
+
Conversation,
|
|
55
|
+
Floor,
|
|
56
|
+
Speaker,
|
|
57
|
+
TurnTiming,
|
|
58
|
+
Utterance,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
# the loop
|
|
63
|
+
"Discussion",
|
|
64
|
+
"Policy",
|
|
65
|
+
# the parts it is written against
|
|
66
|
+
"Listener",
|
|
67
|
+
"Responder",
|
|
68
|
+
"Voice",
|
|
69
|
+
# what crosses the boundaries
|
|
70
|
+
"Conversation",
|
|
71
|
+
"Floor",
|
|
72
|
+
"Speaker",
|
|
73
|
+
"TurnTiming",
|
|
74
|
+
"Utterance",
|
|
75
|
+
"__version__",
|
|
76
|
+
]
|