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.

Author

Warith HARCHAOUI — https://linkedin.com/in/warith-harchaoui

class discussion_helper.adapters.LocalResponder(*, system='', max_sentences=4, stream=True)[source]

Bases: object

Answers, 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.'
async aclose()[source]

Nothing is held open; the client is per-call.

Return type:

None

render(messages)[source]

The conversation so far, as one prompt.

chat takes 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.

Parameters:

messages (list[dict[str, str]])

Return type:

str

async reply(messages)[source]

Yield the answer one speakable sentence at a time.

Parameters:

messages (list[dict[str, str]])

Return type:

AsyncIterator[str]

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: object

Speaks, through speaker-helper, and can be stopped mid-sentence.

Parameters:
  • voice (str) – A built-in voice. Empty means “detect the language of the text and pick the voice that speaks it”.

  • language (str) – Passed through to the engine.

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.

async aclose()[source]

Release the engine and the audio device.

Return type:

None

async speak(sentence)[source]

Synthesise and play one sentence.

Parameters:

sentence (str)

Return type:

SpokenSentence

async stop()[source]

Stop the sound now and discard what was queued.

Return type:

None

class discussion_helper.adapters.VocalListener(*, language='', device_name=None, device_index=None, onset_rms=0.02)[source]

Bases: object

Hears, through vocal-helper and the microphone capture-helper opens.

The two questions in the Listener protocol 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-helper ships Silero VAD and a semantic end-of-turn stage modelled on LiveKit’s turn-detector; reimplementing either here would give it two owners.

async aclose()[source]

Release the microphone.

Return type:

None

async speech_started()[source]

Return as soon as the level tap says somebody is talking.

Return type:

None

async utterances()[source]

Yield each completed user turn, as the pipeline closes it.

Return type:

AsyncIterator[Utterance]

discussion_helper.adapters.split_sentences(text)[source]

Split text into speakable sentences.

Used to turn a token stream into the unit a voice reads well. Kept here rather than imported from speaker-helper so 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.

Parameters:

text (str)

Return type:

list[str]