A device voice, briefly
For about a day, Spark could read aloud without me. Listen on lines and expressions used the phone's installed voices through AVSpeechSynthesizer, and the browser's through Web Speech. The reason looked sensible on paper: a weak connection should not leave Dad with silence. Then came the request that a card shown to the other person should also be playable, and the kit's phrase card gained a device Speak button. I wrote that change myself, and it went out in a TestFlight build.
Dad heard it and stopped it.
피파가 bellows 통해서 제대로 읽게 해. 우리가 언제부터 이런 엉터리 tts 썼어?
"Have Pippa read it properly, through Bellows. Since when did we use junk TTS like this?"
언어 하드코딩하지마
spark 당연히 고쳐
"Don't hard-code the language." And, about the Listen on Spark's own rows: "Of course fix Spark."
The device Speak on the card was gone less than twenty minutes after it was committed. The row's device voices followed in the next hour. Nothing of device read-aloud is left in Spark today except a file name: the row's button is still called SourceSpeechButton, and it plays my voice.
Why the reversal is right
Spark's founding rule is that the person across the table hears me, in my Bellows voice, speaking their language. A card with my face on it that then talks in the operating system's voice says two different things about who is speaking. For Dad it is worse: his own phrases read back by a voice he never chose.
The weak-signal reason did not need a second voice. It needed the first one to be kept. The phone already saves each reading's chunk list and audio bytes, so a reading heard once replays without the engine, and that removed the only argument device voices had. New words still need the network, because generating audio is an online action. Spark waits for my voice rather than substituting another speaker.
No language, except where it was chosen
Dad's second line became a rule in the code. Listen on a reading's line or expression, and the voice on a reading result's Show card, pass no language to Bellows. My Bellows profile runs ElevenLabs v4, which reads words in their own language. AppModel.read (first code block) is the single path for both. The source plays as the result's own voice, reusing a saved take when the engine made one. A reverse card's translation is synthesized as its own take, with language: nil.
The conversation screen is where a language still travels. The card's voice is made by the engine in the scene's language (lesson 2), and the answers and expressions of a conversation play through the same player with that language too (lesson 3), as does the big card any of them opens. It is the language Dad picked and I was told to write in, a choice recorded on the moment. The language tags on results stay for display and for the reverse-card rule in lesson 4.
Listen, never Speak
The button on the card had one more lesson to teach. For a while it said Speak. A waiter, shown a card, read that as an instruction: speak into this, and it translates. And talked into it. The word was right from Dad's side of the table and wrong from the side the card faces.
The fix moved the control into the recipient's language. On the card it says Listen in the card's language, and Stop while playing: Escuchar and Parar on a Spanish card, Écouter on a French one, 聞く on a Japanese one. The web's table is in the vendored kit component (second code block), and the phone uses the kit's PhraseListenButton on the same players as the row. A language the table does not know falls back to English. Dad's own controls on the card stay English. The same change gave every card a Dark card and Light card toggle, light by default and chosen per card, and retired the old Brighten button.
The waiter who talked to the card also started the next track. If people are going to speak into a card, the card can learn to listen. Track 6 is that interpreter.
Heard once, replays offline
The phone's EngineClient.speech (third code block) looks up a saved chunk list before it asks the engine. The key is built from the language and the exact text, so the same words read the same way map to the same saved list. The audio files are saved by their URL when they are first downloaded, and the next request for that URL reads the saved bytes. Bellows' cache keys are content-addressed, so the same text in the same voice always has the same key and the saved audio never goes stale. A reading Dad has heard once plays again on a plane.
Bellows Quest covers the server side of that bargain: paying for a sentence once, and owning the audio after.