Her Own Name
A system that listens to everything hears nothing
The volume 1 loop listens on a timer: five seconds of recording, whether you spoke or not, forever. Upgrade it the obvious way (run transcription continuously, watch for her name in the output) and you meet the problem within ten minutes, when the fans refuse to spin down. Whisper is the most expensive component in the pipeline, and continuous transcription pays its full price on chunk after chunk of fridge hum, keyboard clatter and silence that could never contain a wake word. It is also a privacy problem: everything said in the room now flows through transcription, all day, on the off chance some of it was for her.
What she needs is a doorbell. One phrase that means "now you may pay attention," and a way to wait for it that costs almost nothing. The design is a two-stage gate: a nearly free energy check throws away silent chunks first, and only chunks with something audible in them reach Whisper, whose transcript is then searched for her name. The principle underneath will outlive this chapter by the length of the book: spend cycles in proportion to how likely the input is to matter. Filter cheaply first; compute expensively last.
Matcher, gate, then the expensive part
# labs/wake_word.py
WAKE_WORDS = ["glados", "hey glados", "aperture"]
def contains_wake_word(text: str) -> bool:
text = text.lower()
return any(word in text for word in WAKE_WORDS)
print(contains_wake_word("Hey GLaDOS, are you there?"))
print(contains_wake_word("what time is it"))
print(contains_wake_word("Welcome to Aperture Science"))
$ uv run python labs/wake_word.py
True
False
True
Substring matching, not equality, because Whisper returns sentences: "hey glados what's up," never a bare keyword. Lowercasing first absorbs Whisper's unpredictable capitalization of sentence starts and proper nouns. And the phrase list is configurable data, not constants scattered through logic; you have read enough of this book to have expected that.
import numpy as np
SILENCE_THRESHOLD = 0.005
def rms_energy(audio: np.ndarray) -> float:
return float(np.sqrt(np.mean(audio ** 2)))
def is_silence(audio: np.ndarray) -> bool:
return rms_energy(audio) < SILENCE_THRESHOLD
quiet = np.full(32000, 0.001, dtype="float32")
loud = np.random.default_rng(0).uniform(-0.3, 0.3, 32000).astype("float32")
print(f"quiet rms={rms_energy(quiet):.4f} silence={is_silence(quiet)}")
print(f"loud rms={rms_energy(loud):.4f} silence={is_silence(loud)}")
$ uv run python labs/wake_word.py
quiet rms=0.0010 silence=True
loud rms=0.1726 silence=False
Chapter 4's RMS, promoted from a debugging guard to a load-bearing filter. The threshold sits at 0.005 rather than zero because a real microphone never reads exactly zero; there is always a noise floor, and the threshold lives above it and below speech. The synthetic buffers make the test deterministic (a constant near-silent hum and a seeded burst of noise) so this stage runs identically on any machine, no microphone required. Your live threshold may want tuning against the levels you measured in chapter 4's exercise 2; that number was for exactly this.
import sounddevice as sd
import soundfile as sf
from faster_whisper import WhisperModel
SAMPLE_RATE = 16000
CHANNELS = 1
CHUNK_DURATION = 2
AUDIO_FILE = "glados/data/wake_chunk.wav"
def detect_wake_word(model: WhisperModel) -> tuple[bool, str]:
audio = sd.rec(
int(CHUNK_DURATION * SAMPLE_RATE),
samplerate=SAMPLE_RATE,
channels=CHANNELS,
dtype="float32",
)
sd.wait()
if is_silence(audio):
return False, ""
sf.write(AUDIO_FILE, audio, SAMPLE_RATE)
segments, _ = model.transcribe(AUDIO_FILE, language="en")
text = " ".join(s.text.strip().lower() for s in segments)
return contains_wake_word(text), text
if __name__ == "__main__":
model = WhisperModel("base", device="cpu", compute_type="int8")
print("Waiting for wake word. Say 'Hey GLaDOS'...")
while True:
woke, heard = detect_wake_word(model)
if woke:
print(f"Wake word detected in: {heard!r}")
break
if heard:
print(f"(heard, ignored: {heard!r})")
$ uv run python labs/wake_word.py
Waiting for wake word. Say 'Hey GLaDOS'...
(heard, ignored: 'is this thing even on')
Wake word detected in: 'hey glados are you awake'
Read the function's ordering like a bill: record two seconds, then the free check, and only if energy is present do we pay for the disk write and the transcription. The return type carries both the verdict and the transcript, so a caller can log what was ignored — which is how you discover your threshold is eating quiet speech, or that the cat's collar bell reads as audio. The loop prints the ignored lines for exactly that reason: what a filter rejected tells you more than what it accepted.
The chunk length is a compromise you should understand rather than inherit. Too short and a phrase straddles the boundary, half in each chunk, and neither chunk matches; too long and the gap between speaking her name and her noticing grows past what feels alive. Two seconds catches a short phrase whole with tolerable lag. The straddle problem never fully disappears at any length; saying her name again is the practical fix, just as it is with people. Volume 9's voice-activity detection replaces fixed chunks with speech-edge detection and retires the compromise properly.
Why this works: a cost cascade
Every chunk flows through checks ordered by price. The RMS gate rejects silence for the cost of one square root over an array: no disk, no model, effectively free. Only chunks that pass it reach the expensive stage, and in a quiet room, almost nothing passes. The worst case (full transcription of an audible chunk) is unchanged; the average case collapses, because the average chunk in any home is silence. Order your filters by cost and let each one protect the next. The same cascade reappears later with camera frames, with automation rule matching, and in volume 8 with what the GPU takes over.
After a config cleanup, wake detection stops working entirely. She ignores you no matter how clearly you speak. The loop runs, no errors, nothing is ever heard:
SILENCE_THRESHOLD = 0.05 # was 0.005: one zero, gone
$ uv run python labs/wake_word.py
Waiting for wake word. Say 'Hey GLaDOS'...
(nothing, forever)
A tenfold threshold from a dropped zero, sitting above normal speech levels, so
every chunk classifies as silence and Whisper never runs. No traceback, because
nothing is wrong except a number, and numbers do not throw. Diagnose it in one
line: print rms_energy(audio) for every chunk and speak. If your
speech reads 0.03 and the threshold is 0.05, there is the whole story. Silent
misconfiguration is the hardest bug family in this book precisely because the code
is innocent; when a filter stops passing anything, your first question should
always be "what does the filter actually see?"
Checkpoint, by name
- I can explain both costs of transcribe-everything, the thermal one and the privacy one.
- I can order the cascade's stages by price and say what fraction of a quiet day's chunks die at the first gate.
- I know why the matcher lowercases and why it uses substring rather than equality.
- I can defend the two-second chunk and name the failure mode it cannot fully eliminate.
- Handed a wake word that never fires, my first move is printing what the gate sees, not reading the code again.
Exercise 1 — measure the savings. Count chunks over five quiet minutes: total, passed the gate, transcribed. What did the free filter save you?
In a genuinely quiet room, 150 chunks and single-digit gate passes is typical; each rejection saved a Whisper call worth one to two seconds of CPU. Your numbers depend on your room and threshold, and the count of ignored-but-audible lines is diagnostic gold — high means your threshold is low or your room is loud, and either way you now know which.
Exercise 2 — false accepts. With detection running, play a Portal video on your speakers. What happens, and which stage would you change to prevent it?
She wakes, of course: the video says her name, and nothing in the cascade knows the difference between your voice and a recording. Options live at different stages: speaker verification would be a new expensive filter after Whisper; a smarter placement is muting detection while she herself is speaking, which volume 4's architecture makes natural. There is no perfect fix, only cheaper and dearer mitigations. Say that sentence in a job interview sometime.
Exercise 3 — splice it into the loop. Replace chapter 10's fixed listen with: wait for wake word, then record the five-second command window, then proceed as before. What changed about living with her?
Everything, subjectively: she stops being a program you take turns with and starts being present. The room is quiet, the CPU is idle, and "Hey GLaDOS" works from across the kitchen. Objectively you have added one state to the loop, waiting versus listening, and volume 5's behavior engine will give states like that a real home.
She has a name and answers to it. What she still lacks is the memory chapter 11 built, actually flowing into her sentences; she remembers facts to a file and never brings them up. Next chapter builds the context engine that decides, every single call, what from her past deserves a seat in the prompt.