discussion_helper.types module

What crosses the boundaries of a spoken conversation.

Module summary

A discussion is not a transcript and not a playlist. It is a sequence of turns, each held by exactly one party, with a measurable gap between the moment one ends and the moment the next is audible. The types here name those three things — the turn, the floor, and the gap — because every design question in this package turns out to be about one of them.

Nothing here imports a model, a microphone or a network. The whole state of a conversation is plain data, which is what lets the loop be tested with no hardware at all.

Author

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

class discussion_helper.types.Conversation(utterances=<factory>, floor=Floor.IDLE, timings=<factory>)[source]

Bases: object

Everything said so far, and whose turn it is.

The history is what the language model is shown, so it is kept as utterances rather than as a rendered prompt: a prompt is one consumer’s view of this, and building it here would make every other consumer read somebody else’s formatting.

Parameters:
add(utterance)[source]

Append a turn.

Parameters:

utterance (Utterance)

Return type:

None

as_messages()[source]

The history in the shape chat models expect.

Return type:

list[dict[str, str]]

floor: Floor = 'idle'
property interruption_rate: float

Share of agent turns the user cut into.

A diagnostic, not a score. A high rate usually means the agent answers at too great a length — which a transcript will not show, because the text of an interrupted answer looks complete.

property last_user_text: str

What the user most recently said, or "".

timings: list[TurnTiming]
utterances: list[Utterance]
class discussion_helper.types.Floor(*values)[source]

Bases: Enum

Who may speak, right now.

A spoken conversation has exactly one floor and exactly one holder. Most of the failures in a voice agent are two parties believing they hold it: the agent talking over the user is the obvious one, and both waiting in silence is the one nobody notices until a recording is played back.

The states are deliberately few. THINKING is separate from SPEAKING because they are interrupted differently — thinking can be abandoned silently, speaking has to be stopped, and the audio already queued has to be thrown away rather than drained.

IDLE = 'idle'

Nothing is happening. The loop has not started or has been stopped.

LISTENING = 'listening'

The user has the floor. The microphone is open and nothing is playing.

SPEAKING = 'speaking'

The agent has the floor. Audio is playing and a barge-in may arrive.

THINKING = 'thinking'

The user has finished and the answer is being produced. Silence, and the part of the budget the listener cannot shorten.

discussion_helper.types.Speaker

Who said a thing. "user" and "agent" rather than "human" and "assistant": the words a speech system needs are about who holds the floor, not about what either party is.

alias of Literal[‘user’, ‘agent’]

class discussion_helper.types.SpokenSentence(text, first_audio_s=0.0, played_s=0.0, cut=False)[source]

Bases: object

What came of asking the voice to say one sentence.

Two durations, because they answer different questions and the budget needs the first one. first_audio_s is the silence before the sound — the part of the gap the listener feels. played_s is how much audio they then heard, which says nothing about latency and everything about whether an interruption landed.

Collapsing the two into “how long did speaking take” is the measurement mistake this type exists to prevent: it makes a fast engine playing a long sentence look slower than a slow engine playing a short one.

Parameters:
  • text (str) – The sentence, as it was handed to the engine.

  • first_audio_s (float) – From the call to the first sample leaving the speaker.

  • played_s (float) – Seconds of audio actually heard. Less than the sentence’s full length when it was cut.

  • cut (bool) – Whether the sentence was stopped before it finished.

cut: bool = False
first_audio_s: float = 0.0
played_s: float = 0.0
text: str
class discussion_helper.types.TurnTiming(endpoint_s=0.0, recognise_s=0.0, think_s=0.0, speak_s=0.0)[source]

Bases: object

Where one turn’s latency went.

The number a listener feels is to_first_audio_s: the silence between their last word and the agent’s first sound. Everything else here is the breakdown that says which stage to fix, and the breakdown is the point — a single total tells you a conversation feels slow and nothing about why.

Typical shares, from the published budgets this package was built against: endpointing 150–350 ms, recognition 100–150 ms, the language model’s first token 150–400 ms, synthesis to first audio 100–200 ms. Under 800 ms end to end is the usual target; a well-tuned local pipeline lands near 600.

Parameters:
  • endpoint_s (float) – From the user’s last word to the decision that they had finished. The largest line, and the one a semantic turn detector shortens.

  • recognise_s (float) – Finishing the transcript once the turn is closed.

  • think_s (float) – To the first token of the answer. Not the whole answer: a reply is spoken sentence by sentence, so only the first one is on this path.

  • speak_s (float) – From that first sentence to the first sample leaving the speaker.

as_dict()[source]

The breakdown plus its total, for a log line or a report.

slowest_stage belongs in here rather than being bolted on by each caller. It was not, and the three doors disagreed: the text CLI named the stage to fix, the HTTP event stream merged the key in by hand, and the CLI’s own --json — the one a script reads — omitted it. One serialisation, so a reader gets the same breakdown whichever door they came through.

Return type:

dict[str, float | str]

endpoint_s: float = 0.0
recognise_s: float = 0.0
property slowest_stage: str

Which stage to fix first. Naming it is most of fixing it.

speak_s: float = 0.0
think_s: float = 0.0
property to_first_audio_s: float

The silence the user actually sits through.

class discussion_helper.types.Utterance(speaker, text, started_s=0.0, ended_s=0.0, interrupted=False)[source]

Bases: object

One thing somebody said, with when it was said.

Parameters:
  • speaker (str) – "user" or "agent".

  • text (str) – What was said. For the user this is the transcript and may be wrong; for the agent it is exactly what was synthesised.

  • started_s (float) – Seconds since the conversation began. For the user these bound the speech itself, not the recognition — the gap between the two is the single largest line in the latency budget, and conflating them would hide it.

  • ended_s (float) – Seconds since the conversation began. For the user these bound the speech itself, not the recognition — the gap between the two is the single largest line in the latency budget, and conflating them would hide it.

  • interrupted (bool) – Set on an agent turn the user cut into. Worth keeping: an agent that is interrupted often is usually one that answers at too great a length, and that is invisible in a transcript.

property duration_s: float

How long the speaking took.

ended_s: float = 0.0
interrupted: bool = False
speaker: Literal['user', 'agent']
started_s: float = 0.0
text: str