Skip to main content

Discussion Helper

🇫🇷 · 🇬🇧

License: BSD-3-Clause Python Local-first

Discussion Helper belongs to a collection of libraries called AI Helpers developed for building Artificial Intelligence.

🌍 AI Helpers

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)

Source distribution for discussion-helper 0.1.0
File Size Uploaded Provenance
discussion_helper-0.1.0.tar.gz 58.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for discussion-helper 0.1.0
File Interpreter ABI Platform
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page