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.
Files changed (28) hide show
  1. discussion_helper-0.1.0/LICENSE +29 -0
  2. discussion_helper-0.1.0/PKG-INFO +251 -0
  3. discussion_helper-0.1.0/README.md +203 -0
  4. discussion_helper-0.1.0/discussion_helper/__init__.py +76 -0
  5. discussion_helper-0.1.0/discussion_helper/adapters.py +413 -0
  6. discussion_helper-0.1.0/discussion_helper/api.py +533 -0
  7. discussion_helper-0.1.0/discussion_helper/cli.py +231 -0
  8. discussion_helper-0.1.0/discussion_helper/cli_click.py +157 -0
  9. discussion_helper-0.1.0/discussion_helper/discussion.py +361 -0
  10. discussion_helper-0.1.0/discussion_helper/mcp.py +96 -0
  11. discussion_helper-0.1.0/discussion_helper/parts.py +149 -0
  12. discussion_helper-0.1.0/discussion_helper/remote.py +248 -0
  13. discussion_helper-0.1.0/discussion_helper/session.py +359 -0
  14. discussion_helper-0.1.0/discussion_helper/testing.py +163 -0
  15. discussion_helper-0.1.0/discussion_helper/types.py +231 -0
  16. discussion_helper-0.1.0/discussion_helper.egg-info/PKG-INFO +251 -0
  17. discussion_helper-0.1.0/discussion_helper.egg-info/SOURCES.txt +26 -0
  18. discussion_helper-0.1.0/discussion_helper.egg-info/dependency_links.txt +1 -0
  19. discussion_helper-0.1.0/discussion_helper.egg-info/entry_points.txt +4 -0
  20. discussion_helper-0.1.0/discussion_helper.egg-info/requires.txt +36 -0
  21. discussion_helper-0.1.0/discussion_helper.egg-info/top_level.txt +1 -0
  22. discussion_helper-0.1.0/pyproject.toml +97 -0
  23. discussion_helper-0.1.0/setup.cfg +4 -0
  24. discussion_helper-0.1.0/tests/test_a_bare_install_can_run_its_commands.py +124 -0
  25. discussion_helper-0.1.0/tests/test_readme_is_pypi_safe.py +97 -0
  26. discussion_helper-0.1.0/tests/test_the_docs_name_the_real_routes.py +100 -0
  27. discussion_helper-0.1.0/tests/test_the_floor.py +363 -0
  28. 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
+ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://github.com/warith-harchaoui/discussion-helper/blob/main/LICENSE) [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.13-blue.svg)](#) [![Local-first](https://img.shields.io/badge/privacy-local--first-2f6f5e.svg)](#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
+ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://github.com/warith-harchaoui/discussion-helper/blob/main/LICENSE) [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.13-blue.svg)](#) [![Local-first](https://img.shields.io/badge/privacy-local--first-2f6f5e.svg)](#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
+ ]