Discussion Helper
Discussion Helper belongs to a collection of libraries called AI Helpers
developed for building Artificial Intelligence.
Hold a spoken conversation at the microphone: listen, answer, speak — and stop the moment the user cuts in.
vocal-helper hears. speaker-helper speaks. A local language model answers.
None of them knows whose turn it is. This package is the loop between the
three, and the three questions it has to get right:
- Who holds the floor. Exactly one party at a time. Most voice-agent failures are two parties believing they hold it — the agent talking over the user is the obvious one; both waiting in silence is the one nobody notices until a recording is played back.
- What happens when the user cuts in. Under 150 ms from their first syllable to the agent's silence, and the audio already synthesised and not yet heard is discarded, not drained. Playing it out because it exists is what makes an agent feel deaf.
- Where the latency went. A single total says a conversation feels slow and nothing about why. Every exchange reports its breakdown — endpointing, recognition, first token, first audio — and names the stage to fix.
The promise
Local-first. The microphone, the recogniser, the model and the voice all run on your machine. No audio leaves it, no account, no per-minute billing.
Testable without a room. The three parts are protocols, so the whole loop runs in CI with no microphone, no model and no sound card. That is not tidiness: interruption handling is exactly the behaviour a real room makes impossible to assert. You cannot hear whether the queued audio was discarded.
Install
pip install 'discussion-helper[all]' # hearing, thinking, speaking, playing
speaker-helper install-voice --engine kokoro # the voice's weights, once
On Linux and macOS the voice also needs espeak-ng
(apt install espeak-ng, brew install espeak-ng).
Each half installs on its own if you only want some of it: [listen],
[think], [speak], [play].
Try it with nothing installed
pip install discussion-helper
discussion-helper rehearse "bonjour" "comment ça va ?"
rehearse runs the same loop with the microphone, model and speaker
replaced by fakes. When a voice agent misbehaves the first question is always
"is it the loop or is it the room", and this answers it in a second:
# stage an interruption a quarter of a second into the reply
discussion-helper rehearse "dis-moi tout" --sentences 6 --interrupt-after 0.25 --grace 0
you: dis-moi tout
it: dis-moi tout dis-moi tout (2) (cut off)
Talk to it
discussion-helper talk --voice ff_siwis
discussion-helper talk --no-barge-in # strict half-duplex
discussion-helper talk --json # transcript + latency breakdown
As a library
import asyncio
from discussion_helper import Discussion, Policy
from discussion_helper.adapters import VocalListener, LocalResponder, SpeakerVoice
async def main():
async with Discussion(
VocalListener(language="fr"),
LocalResponder(),
SpeakerVoice(voice="ff_siwis"),
Policy(barge_in_grace_s=0.35),
) as talk:
conversation = await talk.run()
for turn in conversation.utterances:
print(turn.speaker, turn.text)
print(conversation.timings[0].as_dict())
asyncio.run(main())
Bring your own parts by implementing Listener, Responder or Voice — a
cloud recogniser, a scripted model for a demo, a phone line instead of a
speaker. Each is one class, and the loop does not change.
Over HTTP, and over a socket
A conversation does not fit a request — it has no single answer and lasts minutes — so the HTTP surface is a session you create, a stream you follow and a button you press:
pip install 'discussion-helper[server]'
discussion-helper-mcp # or: uvicorn discussion_helper.api:build --factory
| Route | What it is |
|---|---|
GET /health |
whether each of the three parts could run here, and what is missing |
POST /sessions |
start one; returns an id immediately |
GET /sessions |
every conversation this process is holding |
GET /sessions/{id} |
the transcript, the floor and the timings, as they stand |
GET /sessions/{id}/events |
follow it — floor, turns, latency, as they happen |
POST /sessions/{id}/say |
push a user turn in as text, for a remote session |
POST /sessions/{id}/interrupt |
stop the agent, through the same path a barge-in uses |
DELETE /sessions/{id} |
end it, and get what was said |
WS /sessions/{id}/audio |
the microphone at the other end |
POST /turn |
one exchange, no audio — the shape an agent can use |
/mcp |
the same routes as agent tools |
The WebSocket carries real audio both ways: the client sends PCM and control
frames, the server sends events and WAV. That is how every hosted voice agent
works, and it means the browser can hold the microphone while this end holds
the model. Two duties fall on the client and are not optional — send
{"type":"speaking"} on the first syllable so a barge-in can travel, and
drop buffered audio on {"kind":"stop"}. A client that plays its buffer
out reproduces, at the far end, exactly the deafness that stopping it here
was meant to prevent.
In a container
docker compose up -d
curl -s localhost:8090/health
The image serves the remote transport — the caller owns the microphone and
sends frames — and that is the honest half of this package a container can
run. It does not open a microphone of its own: --transport local reads
the machine's sound card, and a container has none. Passing one in is
--device /dev/snd on Linux and impossible on macOS, which is a per-machine
arrangement rather than an image, so the listen extra is deliberately left
out and the refusal happens at install time instead of at the first call.
The port is bound to the loopback, and the reason is in discussion-helper-mcp --help as well: this server can open a microphone. One worker, also
deliberately — the voice backend loads its ONNX graph once per process and a
second worker pays for its own copy, and sessions are stateful besides.
Read this before tuning anything
KNOWLEDGE.md is the design notebook: the 800 ms latency budget and where it actually goes, why a silence threshold is not turn detection, the two mistakes everyone makes with barge-in, why replies are chunked at sentences and not tokens, and why acoustic echo is the problem that decides your defaults.
The short version: a voice agent has about 800 ms before the conversation feels broken, the largest line in that budget is endpointing rather than the model, and the agent's own voice reaching its own microphone looks exactly like the user speaking.
How this differs from its neighbours
| You have | Reach for |
|---|---|
| A recording somebody needs as text | vocal-helper |
| Text somebody needs as speech | speaker-helper |
| A live camera or microphone as frames | capture-helper |
| A conversation to hold | this one |
What is deliberately not here
No wake word, no speaker diarization, no telephony, no acoustic echo
cancellation, and no turn detection — that last one lives in vocal-helper,
which ships Silero VAD and a semantic end-of-turn stage. Reimplementing it
here would give it two owners. KNOWLEDGE.md §10 explains each.
Author
Warith HARCHAOUI — warithmetics@deraison.ai
License
BSD-3-Clause. See LICENSE.
Metadata
Release files for discussion-helper 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| discussion_helper-0.1.0.tar.gz | 58.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| discussion_helper-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 109.0 kB
Release files / discussion_helper-0.1.0.tar.gz
| Download URL | discussion_helper-0.1.0.tar.gz |
|---|---|
| Size | 58.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
076b1036d8327eb01681918a49384e7123cb82d522d175e44642fd6f29994667
|
|
BLAKE2b-256 checksum How to use checksums |
b7dcdeda5e77fd2c5131bb3e9fa333d3505b4061b624031b987ddf3feb6a2217
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|
Release files / discussion_helper-0.1.0-py3-none-any.whl
| Download URL | discussion_helper-0.1.0-py3-none-any.whl |
|---|---|
| Size | 50.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
26e956f57c6387631829de40802c60e2acec459d00b2dc8f97a62e521466b58e
|
|
BLAKE2b-256 checksum How to use checksums |
e466c730aeaaf4d22d0c8f42595e937ba89cde45792371f0e9e5f417232482db
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|
Security for discussion-helper 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.