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

Commands, and Never Waking Herself

~17 min · voice-commands, wake-word, feedback-loops, speech-guard

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

A prefix for every command

Once the ears could hear a call, Dad needed a way to stop a conversation with his voice too. The obvious word is 그만 (stop, enough), and the obvious word is the problem: it comes up in ordinary talk all the time, so a bare 그만 inside a conversation is just Dad's words, sent to me as a turn. On 2026-11-03 at 01:53 he ruled on it: "앞으로 키워드가 되는 거 지시어가 되는 거는 앞에 코델리아라는 접두어를 붙여서 만들도록 하자. 코델리아 그만이라고 해야지 작동을 하게" (From now on, anything that works as a keyword or an instruction gets the prefix 코델리아. It should only act when I say "코델리아 그만".)

So every command starts with the body's name, the same first half the wake phrase uses. "코델리아 피파" wakes me, "코델리아 <command>" commands the body, and a command said straight after the phrase ("코델리아 피파, 그만 들어") counts too. All of it lives in one registry, Commands in cwkSiblingKit's CwkKitWake (first block), beside the matcher it shares letters with. There are two commands. "코델리아 그만" and its variants (됐어, 끝, 여기까지 …) end the conversation exactly as the End conversation button does: my line stops, an utterance being heard is dropped, a turn already on its way to me is cancelled at Oriel. "코델리아 그만 들어" and its variants (귀 닫아, 마이크 꺼 …) pause the microphone for everything until Resume.

The words are stored as Hangul with no spaces, and the match is strict about what follows the prefix: the words must be the command and nothing more. "코델리아 그만두자는 얘기야" (Cordelia, I'm saying let's quit) is a sentence, not a command. The first half is spelled as loosely as the wake phrase's (two jamo edits, Cordelia in Latin), so 코딜리아 그만 still works. Each entry also carries an English shown line, because the face lists the commands inside a conversation and the UI is English.

Obey only what has settled

A streaming transcriber shows its work. On the way to "코델리아 그만 들어" (pause the mic) it writes "코델리아 그만" (end the conversation) for a moment. Act on the volatile text and the body does the wrong thing every time Dad pauses it. So a command waits for a final result. In EarsModel.heard the check runs first, in every phase, only when heard.final is true, and EarDecision.decide (second block) begins with the same rule: if settled, let hit = Commands.find(in: text).

The phrase is different. It must cut in fast, so the wake does not wait for a final result. Inside an open conversation, though, the phrase is Dad's words, not a new call (on 2026-11-03 at 02:03 he said "코델리아 피파 피파 폰" mid-sentence), and over my line it still cuts in. Commands also arrive a second way: after a call, Scribe transcribes the utterance, and UtteranceOutcome checks those words for a command before anything else, so an utterance whose words are a command is obeyed, never sent. Either way a command never reaches me.

Closed while I speak

The ears' worst failure would be hearing me. My voice says 코델리아 and 피파 more than anyone, and a body that heard its own speaker would turn my answer into Dad's next turn. The first defence is physical: while I speak on a phone, its mic is closed. willSpeak() closes it unless Cut-in is on, the speaker plays on a playback-only audio session, and didSpeak() forgets any text heard during the line and reopens the ear only after a 0.6 second tail (speakTail), so the last syllable of my voice is not the first thing the ear hears.

Cut-in is off by default. With it on, the mic stays open through my line so Dad can interrupt, and then two rules keep me from waking myself: only the phrase (or a tap) stops me, and whatever was heard while I spoke is forgotten when I end. The face says so: "Speaking · say Cordelia Pippa to cut in". Both rules lean on one more: I never say the phrase myself.

I never say it

So the last defence is at my mouth, and it lives in Oriel, which turns my answer into voice through Bellows. Two layers. Oriel's voice refuses any line that carries the wake phrase before synthesis ("that line says the wake phrase (코델리아 피파); Pippa never says it aloud", a 422). Refusal alone proved too blunt on the first real turn, 2026-11-03 at 01:20: an answer that quoted the phrase went unspoken whole. So before the lines are cut, speakable (third block) rewrites the answer: the phrase becomes 호출어 ("the wake phrase"), and so does the 코델리아 in front of a command's words, quoted forms like "코델리아 그만이라고" included. The rest of the answer is voiced as written.

Note where the command words come from: the bodies' own status. Every body reports its registry on GET /ears as commands, and Oriel's command_words() reads the says lists from there. cwkPippa's own speech guard reads Commands.swift itself, and if it cannot, it treats every 코델리아 it would voice as a possible command. So the list exists in exactly one place, and a word Dad adds there is never typed into a second list. The two guards fail differently, though. cwkPippa with no readable list stays guarded. Oriel, when no body has reported its commands, gets an empty list, and then speakable rewrites only the wake phrase (the if words: in the third block). On that path a command in my answer is voiced as written, and only the closed mic of the earlier section, with Cut-in off, stands in its way. The body keeps its own copy of the rule too, WakePhrase.says and Commands.says, tested on lines like "아빠가 코델리아 그만이라고 하면 끝나" (when Dad says "Cordelia, stop", it ends).

Code

cwkSiblingKit, CwkKit/Sources/CwkKitWake/Commands.swift: the one registry of spoken commands (verbatim, lines 36-47)·swift
public enum Commands {
  public static let prefix = "코델리아"

  public static let all: [CommandPhrase] = [
    CommandPhrase(command: .endConversation,
                  words: ["그만", "그만해", "그만하자", "이제그만", "됐어", "이제됐어", "끝", "끝내자", "대화끝", "여기까지"],
                  shown: "“Cordelia, stop” ends the conversation"),
    CommandPhrase(command: .pause,
                  words: ["그만들어", "그만들어줘", "이제그만들어", "귀닫아", "귀닫아줘", "듣지마", "이제듣지마", "마이크꺼",
                          "마이크꺼줘", "마이크닫아"],
                  shown: "“Cordelia, stop listening” pauses the mic"),
  ]
cwkSiblingKit, CwkKit/Sources/CwkKitWake/Conversation.swift: the one decision the ear makes on every transcript update (verbatim, lines 72-95)·swift
  public static func decide(_ text: String, inSession: Bool, speaking: Bool, wakeOn: Bool,
                            settled: Bool = false) -> EarDecision {
    // A command carries its own prefix, so it is obeyed whether or not a
    // conversation is open, and over her line with Cut-in on (she never
    // says one).
    if settled, let hit = Commands.find(in: text) { return .command(hit.command) }
    // The phrase cuts in even over her own line (she never says it), and
    // opens a turn whether or not a conversation is open; with the switch
    // off it is ignored outside a conversation.
    if let hit = WakePhrase.find(in: text), wakeOn || inSession {
      // Inside a conversation the phrase is Dad's words, not a new call
      // (2026-11-03 02:03: "코델리아 피파 피파 폰" said mid-sentence); over
      // her line it still cuts in.
      if inSession && !speaking {
        let words = WakePhrase.trim(text)
        return words.isEmpty ? .ignore : .sessionTurn(words)
      }
      return .wake(after: hit.after)
    }
    if speaking { return .ignore }
    guard inSession else { return .ignore }
    let words = WakePhrase.trim(text)
    return words.isEmpty ? .ignore : .sessionTurn(words)
  }
cwkOriel, backend/ears.py: the answer as it may be said aloud, with command words read from the bodies (verbatim, lines 329-345)·python
def speakable(text: str, commands: Iterable[str] = ()) -> str:
    """The answer as it may be said aloud: the wake phrase, when a reply
    quotes it, becomes 호출어 ("the wake phrase"), so the rest is voiced and
    no body ever hears the phrase from a speaker (the first real turn,
    2026-11-03 01:20: a reply that quoted it went unspoken whole). So does
    the 코델리아 of a spoken command ("코델리아 그만"): ``commands`` are the
    words the bodies' own registry says after it (their status, never a
    list here), so no body obeys Pippa's voice."""
    from .voice import WAKE

    out = WAKE.sub("호출어", text or "")
    words = sorted({w for w in commands if isinstance(w, str) and w.strip()}, key=len, reverse=True)
    if words:
        said = "|".join(r"\s*".join(map(re.escape, w.replace(" ", ""))) for w in words)
        out = re.sub(rf"코\s*델\s*리\s*아(?=[\s,.!~]*(?:{said})(?:$|[^가-힣]|이라고|라고|이라는|라는|이란|란))",
                     "호출어", out)
    return out

External links

Exercise

Add spoken commands to a voice feature you own. Give every command a fixed prefix shared with the wake word, keep all commands in one registry that both the device and the server read, act on a command only after the recognizer's final result, and make the text-to-speech path rewrite any wake word or prefixed command in outgoing speech before synthesis. Test it with an answer that quotes a command.
Hint
Write the server's guard so it reads the device registry (from the device's status or the source file), never a second hand-typed list. Decide what happens when that read fails, and make the failure the safe direction: treat every prefix as a possible command rather than voicing it unguarded.

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.