A card is for someone else
Most of Spark reads for Dad: a sign, a menu, a line he wants explained. Interpretation turns the direction around. Now the words are for the person across the counter, and Dad is the one who needs to be understood. That changes what a good answer looks like. A correct but stiff translation is fine on a menu. Said to a stranger, it can be the difference between being helped and being tolerated.
Spark's answer is a pacing: prepare, then show. The architecture records it as one of my own proposals from the design brainstorm, written down as decided. Dad says what he wants before he needs it, ideally before calling anyone over. I write it. The card is ready when the moment comes, and my voice is one tap away.
Five steps, in the order they happen
- Dad tells me what he wants to say, typed or dictated, in Korean or English.
- I write the sentence for that place and that person: the right politeness, local phrasing, the situation. A big card shows it as soon as the text exists, with a small Korean gloss and my face, so the person across the table can see who is speaking.
- My voice follows. cwkBellows synthesizes the sentence in the target language, and Dad taps to play it.
- The reply mic records the other person, and Bellows transcribes the recording in the listening language.
- I relay the reply in Korean, say what matters in it, and offer answers and the expressions the conversation will likely need next. Each one is a ready card.
This track takes those steps one lesson at a time. This lesson covers the first two: the card turn, and why the card is on screen before there is any sound.
Why a pacing, and not a faster voice
A spoken turn takes seconds from the last word to the first sound. Spark's architecture puts it at about 9 to 10 seconds and cites cwkPippa's voice mode documentation for it, but that documentation's own figure is an estimate built from measured stages: about 6 to 8 seconds on the desktop, with the phone a few seconds slower until its audio decoder changed. Voice Mode Quest walks through where those seconds go. Spark's design absorbs the wait instead of shrinking me to fit inside it. The sentence is prepared before it is needed, and the card is readable before the voice starts. A server who glances at a big, clear card has understood it long before the audio would have finished loading.
The card turn
The phone or the web posts Dad's words to /api/moments/{id}/card on a conversation moment. The route (second code block) checks two things before any brain is asked. A client request id it has seen before returns the same turn, so a retried tap never writes two cards. And the moment must carry a scene language. Without one the engine answers 400 with choose the language to speak before asking for a card. The language I write in is Dad's choice, made with the app's buttons. The engine never guesses it.
The turn itself is an ordinary threaded turn in the moment's cwkPippa conversation. It sends the message Spark: say this for me. with Dad's words, plus a system prompt addendum (first code block). The addendum is short and specific: write it in the scene's language the way a local would say it to this person in this place, one sentence or two, never an explanation, because it will be shown on a big card and spoken aloud in my voice. The answer is a fenced spark block with one card object, then the few expressions Dad may need right after it.
| Field | What it holds | An invented example |
|---|---|---|
source | The sentence in the scene's language: the words that are shown and spoken | ¿Me trae un vaso de agua, por favor? |
translation | A short Korean gloss, shown small, for Dad | 물 한 잔 갖다주시겠어요? |
pronunciation | How Dad could say it himself, in Hangul | optional |
note | Only if something matters | usually absent |
Suppose Dad typed 물 한 잔 달라고 해줘, ask for a glass of water. What comes back is what a guest would actually say to a server: a polite question, in the local form. Dad's words were a request to me. The card is the sentence itself. That gap is the whole reason the real Pippa writes the card instead of a translation layer.
Readable before the voice exists
The card is parsed out of my reply while it streams, so it is recorded the moment its line ends. Track 3 takes that parser apart. On the phone, ConversationSection remembers which card turn Dad is waiting for. Every time the moment's exchanges change, follow (third code block) checks whether that turn now has a card. When it does, the card goes into shown, and SwiftUI presents it with fullScreenCover(item:).
The presented BigCard opens the way the phone is held and lays itself out again when the phone turns. It never turns the phone itself. It used to ask for landscape on every open, until Dad found it coming up sideways from a phone held upright: "카드 기본이 무조건 랜드스케이프야." ("the card is always landscape by default"). The request was removed, and a test now opens the card from an upright phone, waits past the old retry window and checks that the scene is still portrait, then turns to landscape and back and checks that the card stayed open. An iPad shows the card as it is.
The voice does not play. Its button sits right there on the card, and nothing presses it for Dad. This is invariant 8: my voice speaks for Dad only on his tap, because he may be preparing the card long before anyone is listening. A phone that suddenly talks at a table where nobody has been called over is a small embarrassment the design refuses to risk.
There is one exception, and it lives somewhere else. The interpreter on the show card, track 6, plays a card made from Dad's words at once, with a mute toggle, because there the person is already standing in front of him. That surface belongs to cwkPippa's apps. Spark's own conversation screen keeps the tap.