Skip to content
C.W.K.
Stream
Lesson 04 of 06 · published

Say It, Then Do It

~12 min · voice-chat, tool-use, speech-gate, streaming, ordering

Level 0Unlit
0 XP0/48 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"He heard 'I'll turn it on' after the air conditioner was already on. The words were right. Their order in time was wrong."

The Bug

In a spoken turn, Pippa says a short line before she uses a tool — "hold on, I'll turn it on" — and then the tool runs. The first panel waited for the whole streamed answer, then read it. So Dad heard the announcement after the action had already happened in the room. Nothing was technically lost; the conversation just stopped making sense, because speech is a promise about what comes next, and this promise arrived late.

Cut the Reply at Every Tool Call

The fix borrows the WebUI's rule and lives in the voice kit as ReplyUnits. While the reply streams, the text so far becomes a unit the moment a tool call begins — at its tool_input_start event, or at its tool_use when no start arrived. That unit is synthesized and read at once, while the stream keeps going. At the end of the answer, only the rest after the last cut is read. A stretch with nothing speakable in it (only cards or tags) is passed over. A stream_reset stops the reading, drops the queue and starts the cuts over.

stream:  "잠깐, 켜 볼게" | tool_use(a home device) | "켰어."
          ^ unit 1 read NOW        ^ waits for quiet   ^ unit 2 at the end

phase:   speaking ........ thinking ................ speaking -> listening

One Unit at a Time, Never Split

Units are read one at a time, in order, and a unit is never split to start sooner — the voice engine's rule, so a sentence keeps its prosody. The panel says speaking while a unit plays or waits, goes back to thinking between the line before a tool and the rest of the answer, and listens again only after the last unit of a finished answer. If an answer breaks off, nothing more is spoken, but lines already cut before the break were whole units and are read.

The Speech Gate: the Tool Waits for the Ear

Reading the line first is half of it. The other half is making the tool wait until Dad has actually heard the line. Queuing a line before a tool reports {"busy": true} to cwkPippa's speech gate (POST /api/conversations/{id}/playback). The queue draining reports false, and so do a cut-in, a pause, a stop, a leave and a reset. An action tool on the engine — a home device, an On Air effect — waits for quiet, up to 12 seconds. The reports go out in order and best effort, and the talk never awaits them: a slow server can delay an action, never the voice.

Cutting In Across Units

With several units in flight, "how far did he hear?" needs care. A key cut, a face click or a voice cut-in stops the whole queue. cwkPippa is told the playing unit's start plus how far into it the audio got, or the end of the last unit read through. If the cut lands while the reply still streams, the rest is passed over, the stream is followed quietly to its end so the report can reach the reply's id, and Dad's next turn in that conversation waits for that report. The record of what he heard stays true even when he interrupted.

When words announce actions, order them in time, not just in text. A stream is correct when its bytes are in the right order. A spoken turn is correct only when the listener hears each announcement before the thing it announces. Make the action wait for the ear, and make that wait a report the voice never blocks on.
Never split a unit to save a few hundred milliseconds. Starting a sentence's audio early by cutting it mid-clause buys latency and spends prosody. A listener forgives a beat of silence far more readily than a sentence read in two tones.

Code

The speech gate around one tool call (Firekeeper ↔ cwkPippa)·text
unit "잠깐, 켜 볼게" queued      -> POST /playback {"busy": true}
unit plays ...
cwkPippa: device tool           -> waits for busy=false (max 12 s)
queue drains                    -> POST /playback {"busy": false}
cwkPippa: tool runs
stream continues: "켰어." -> read at the end of the answer

Also reported as busy=false: cut-in, pause, stop, leave, stream_reset.
Reports are sent in order, each change once; the talk never awaits them.

Exercise

Find a system where a message announces an action ("sending now", "deleting 3 files") and the action can run before the announcement is seen or heard. Sketch the gate: what reports 'busy', what clears it, what is the ceiling, and what must never wait on the gate?
Hint
The gate needs four parts: a busy signal raised when the announcement is queued, a clear when it has been delivered (or abandoned by cut, pause or stop), a ceiling so a lost report cannot block the action forever, and a rule that the announcing side never waits on its own report.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.