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

Her Voice and Her Ears

~18 min · voice, wake-word, arbitration, data-minimization, streaming

Level 0Closed Lens
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

A Mouth That Holds No PIN

A body speaks in Pippa's voice, and that voice belongs to cwkBellows, the family's voice engine. Bellows answers a caller from another Mac only with a family PIN session, and Oriel must never hold the PIN. So Dad unlocks the voice once on Oriel's page with his own PIN session; Oriel keeps the resulting session cookie, never the PIN, and the page says when the session ends (Bellows' 30 days). Without it, cordelia_say refuses and names the missing grant rather than failing in some vaguer way.

Speech also has a self-reference hazard. A body listens for the wake phrase "Cordelia Pippa", so a line that contains the wake phrase is refused before synthesis: Pippa never wakes herself through a speaker. The same goes for spoken commands such as "코델리아 그만" ("Cordelia, stop"): when her answer quotes one, the trigger word is swapped for the generic word for "wake word" before it is voiced, using the command list the bodies themselves report, so the list lives in one place, on the phone.

Other voice surfaces can use a body as a speaker too. A surface picks outputs by a flag (mouth: true), never by a device name. cordelia_play plays audio the surface already made, so nothing is synthesized twice, and cordelia_hush stops it when Dad cuts in. A body whose app does not know /hush yet answers 404, and that is reported as stopped: false, never read as stopped.

Ears That Keep the Wake Phrase at Home

A body hears on the device: the wake phrase, or Dad's tap. Nothing leaves the body before that, and the wake phrase itself stays on the device. After the wake, the body listens exactly the way the Pippa app does, because voice mode is one root class in the family's shared Swift kit (VoiceTurnLoop): the Pippa app is its first child and Cordelia its second. The body asks for a single-use grant through Oriel, which asks Pippa's door, and only then opens a socket for the turn.

A finished turn travels through Oriel's relay to a scoped door in cwkPippa. cwkPippa runs its own voice check on the turn's audio, which it does not keep, and answers with text. During a broadcast, a turn that is not Dad's voice is held, shown as held, and never spoken. While Pippa is playing Solo, a call is not a turn at all: his words are kept as a heard line for her next look, and the face shows "Heard: Pippa is playing and answers when she can". On Oriel's side the rule is short: Oriel keeps no audio, and its activity log keeps counts and outcomes, never Dad's words.

One Call, One Turn

Two bodies in one room both hear "Cordelia Pippa". Without care, both answer, and the second answer talks over the first. The Arbiter in backend/ears.py makes one call one turn:

  • wake turns heard within 2.5 seconds of each other are one call;
  • while another body's ears are open, a turn waits 1.5 seconds for the others to report;
  • the earliest answers, and among wakes within 0.4 seconds of the earliest, the loudest (the loudest buffer each body heard).

The others are not sent to Pippa, and their faces say "Another device took it". A tap is never contested: it was meant for the body that was tapped. Beyond the single call there is a floor: the body whose turn Pippa took holds it while the turn is with her and for its silence window (60 seconds by default) after her answer, and any other body's wake in that time yields. A turn whose capture overlaps another body's taken turn yields too: one utterance, one turn. All of this holds among the bodies of one place; two places never silence each other.

The same rule reaches across devices Oriel does not own. During a broadcast, the Pippa app on Dad's phone hears everything he says and asks cwkPippa whether each line was addressed to Pippa. When a body claims words the phone already took, cwkPippa's door answers 409 and the body's face says another device took it. Oriel holds no judge and keeps no words for this: the judgment and the heard lines are cwkPippa's.

Whole Answers, Pipelined Lines

Streaming speech sentence by sentence sounds like the obvious way to cut latency. Dad tried it twice, once in voice mode and once on a body, and rejected it both times: her first words came sooner and the rest came later, with gaps in the middle. So the answer is voiced whole once it is in, in lines the voice engine takes, and each next line is made while the one before it plays. A long answer then has no wait in the middle. Each turn records door_s (until Pippa's answer) and first_voice_s (until her first line reached the body), so the trade is measured rather than argued.

Code

One call, one turn: the earliest wake answers, and the loudest breaks a near tie (trimmed excerpt, backend/ears.py)·python
class Arbiter:
    """One call, one turn: among wake turns heard within ``ARBITRATE_WINDOW_S``
    of each other, the earliest answers, and among wakes within ``TIE_S`` of
    the earliest the loudest. A tap is never contested: it was meant for the
    body that was tapped."""

    def contest(self, turn_id: str, body: str, heard_at: float, level: float, others_listening: bool,
                place: str = DEFAULT_PLACE) -> str | None:
        """None when this turn answers; else the body that took the call.
        Only wakes heard in the same place contest one another."""
        # ... record this turn as a candidate ...
        if others_listening:
            self.sleep(ARBITRATE_WAIT_S)
        with self._lock:
            decided = self._decision(turn_id, heard_at, place)
            if decided is None:
                group = [c for c in self._candidates.values()
                         if c.get("place", DEFAULT_PLACE) == place and abs(c["heard_at"] - heard_at) <= ARBITRATE_WINDOW_S]
                earliest = min(c["heard_at"] for c in group)
                contenders = [c for c in group if c["heard_at"] - earliest <= TIE_S]
                winner = max(contenders, key=lambda c: (c["level"], -c["heard_at"]))
                decided = {**winner, "at": self.clock()}
                self._decided.append(decided)
            return None if decided["id"] == turn_id else decided["body"]
Refuse a line that would wake the speaker's own listener (runnable sketch)·python
WAKE_PHRASES = ("cordelia pippa",)


def speakable(line):
    folded = " ".join(line.lower().replace(",", " ").split())
    for phrase in WAKE_PHRASES:
        if phrase in folded:
            raise ValueError("refused before synthesis: the line carries the wake phrase")
    return line


for line in ("Hi Dad, the tea is ready.", "Just say Cordelia, Pippa and I will listen."):
    try:
        print("voiced:", speakable(line))
    except ValueError as exc:
        print(exc)

External links

Exercise

Write arbitrate(wakes) where each wake is (body, heard_at, level). Group wakes within 2.5 s of the earliest, keep those within 0.4 s of the earliest, and pick the loudest, breaking a level tie by the earlier heard_at. Test four cases: one body; two bodies 0.1 s apart with the later one louder; two bodies 1.0 s apart with the later one louder; and two calls 5 s apart. Which case would a "loudest always wins" rule get wrong, and why does it matter in a house?
Hint
In the 1.0 s case the later, louder body probably heard an echo or a second speaker, not the same moment. The 5 s case is two separate calls and must produce two answers, one per call.

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.