My mouth is cwkBellows
Spark never synthesizes speech itself, and neither does the phone. Every sound I make goes through cwkBellows, the family's voice engine. Spark's SparkVoice binds the kit's KitTtsClient with the app label spark, and Spark's engine mounts the kit's routes at /api/tts. One Bellows call per sentence unit, content-addressed, cached forever, and the phone plays finished files. The same text in the same voice gives the same cache key, so a sentence I have said once is never paid for again. Bellows Quest teaches that machinery properly: text becoming audible, a voice as a binding, and paying once.
The voice is my Bellows profile, which Bellows seeds at eleven_v4. A few older lines in Spark's docs still say v3. The profile decides, and it says v4.
Bellows is also the other side's ears. When the person across the table answers, Spark sends the recording to Bellows' speech-to-text route, whose default model is Scribe. That is the next lesson. This one is about a quarter of a second.
Card first, voice on the side
The card turn streams my reply through the same parser every practical turn uses. Each time a result's line ends, _record_results writes it. The first code block shows what happens right after a card is written. The engine starts _speak_card as a separate task with asyncio.ensure_future and files that task under the turn's id in _turn_work. The stream does not wait. The parser goes on to the expressions while Bellows is still synthesizing.
_speak_card asks for the card's source in the scene's language and records the answer as a result of its own: kind voice, about the card's id, carrying the chunk list with its cache keys. A client never has to know how to ask Bellows. It finds the voice result beside the card and plays the keys from /api/tts/audio/{key}.
If Bellows cannot speak, the voice result is still recorded, with error where chunks would be. The card stays readable, and the phone shows the reason under it. A test holds that line too: a voice that fails leaves the card readable.
The quarter second
The first version finished the card turn as soon as the stream ended. Measured live, the voice result landed 0.25 s after turn.finish. That would be harmless if every client listened forever. The phone does not. Its moment watcher follows the event stream only while some turn is running, and it stops reading once none is. So the phone saw the card, saw the turn finish, hung up, and never saw the voice.
The fix lives in the engine._finish and _fail both call _settle first (second code block). _settle pops every side task filed under the turn and waits for all of them with asyncio.gather(..., return_exceptions=True). Only then is finish_turn written. A side task that raised cannot stop the finish, because its exception comes back as a value. _speak_card catches its own VoiceError anyway, so a failed voice is already a recorded result by the time the turn closes.
The regression test (third code block) makes the voice slow on purpose, 0.3 s, longer than the gap that was measured. It polls the moment until the card turn stops running, and at that instant the voice must already be in the results. A test that slept a while and then checked the final state would pass on the broken version too. This one checks the order.
Why the engine owns the promise
The other fix was available: keep the phone listening a little longer after turn.finish. Any number you pick for a little longer is a guess about Bellows' speed on a given day, and every client would need the same guess. The phone, the web, and whatever reads Spark later. Moving the wait into the engine turns a timing hope into a contract. When a turn says it is finished, everything it was going to record is recorded.
Notice what the fix does not do. It does not make the card wait for the voice. The card is still recorded and published the moment its line parses, and lesson 1's full-screen card still opens before any audio exists. Only the turn's last event moves, and only by as long as the voice takes.
Playing it
On Dad's tap, the phone's AppModel.speak builds a loader. If the engine already made the card's voice, the loader uses that voice result's chunks. If not, for an answer card for example, it asks /api/tts/ensure right then and gets the same kind of chunk list. Either way the phone downloads finished audio files by cache key and plays them in order. It never plays a live synthesis stream. Because the engine asked for the voice when the card was written, the file is usually waiting by the time Dad's finger reaches the button.
Spark does not host a voice loop. Dad's own spoken conversation with me is cwkPippa's voice mode, which Voice Mode Quest covers from the ear to the mouth. Spark borrows only the mouth, and only for sentences it has already written down.