discussion_helper.adapters module
Binding the three protocols to the real siblings.
Module summary
discussion_helper.parts says what a listener, a responder and a voice
must do. This module says who actually does it: vocal-helper hears,
best-engine-ai-helper answers, speaker-helper speaks.
Every import here is inside a function. A base install of this package carries the loop and nothing else — no recogniser, no model, no sound card — and that is what lets the floor and interruption logic be tested in CI. An adapter that imported its sibling at module scope would take that away from everyone who only wanted the loop.
Each adapter is thin on purpose. When one of them starts holding logic, the behaviour it holds has two owners — the sibling that fought for it and the copy here — and the two drift. The one thing these are allowed to own is shape: turning a stream of recogniser events into completed turns, or a stream of tokens into speakable sentences.
- class discussion_helper.adapters.LocalResponder(*, system='', max_sentences=4, stream=True)[source]
Bases:
objectAnswers, with whatever model fits this machine.
- Parameters:
system (str) – The instruction the model is given. The default asks for brevity, and that is a latency decision rather than a style one — see below.
max_sentences (int) – Stop after this many sentences even if the model keeps going.
here (The streaming that is not)
costs (and what it)
-------------------------------------------------
not (best_engine_ai_helper.llm.chat returns a finished string. It does)
tokens (stream)
the (so the usual trick of speaking the first sentence while)
path (rest is still being written is not available on this)
it (and)
a (would be dishonest to pretend otherwise by yielding sentences from)
complete. (string that is already)
budget (The consequence is concrete and belongs in the)
generation (the whole)
client (and why max_sentences exists — on a non-streaming)
answer (spends 150–400 ms on that line; this one spends however long the)
two (takes. Which is exactly why the default system prompt asks for one or)
sentences
client
stream (bool)
:param : :param answer length is latency: :param directly and entirely.: :param Sentences are still the unit yielded: :type Sentences are still the unit yielded: it is :param and that still earns its keep: :type and that still earns its keep: it is :param what gives a barge-in somewhere to land: :param and what keeps the synthesiser: :param reading whole sentences rather than clauses.:
- DEFAULT_SYSTEM = 'You are speaking out loud, not writing. Answer in one or two short sentences. No lists, no markdown, no headings — none of it can be heard. If the question needs a long answer, give the short one and offer to go further.'
- render(messages)[source]
The conversation so far, as one prompt.
chattakes a prompt and a system instruction, not a message list, so the history has to be rendered. Written as a transcript with named turns rather than concatenated: without the names, a model answering the third turn cannot tell its own earlier words from the user’s, and starts agreeing with itself.
- async warmup()[source]
Pay the model client’s first-call cost before anybody is waiting.
Measured on this stack: the first call in a process takes 4.8 s because the content-safety scan loads a classifier; every call after takes 0.25 s. Unwarmed, those four and a half seconds land on the user’s first question — the single turn where somebody is deciding whether this thing works at all.
Failures are swallowed on purpose. This is a pre-load; if the model is unreachable the real call will say so, with a better message and at a moment the caller asked for.
- Return type:
None
- class discussion_helper.adapters.SpeakerVoice(*, voice='', language='')[source]
Bases:
objectSpeaks, through
speaker-helper, and can be stopped mid-sentence.- Parameters:
Notes
stop()cancels the playback task and clears the queue. Draining it instead — playing out what was already synthesised — is the mistake that makes an agent feel deaf, and it is the whole reason this method exists rather than a flag the loop checks between sentences.
- class discussion_helper.adapters.VocalListener(*, language='', device_name=None, device_index=None, onset_rms=0.02)[source]
Bases:
objectHears, through
vocal-helperand the microphonecapture-helperopens.The two questions in the
Listenerprotocol are answered by two different parts of that pipeline, and keeping them apart is the point: completed turns come from the end of it (VAD, then semantic end-of-turn, then recognition), while “is somebody talking right now” comes from the VAD stage alone. A barge-in that waited for a transcript would arrive a second late — by which time the agent has been talking over the user for a sentence.- Parameters:
language (str) – Hint for the recogniser. Empty lets it detect.
device_name (str or int, optional) – Which microphone. Resolved by
capture-helper.device_index (str or int, optional) – Which microphone. Resolved by
capture-helper.onset_rms (float) – Level above which a frame counts as somebody starting to talk, used only to answer
speech_started(). The default suits a headset at a normal distance; raise it in a noisy room, where the cost of getting it wrong is an agent that interrupts itself.
Notes
This package does no turn detection of its own.
vocal-helperships Silero VAD and a semantic end-of-turn stage modelled on LiveKit’sturn-detector; reimplementing either here would give it two owners.- async speech_started()[source]
Return as soon as the level tap says somebody is talking.
- Return type:
None
- discussion_helper.adapters.split_sentences(text)[source]
Split
textinto speakable sentences.Used to turn a token stream into the unit a voice reads well. Kept here rather than imported from
speaker-helperso a caller can stream a reply without the speech extra installed — the rule is four lines, and a dependency for four lines is a worse trade than the duplication.