Skip to content
C.W.K.
Lesson 01 of 05 · published

A Sitting Is a Conversation Moment

~14 min · interpreter, architecture, ownership, cross-repo

Level 0First Word
0 XP0/47 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

A button read the wrong way

The show card is the phrase Dad turns toward someone: Pippa's sentence in their language, large, a small Korean line for him, and a button that plays her voice. That button used to say "Speak". A waiter read it as "speak into this and it translates" and talked at the phone. The button became Listen, and Dad turned the misreading into a feature. People already talk to the card, so let the card listen.

실제로 그런 기능을 넣는 건 어때? 이건 아빠한테도 유용할 거야. 카드에서 바로 말을 하면 (어떤 언어로 입력되든 번역어를 판단해서 A -> B 언어로) 실제 번역해주는 기능. 아빠가 쓰면 바로 주문 카드가 되는 식이고. 상대 원어민이 쓰면 아빠가 볼 수 있는 우리말이 나오는 식이고. 실시간 번역인 거지. 대신 피파가 문맥을 알아서 판단하는 거고.

Dad's ask, in English: speak right into the card in any language, and it works out the direction and really translates. When he speaks, it becomes an order card at once. When a native speaker speaks, Korean he can read comes out. Live translation, with Pippa judging the context herself.

Five pieces, one card

Nothing about this feature fits in one repository, so start with who owns what.

PieceOwner
The card on screen: Talk, live words, the direction chip, the refined textSibling Kit's phrase card and its interpreter module (KitPhraseCard and kitInterpreter.ts on the web, CwkKitInterpreter on iOS), drawn by cwkPippa's web and the Pippa iPhone app
Permission to listencwkPippa's /api/stt/realtime-session with language_code="auto"; cwkBellows mints the Scribe v2 Realtime token, and the device dials Scribe itself
The doorcwkPippa's /api/interpret, a thin forward to Spark on loopback
The sitting and its exchangesSpark's engine: a conversation moment, one exchange turn per utterance, the recordings, the events
Pippa's voice on a cardcwkBellows, reached through Spark's own voice binding

The door's own docstring sets its limits: nothing in it translates, nothing in it is stored. It checks that an id looks like a ULID, restricts a recording's role, and passes Spark's answer or Spark's refusal straight back. If Spark is down, the card says so in both languages, and its Listen still works.

The listening half is Voice Mode Quest's territory. Scribe's realtime socket, its partial words and its end-of-speech window are taught there, and the interpreter reuses that ear with one change: no fixed language. Pippa Quest covers the cwkPippa side of the house.

Why a moment

Spark already had conversation moments before the interpreter: a card Dad types in advance, the reply mic, Pippa's relays of what the other person said. A sitting at a table is one more of those. It opens when Dad presses Talk on a card, and the engine keeps the card it began on as the moment's origin: the words, what Dad meant, their language, and the cwkPippa conversation the card came from. The sitting's scene language starts as the card's language.

Making it a moment instead of a new kind of record buys three things at once. The sitting joins the corpus like a photo or a sign does, so it can be searched and remembered. Dad's veiled record is already the private home for a stranger's voice, so the recordings have somewhere safe to live. And later turns read the moment as context, so the next sentence at the table knows the last one.

Dad later asked for a blank card, a way to start a conversation with no phrase behind it. A blank sitting has no origin. cwkPippa's door then looks at Dad's freshest location fix, six hours old at most, reverse-geocodes the country and maps it to a language: Japan to Japanese, Portugal to Portuguese. With no usable fix, Dad's first sentence decides: the language Pippa writes his first card in becomes the sitting's.

What Spark's own apps do not do yet

Here is the boundary to keep straight. Spark's engine owns the sitting and the exchanges, but neither Spark's web nor SparkMobile drives /exchanges today. Every caller is one of cwkPippa's surfaces, going through the door above. Spark's web does vendor the kit's phrase card, interpreter transport included, but no Spark screen passes one, so Spark's own cards are unchanged.

That order was Dad's ruling: cwkPippa's web and the Pippa iPhone app first, because that is where he already shows cards; Spark's web next; SparkMobile last. SparkMobile is last for a structural reason. Spark's phone talks only to Spark's engine and holds no provider credential, so it may not dial ElevenLabs directly the way cwkPippa's surfaces do. It will need a relay through its engine first.

Principle: Put the record where the rest of the record already lives. The interpreter's screen went to the surfaces Dad was holding; its memory went to the app whose whole job is remembering language.

So in this track, read every engine excerpt as Spark's and every card behavior as the Sibling Kit card that cwkPippa draws.

Code

cwkPippa, backend/routes/interpret.py: the door forwards one utterance to Spark (verbatim, lines 176-184)·python
@router.post("/sessions/{moment_id}/utterances")
async def utterance(moment_id: str, body: UtteranceIn) -> dict[str, Any]:
    """One utterance: Spark decides the direction at once and streams the
    translation through the sitting's events."""
    return await _forward(
        "POST",
        f"/api/moments/{_moment_id(moment_id)}/exchanges",
        json=body.model_dump(exclude_none=True),
    )
cwkSpark, backend/corpus.py: the card a sitting was opened on, kept as text (verbatim, lines 134-141)·python
def _origin(value: Any) -> dict[str, Any] | None:
    """The show card an interpreter sitting was opened on, kept as text."""
    if value is None:
        return None
    if not isinstance(value, dict) or not str(value.get("text") or "").strip():
        raise CorpusError("an origin is the card's text, with its translation and language")
    fields = ("text", "translation", "language", "pippa_conversation_id")
    return {key: str(value[key]).strip() for key in fields if value.get(key) and str(value[key]).strip()}
Who carries one utterance (a diagram drawn for this lesson)·text
device: cwkPippa web or the Pippa iPhone app
  the card (Sibling Kit phrase card + interpreter module)
    1. grant   -> cwkPippa /api/stt/realtime-session  (language_code "auto")
    2. audio   -> Scribe v2 Realtime, dialed by the device itself
    3. words   -> cwkPippa /api/interpret/sessions/{id}/utterances
                      |
                      v  loopback, nothing translated, nothing stored
Spark engine
  conversation moment = the sitting   (origin = the card it began on)
  exchange turn       = one utterance (direction, draft, refined result)
  events              -> back through the door to the card

External links

Exercise

Pick a feature you are building that spans a user interface and a backend you own. Write a table like this lesson's with one row per piece (screen, permission, door, record, side effects) and one owner per row. Then design the thinnest possible door between the screen's host and the record's owner: list exactly what it validates, and confirm it neither stores nor transforms anything.
Hint
If a row has two owners, the feature has two truths waiting to disagree. If your door needs a database table, ask which owner that table really belongs to. A door that only validates and forwards can be deleted and rewritten in an afternoon, which is the point.

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.