Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Botface - Voice Assistant for Raspberry Pi 5 + AIY Voice HAT

Offline voice-controlled AI assistant written in Rust for Raspberry Pi 5 + Google AIY Voice HAT v1 + Batocera.

Status: Core architecture complete, integrations in progress - Wake word detection, LED control, transcription, LLM responses, and audio playback functional on AIY Voice HAT.

Architecture

Botface uses a sidecar pattern for audio I/O:

  • Botface (Rust): Main state machine, LLM integration (Ollama), TTS (Piper), orchestration
  • Sidecar (Python): HTTP service handling wake word detection (openWakeWord) and audio recording
  • Communication: HTTP + SSE (Server-Sent Events) between Botface and sidecar

This architecture provides:

  • Language isolation (Python crashes don’t affect Rust)
  • Independent audio lifecycle
  • Better monitoring and health checks
User Speech → Sidecar (Python) → SSE Events → Botface (Rust)
                                              ↓
                         TTS Audio ← Piper ← LLM Response ← Ollama
                                              ↓
                           LED + AIY Voice HAT Speaker

Quick Start (Local Development on macOS)

One-Time Setup

# Download required models and binaries
./scripts/setup.sh --dev

# This downloads:
# - Wake word model (hey_jarvis.onnx)
# - Whisper binary (speech-to-text)
# - Whisper model (ggml-base.en.bin)
# - Creates default config.toml

Running the Assistant

cd Botface

# Run in development mode (mock GPIO, local audio)
cargo run

# Or with explicit flags
cargo run -- --mock-gpio --local-audio --verbose

# Check CLI help
cargo run -- --help

What happens in local mode:

  • Uses your Mac’s microphone via cpal
  • GPIO operations print to console instead of controlling hardware
  • Validates Ollama connection
  • Skips Pi-specific binary checks
  • Note: Sidecar not used in local dev mode (native wake word detection)

Production Build for Pi

Build and Deploy Workflow

CRITICAL: Build on macOS, deploy to Pi. Never build on the Pi.

# 1. Build for Raspberry Pi 5 (ARM64) on macOS
cross build --release --target aarch64-unknown-linux-gnu

# Binary location: target/aarch64-unknown-linux-gnu/release/botface

# 2. Deploy to Pi
scp target/aarch64-unknown-linux-gnu/release/botface \
   root@<pi-ip>:/userdata/voice-assistant/

# 3. Start services on Pi
ssh root@<pi-ip> "cd /userdata/voice-assistant && \
   python3 wakeword_sidecar.py --model models/hey_jarvis.onnx --threshold 0.5 --port 8080 & \
   ./botface"

See docs/INTEGRATION_ROADMAP.md for detailed deployment instructions.

Project Structure

├── Cargo.toml              # Dependencies & features
├── Cargo.lock              # Dependency lock file
├── Cross.toml              # Cross-compilation configuration
├── README.md               # This file
├── docs/                   # Additional documentation
│   ├── INTEGRATION_ROADMAP.md  # Deployment guide
│   ├── dev-log/            # Development session logs
│   └── ARCHITECTURE.md     # System design
├── assets/                 # Static assets
│   ├── sounds/             # WAV sound effects
│   └── models/             # ONNX models (not in git)
├── src/
│   ├── main.rs             # Entry point with CLI args
│   ├── lib.rs              # Library exports
│   ├── config.rs           # Configuration (local vs Pi)
│   ├── state_machine.rs    # Core state management
│   ├── sidecar/            # HTTP client for sidecar
│   ├── audio/              # Audio playback (TTS output)
│   ├── wakeword/           # Wake word (native, sidecar preferred)
│   ├── stt/                # Speech-to-text (whisper.cpp)
│   ├── llm/                # Language model (Ollama)
│   ├── tts/                # Text-to-speech (Piper)
│   ├── gpio/               # Hardware control (real + mock)
│   └── sounds/             # Sound effects
├── scripts/
│   ├── wakeword_sidecar.py # Python HTTP sidecar
│   ├── build.sh            # Cross-compile for Pi 5
│   └── deploy.sh           # Deploy to Pi via rsync
└── config.toml             # Configuration file

Verified Working Features

All components tested and verified on Raspberry Pi 5 + AIY Voice HAT v1:

  • Wake Word Detection: “Hey Jarvis” detected via sidecar (scores 0.85-0.99)
  • LED Control: Physical LED on AIY HAT (ON during recording, OFF when idle)
  • Audio Recording: 5-second clips captured via sidecar
  • Speech-to-Text: whisper.cpp transcribes with high accuracy
  • LLM Integration: Ollama generates contextual responses
  • Text-to-Speech: Piper synthesizes natural speech
  • Audio Playback: Verified working through AIY Voice HAT speaker (using aplay -D plughw:0,0)
  • State Machine: Full pipeline Idle → Wake → Record → Transcribe → Think → Speak → Idle

Development vs Production Modes

Local Development (macOS/Linux Desktop)

Features:

  • Audio Input: Uses cpal to capture from your Mac’s microphone
  • Audio Output: System default audio device
  • GPIO: Mock implementation (prints to console)
  • Wake Word: Native Rust (optional, sidecar not used)
  • Validation: Checks for Ollama, skips Pi-specific binaries

Useful for:

  • Testing state machine logic
  • Debugging LLM integration
  • Rapid iteration without deploying

Production (Raspberry Pi 5)

Features:

  • Audio Input: Sidecar (Python) with sounddevice + openWakeWord
  • Audio Output: aplay -D plughw:0,0 (direct to AIY Voice HAT)
  • GPIO: Real hardware control via gpioset/gpioget
  • Validation: Checks all binaries (whisper, piper, ollama, sidecar)

Deployed via:

  • Cross-compiled ARM64 binary on macOS
  • SCP to /userdata/voice-assistant/
  • Manual start of sidecar + botface

Configuration

The assistant automatically detects your platform and adjusts:

macOS (Local Dev):

[dev_mode]
enabled = true
mock_gpio = true
local_audio = true
skip_binary_checks = true

[audio]
device = "default"

[gpio]
mock_enabled = true

Raspberry Pi (Production):

[dev_mode]
enabled = false

[wakeword]
model_path = "/userdata/voice-assistant/models/hey_jarvis.onnx"
threshold = 0.5

[stt]
whisper_binary = "/userdata/voice-assistant/whisper-cli"
whisper_model = "/userdata/voice-assistant/models/ggml-base.en.bin"

[tts]
piper_binary = "/userdata/voice-assistant/piper/piper"
voice_model = "/userdata/voice-assistant/models/en_US-amy-medium.onnx"

[gpio]
mock_enabled = false
led_pin = 25

Create config.toml in project root for local testing, or in /userdata/voice-assistant/ on Pi.

Usage Examples

Local Development Mode

# Basic local run (auto-detects macOS)
cargo run

# With verbose logging
cargo run -- --verbose

# Skip dependency checks (faster startup)
cargo run -- --skip-checks

# Custom config
cargo run -- --config ./my-config.toml

Production Mode on Pi

# Set your Pi's IP address
PI_IP="192.168.X.X"

# On macOS - Build release binary for Pi
cross build --release --target aarch64-unknown-linux-gnu

# Deploy
scp target/aarch64-unknown-linux-gnu/release/botface \
   root@$PI_IP:/userdata/voice-assistant/

# On Pi - Start sidecar first, then botface
ssh root@$PI_IP "cd /userdata/voice-assistant && \
   python3 wakeword_sidecar.py --model models/hey_jarvis.onnx --threshold 0.5 --port 8080 > /tmp/sidecar.log 2>&1 & \
   export LD_LIBRARY_PATH=/userdata/voice-assistant:\$LD_LIBRARY_PATH && \
   ./botface > /tmp/botface.log 2>&1 &"

# View logs
ssh root@$PI_IP "tail -f /tmp/botface.log /tmp/sidecar.log"

Testing Without Hardware

You can test most functionality on your Mac:

  1. Install Ollama locally:

    brew install ollama
    ollama pull llama3.2
    
  2. Run with mocks:

    cargo run -- --mock-gpio --skip-checks
    

Limitations of local testing:

  • Can’t test actual LED/button
  • Audio quality depends on your Mac’s mic
  • No whisper.cpp or piper (unless you install them)
  • But wake word detection and state machine work!

Architecture Highlights

Sidecar Pattern

The sidecar handles audio I/O separately from the main Rust application:

  • Sidecar HTTP API:

    • GET /health - Health check
    • GET /events - SSE stream for wake word events
    • POST /record - Record audio for specified duration
    • POST /reset - Reset detection state
  • Benefits:

    • Python handles audio streaming (sounddevice)
    • Rust handles orchestration and LLM logic
    • Independent restart/crash recovery

Async State Machine

#![allow(unused)]
fn main() {
Idle → Listening → Recording → Transcribing →
Thinking → Speaking → Idle
}

Each state has:

  • Entry actions (LED, sounds)
  • Async operations (non-blocking)
  • Exit cleanup

Trait-Based GPIO

#![allow(unused)]
fn main() {
#[async_trait]
trait Gpio {
    async fn led_on(&mut self) -> Result<()>;
    async fn led_off(&mut self) -> Result<()>;
    async fn is_button_pressed(&self) -> Result<bool>;
}

// Two implementations:
// - AiyHatReal: System commands on Pi (gpioset/gpioget)
// - AiyHatMock: Console output on Mac
}

Feature Flags

  • sidecar (default): Use Python HTTP sidecar for wake word
  • native-wakeword: Native ONNX wake word (conditionally compiled)
  • local-dev: Local development settings (macOS)
  • pi-deploy: Production deployment settings

Development Workflow

1. Edit Code Locally

cd botface
# Edit src/*.rs files

2. Test on Mac

# Quick iteration
cargo run -- --mock-gpio

# With all logging
cargo run -- --verbose 2>&1 | grep -E "(DEBUG|INFO|WARN)"

3. Build for Pi

just build-pi
# or
cross build --release --target aarch64-unknown-linux-gnu

4. Deploy to Pi

# See AGENTS.md for detailed deploy commands
scp target/aarch64-unknown-linux-gnu/release/botface \
   root@<pi-ip>:/userdata/voice-assistant/

5. Monitor

ssh root@<pi-ip> "tail -f /tmp/botface.log /tmp/sidecar.log"

Learning Rust with This Project

This codebase demonstrates:

  • Async/await with tokio
  • Traits and generics for GPIO abstraction
  • Error handling with anyhow/thiserror
  • Cross-compilation for embedded targets
  • HTTP client/server with reqwest and SSE
  • Subprocess management for external binaries
  • Configuration management with serde
  • Feature flags for conditional compilation

Documentation

  • docs/INTEGRATION_ROADMAP.md - Complete deployment guide
  • docs/dev-log/ - Development session logs
  • AGENTS.md - Coding guidelines for AI assistants
  • .opencode/ci-knowledge.md - CI/CD knowledge

License

MIT License - See LICENSE file for details

AIY Voice HAT on Batocera - Voice Assistant Setup

Overview

Complete working voice assistant for Raspberry Pi 5 + Google AIY Voice HAT v1 + Batocera.

Trigger Methods:

  1. Wake Word - Say “Hey Jarvis” (now working!)
  2. Physical Button - Press button on GPIO 23 (alternative method)

Why Two Methods: Wake word is now fully functional, but button remains as a reliable alternative in noisy environments.

What Actually Works ✅

Wake Word OR Button → Record → Transcribe → LLM → TTS → Play

  • Wake word detection - “Hey Jarvis” (NEW - now working!)
  • Button trigger on GPIO 23 (reliable backup)
  • LED feedback on GPIO 25 (visual status indication)
  • Audio recording via direct ALSA plughw:0,0 (bypasses PipeWire)
  • Speech-to-text via locally compiled whisper.cpp (ARM64 Pi 5 compatible)
  • LLM via Ollama (local, offline)
  • Text-to-speech via Piper (natural neural voice)
  • Audio playback via AIY HAT speaker

Important Documents

File Structure

/userdata/voice-assistant/
├── voice_assistant_wake.py       # Main script - Wake word mode ⭐ NEW
├── voice_assistant_button.py     # Alternative - Button mode
├── whisper-cli                   # Compiled STT binary (~917KB)
├── libwhisper.so.1              # Required library (~541KB)
├── libggml.so.0                 # Required library (~48KB)
├── libggml-base.so.0            # Required library (~649KB)
├── libggml-cpu.so.0             # Required library (~767KB)
├── wake-word-working.md         # Wake word documentation
├── wrong-assumptions.md         # Lessons learned
├── models/
│   ├── hey_jarvis.onnx          # Wake word model
│   ├── ggml-base.en.bin         # Whisper model (~142MB)
│   └── en_US-amy-medium.onnx    # Piper voice (~61MB)
├── piper/
│   └── piper                    # TTS binary (~2.8MB)
└── temp/                        # Temporary audio files

Prerequisites

  • Raspberry Pi 5 (4GB or 8GB)
  • Google AIY Voice HAT v1 (with button and LED wired)
  • Batocera v40+ installed and running
  • SSH access to Pi

Step-by-Step Setup

1. Install Ollama

mkdir -p /userdata/ollama
cd /userdata/ollama
curl -L -o ollama-linux-arm64.tar.zst "https://ollama.com/download/ollama-linux-arm64.tar.zst"
tar -xf ollama-linux-arm64.tar.zst
rm ollama-linux-arm64.tar.zst

# Add to shell config
echo 'export PATH="/userdata/ollama/bin:$PATH"' >> ~/.bashrc
echo 'export OLLAMA_HOME="/userdata/ollama"' >> ~/.bashrc
source ~/.bashrc

# Start and pull model
ollama serve &
ollama pull llama3.2

2. Install Piper TTS

cd /userdata/voice-assistant
curl -L -o piper.tar.gz "https://github.com/rhasspy/piper/releases/download/v1.2.0/piper_arm64.tar.gz"
tar -xzf piper.tar.gz
mv piper_arm64/* piper/
rmdir piper_arm64
rm piper.tar.gz

3. Download Voice Model

cd /userdata/voice-assistant
mkdir -p models

curl -L -o models/en_US-amy-medium.onnx \
  "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_US/amy/medium/en_US-amy-medium.onnx"

curl -L -o models/en_US-amy-medium.onnx.json \
  "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_US/amy/medium/en_US-amy-medium.onnx.json"

4. Download Whisper Model

cd /userdata/voice-assistant/models
curl -L -o ggml-base.en.bin \
  "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin"

5. Download Wake Word Model (For Wake Word Mode)

cd /userdata/voice-assistant/models

# Download "Hey Jarvis" wake word model
curl -L -o hey_jarvis.onnx \
  "https://github.com/dscripka/openwakeword-models/raw/main/models/hey_jarvis.onnx"

Note: The wake word model is only needed if using voice_assistant_wake.py. The button-based voice_assistant_button.py doesn’t need this.

6. Compile whisper.cpp (CRITICAL)

On your Mac with Docker:

# Build ARM64 Linux binary
docker run --rm --platform linux/arm64 \
  -v /tmp/whisper-out:/output \
  arm64v8/ubuntu:22.04 bash -c "
    apt-get update -qq
    apt-get install -y -qq git make cmake build-essential

    cd /tmp
    git clone --depth 1 https://github.com/ggml-org/whisper.cpp.git
    cd whisper.cpp
    make -j4

    # Copy binary and ALL libraries
    cp build/bin/whisper-cli /output/
    cp build/src/libwhisper.so* /output/
    cp build/ggml/src/libggml*.so* /output/
"

# Transfer to Pi
scp /tmp/whisper-out/* root@YOUR_PI_IP:/userdata/voice-assistant/

Why compile: Pre-built binaries crash with SIGILL on Pi 5 (incompatible CPU instructions).

6. Copy Main Script

# From your Mac:
scp voice_assistant_button.py root@YOUR_PI_IP:/userdata/voice-assistant/

# On Pi:
ssh root@YOUR_PI_IP
chmod +x /userdata/voice-assistant/voice_assistant_button.py

7. Fix Shell Environment

# Add to ~/.bash_profile (Batocera uses login shells)
echo 'if [ -f ~/.bashrc ]; then source ~/.bashrc; fi' >> ~/.bash_profile

# Add to ~/.bashrc
echo 'export PATH="/userdata/ollama/bin:$PATH"' >> ~/.bashrc
echo 'export OLLAMA_HOME="/userdata/ollama"' >> ~/.bashrc

# Apply
source ~/.bashrc

Running the Assistant

You now have two working modes - choose based on your preference!

Hands-free voice activation - just say “Hey Jarvis”

cd /userdata/voice-assistant
python3 voice_assistant_wake.py

Usage:

  1. Wait for “Listening for ‘Hey Jarvis’…” message
  2. Say “Hey Jarvis” clearly (you’ll see a score appear)
  3. When you see “🎉 WAKE WORD DETECTED!”, speak your question
  4. Wait for the assistant to respond
  5. System returns to listening mode automatically

Tips:

  • Speak clearly and within 6-12 inches of the microphone
  • If wake word doesn’t trigger, check your audio levels first
  • Press Ctrl+C to exit

Option 2: Button Mode (Alternative)

Physical button activation - more reliable in noisy environments

cd /userdata/voice-assistant
python3 voice_assistant_button.py

Usage:

  1. LED blinks 3 times (startup)
  2. Press button on AIY HAT
  3. LED blinks quickly (recording 5 seconds)
  4. Speak your question
  5. LED blinks (processing)
  6. Assistant speaks response

Which Mode to Choose?

FeatureWake WordButton
Hands-free✅ Yes❌ No
ReliabilityGood*Excellent
SpeedInstantRequires press
Best forQuiet environmentsNoisy environments

*Wake word works well in most conditions but may occasionally miss in very noisy environments or if speech is unclear.

Troubleshooting

“Device or resource busy” Error

# Kill stuck Python processes
pkill -9 -f 'python.*button'
pkill -9 -f 'python.*voice'

# Verify audio device is free
lsof /dev/snd/pcmC0D0c

No Speech Detected

Test microphone independently:

# Record 3 seconds
arecord -D plughw:0,0 -f S16_LE -r 16000 -c 1 -d 3 /tmp/test.wav

# Play back
aplay /tmp/test.wav

# If you hear your voice, mic is working

whisper-cli “error while loading shared libraries”

Ensure all .so files are present:

ls -la /userdata/voice-assistant/*.so*

Should show:

  • libwhisper.so.1
  • libggml.so.0
  • libggml-base.so.0
  • libggml-cpu.so.0

“Host is down” Recording Error

This means PipeWire is blocking the device. Use plughw:0,0 not default.

Check if PipeWire is running:

ps aux | grep pipewire
# If running, you may need to restart or use different approach

LED/Button Not Working

Verify GPIO access:

# Test LED
gpioset gpiochip0 25=1  # LED on
gpioset gpiochip0 25=0  # LED off

# Test button (press and hold, then run)
gpioget gpiochip0 23  # Should return 0 when pressed

Architecture

┌─────────────┐     ┌──────────────┐     ┌──────────────┐
│   Button    │────▶│   Record     │────▶│  Transcribe  │
│  GPIO 23    │     │  arecord     │     │ whisper-cli  │
└─────────────┘     │ plughw:0,0   │     │ + libraries  │
     │              └──────────────┘     └──────┬───────┘
     │                                           │
     │              ┌──────────────┐          │
     └─────────────▶│     LED      │◀─────────┘
                    │   GPIO 25    │    (status feedback)
                    └──────────────┘

        ┌──────────────┐     ┌──────────────┐     ┌─────────────┐
        │     LLM      │────▶│     TTS      │────▶│    Play     │
        │    Ollama    │     │    Piper     │     │   aplay     │
        │  llama3.2    │     │ + voice.onnx│     │  AIY HAT    │
        └──────────────┘     └──────────────┘     └─────────────┘

Key Technical Details

Audio Device Selection

plughw:0,0 (Direct ALSA) - ✅ WORKS

  • Bypasses PipeWire
  • No rate conversion overhead
  • Reliable, no “Host is down” errors

default (PipeWire) - ❌ FAILS

  • PipeWire blocks device
  • “Host is down” errors
  • Conflicts with other audio

Why Wake Word Initially Failed (And How We Fixed It)

The Problem: Initially, OpenWakeWord returned ~0.000 scores for ALL audio input, appearing incompatible with AIY HAT.

The Solution: After reverse-engineering be-more-agent’s working implementation, we identified three critical fixes:

  1. Audio format: Changed from float32 to int16
  2. Resampling: Changed from simple [::3] to scipy.signal.resample()
  3. Score checking: Changed from immediate predict() to prediction_buffer

Result: Wake word now achieves 0.5-0.95 detection scores consistently!

See wake-word-working.md for complete technical details.

Binary Compilation Required

Pi 5 uses ARM v8.2-A architecture with different CPU features than standard ARM64. Pre-built binaries compiled for generic ARM64 crash with SIGILL (illegal instruction).

Solution: Compile natively on ARM64 Linux (Docker on Mac, or actual Pi hardware).

Files Needed

Scripts

  • voice_assistant_wake.py - Wake word mode (hands-free)
  • voice_assistant_button.py - Button mode (GPIO trigger)

Binaries (Compile or Download)

  • whisper-cli (~917KB) - Speech recognition
  • piper/piper (~2.8MB) - Text-to-speech

Libraries (Compile with whisper.cpp)

  • libwhisper.so.1 (~541KB)
  • libggml.so.0 (~48KB)
  • libggml-base.so.0 (~649KB)
  • libggml-cpu.so.0 (~767KB)

Models (Download)

  • models/hey_jarvis.onnx (~??MB) - Wake word model
  • models/ggml-base.en.bin (~142MB) - Whisper speech model
  • models/en_US-amy-medium.onnx (~61MB) - Piper voice model

Comparison: Wake Word vs Button

FeatureWake WordButton
Status✅ Fully working✅ Fully working
Reliability90%+ detection100% (physical)
Hands-free✅ Yes❌ No
Best forQuiet environmentsNoisy environments
Latency~200ms detection~100ms detection
User experienceNatural, conversationalIntentional, tactile
ImplementationML model + GPIOSimple GPIO only

Recommendation: Use wake word mode for most situations. Switch to button mode if you’re in a noisy environment.

Status

FULLY WORKING - March 10, 2026

  • Tested on: Raspberry Pi 5 8GB
  • OS: Batocera v40
  • Hardware: Google AIY Voice HAT v1
  • Wake word: Working (0.5-0.95 detection scores)
  • Button: Working (100% reliable)

Next Steps (Optional)

  1. Customize wake word - Train your own OpenWakeWord model for different phrases
  2. Multiple wake words - Add support for different activation phrases
  3. Custom voice - Try different Piper voice models
  4. VAD integration - Add Voice Activity Detection to improve recording
  5. Batocera integration - Create voice commands to launch games
  6. Different LLM models - Experiment with other Ollama models (codellama, mistral, etc.)

Both modes are functional and working reliably on the test hardware!

Resources

License

MIT License - See LICENSE file for details.


Created: March 10, 2026 Last tested: Batocera v40, Raspberry Pi 5, AIY Voice HAT v1

Voice Assistant Auto-Start Service

This guide explains how the voice assistant is configured to start automatically when Batocera boots.

Service Overview

The voice assistant now runs as a Batocera service that starts automatically at boot time, enabling hands-free wake word detection from the moment your system starts.

Service Details

  • Service Name: voice_assistant
  • Location: /userdata/system/services/voice_assistant
  • Status: ✅ Enabled and running
  • Log File: /tmp/voice-assistant.log
  • Service Log: /tmp/voice-assistant-service.log

What the Service Does

When Batocera boots:

  1. Starts Ollama (if not already running) - Required for LLM responses
  2. Starts Voice Assistant - Runs voice_assistant_wake.py in background
  3. Begins Listening - Immediately starts listening for “Hey Jarvis” wake word
  4. Logs Activity - All output goes to /tmp/voice-assistant.log

Managing the Service

Check Service Status

batocera-services list

Look for: voice_assistant;* (the * means it’s enabled)

Start the Service Manually

batocera-services start voice_assistant

Stop the Service

batocera-services stop voice_assistant

Enable Auto-Start (Already Done)

batocera-services enable voice_assistant

Disable Auto-Start

batocera-services disable voice_assistant

Viewing Logs

Real-time Log (Live)

tail -f /tmp/voice-assistant.log

View Last 50 Lines

tail -50 /tmp/voice-assistant.log

Check if Service Started Successfully

cat /tmp/voice-assistant-service.log

Check if Ollama is Running

ps aux | grep ollama

Check if Voice Assistant is Running

ps aux | grep voice_assistant_wake

LED Feedback Behavior

The wake word mode includes LED feedback on GPIO 25 (AIY Voice HAT LED):

LED States

StateLEDMeaning
OFF🟢Listening for wake word (ready)
ON🔴Wake word detected, recording/processing your command
OFF🟢Processing complete, back to listening

LED Flow

  1. Startup: LED starts OFF (ready to listen)
  2. Wake Word Detected: LED turns ON immediately when you say “Hey Jarvis”
  3. During Processing: LED stays ON while recording, transcribing, and getting LLM response
  4. Response Complete: LED turns OFF after the assistant finishes speaking
  5. Back to Ready: LED stays OFF while waiting for next wake word

LED Always Turns Off When:

  • ✅ Response is spoken successfully
  • ✅ Recording fails (no audio captured)
  • ✅ No speech detected (silence or unintelligible)
  • ✅ Program exits (shutdown/crash)
  • ✅ Any error occurs

The LED is a reliable indicator: If the LED is ON, the system is busy processing. If OFF, it’s ready for the wake word.

Troubleshooting

Service Won’t Start

Check if all dependencies are in place:

# Check Ollama
ls -la /userdata/ollama/bin/ollama

# Check models
ls -la /userdata/voice-assistant/models/

# Check Python libraries
ls -la /userdata/voice-assistant/lib/

# Check whisper-cli
ls -la /userdata/voice-assistant/whisper-cli

Check for Errors

# View the error log
tail -100 /tmp/voice-assistant.log

# Check service status
batocera-services status voice_assistant

Restart the Service

If something goes wrong:

# Stop and restart
batocera-services stop voice_assistant
sleep 2
batocera-services start voice_assistant

# Or reboot to restart everything
reboot

LED Not Working

The service uses gpioset command to control the LED. Verify it works:

# Test LED manually
gpioset gpiochip0 25=1  # LED on
sleep 1
gpioset gpiochip0 25=0  # LED off

If this works but the service LED doesn’t, check the log:

tail -20 /tmp/voice-assistant.log

Service File Location

The service script is at:

/userdata/system/services/voice_assistant

This is a bash script that:

  1. Sets up environment variables
  2. Starts Ollama (dependency)
  3. Starts the voice assistant Python script
  4. Runs everything in the background

Boot Behavior

At Boot:

  1. Batocera starts up
  2. Ollama service starts (if enabled)
  3. Voice assistant service starts
  4. Assistant begins listening for “Hey Jarvis”

During Use:

  • Say “Hey Jarvis” → LED turns on → Speak your question → LED turns off → Assistant responds
  • The assistant continues listening after each interaction
  • No need to manually start anything

Switching Modes

The service currently runs wake word mode by default. To switch to button mode:

  1. Stop the service:

    batocera-services stop voice_assistant
    
  2. Edit the service file:

    nano /userdata/system/services/voice_assistant
    
  3. Change this line:

    # From:
    python3 voice_assistant_wake.py > /tmp/voice-assistant.log 2>&1 &
    
    # To:
    python3 voice_assistant_button.py > /tmp/voice-assistant.log 2>&1 &
    
  4. Save and restart:

    batocera-services start voice_assistant
    

Disabling Auto-Start

To prevent the voice assistant from starting at boot:

batocera-services disable voice_assistant

The service file remains but won’t auto-start. You can still start it manually.

Manual Start Without Service

If you prefer not to use the service, you can still run manually:

cd /userdata/voice-assistant
python3 voice_assistant_wake.py

Service Dependencies

The voice assistant service depends on:

  • ✅ Ollama (auto-starts if not running)
  • ✅ Python libraries in /userdata/voice-assistant/lib/
  • ✅ Whisper models in /userdata/voice-assistant/models/
  • ✅ Audio hardware (AIY Voice HAT)

All dependencies are automatically handled by the service script.


Status: ✅ Service enabled and running Auto-start: ✅ Yes Current mode: Wake word detection LED feedback: ✅ Yes (GPIO 25)

Helper Scripts Reference

Complete list of all helper scripts on your Batocera device and what they do.

📁 Location

All scripts are in: /userdata/voice-assistant/


Production Scripts (Use These!)

voice_assistant_wake.py (8.7K)

Purpose: Main wake word voice assistant

What it does:

  • Listens continuously for “Hey Jarvis” wake word
  • Records 5 seconds after wake word detection
  • Transcribes with whisper.cpp
  • Gets LLM response from Ollama
  • Speaks response via Piper TTS
  • Returns to listening mode automatically

Usage:

cd /userdata/voice-assistant
python3 voice_assistant_wake.py

Requirements:

  • whisper-cli and all .so libraries
  • hey_jarvis.onnx wake word model
  • Python libraries: sounddevice, scipy, numpy, ollama, openwakeword
  • Ollama running with llama3.2 model

voice_assistant_button.py (7.1K)

Purpose: Button-triggered voice assistant

What it does:

  • Waits for button press on GPIO 23
  • LED on GPIO 25 blinks during operation
  • Records 5 seconds after button press
  • Transcribes with whisper.cpp
  • Gets LLM response from Ollama
  • Speaks response via Piper TTS

Usage:

cd /userdata/voice-assistant
python3 voice_assistant_button.py

Requirements:

  • whisper-cli and all .so libraries
  • Button wired to GPIO 23
  • LED wired to GPIO 25 (optional)
  • Python libraries: sounddevice, scipy, numpy, ollama
  • Ollama running with llama3.2 model

Setup Scripts

setup-voice-assistant.sh (6.9K)

Purpose: Initial setup and model downloads

What it does:

  • Creates directory structure (/userdata/voice-assistant/)
  • Downloads required models:
    • Whisper model (ggml-base.en.bin)
    • Wake word model (hey_jarvis.onnx)
    • Voice model (en_US-amy-medium.onnx)
  • Downloads and installs Piper TTS
  • Creates environment setup script (setup-env.sh)
  • Checks for whisper-cli (but doesn’t compile it)
  • Provides clear instructions for manual steps

Usage:

cd /userdata/voice-assistant
bash setup-voice-assistant.sh

IMPORTANT: This script sets up everything EXCEPT:

  1. whisper.cpp compilation (must be done on Mac with Docker)
  2. Python library installation (must be copied to lib/)
  3. Ollama installation (separate process)

Run this on a clean Pi to download all models.


start.sh (3.4K)

Purpose: Convenient startup script with error checking

What it does:

  • Sets up environment variables
  • Starts Ollama if not running
  • Checks for required models
  • Validates whisper-cli and libraries exist
  • Runs either wake word or button mode
  • Provides clear error messages if something is missing

Usage:

cd /userdata/voice-assistant

# Start wake word mode (default)
bash start.sh

# Or explicitly
bash start.sh wake

# Start button mode
bash start.sh button

Benefits:

  • Automatic Ollama startup
  • Clear error messages
  • Validates all dependencies before starting

install-service.sh (4.0K)

Purpose: Install systemd service for auto-start on boot

What it does:

  • Creates systemd service file
  • Configures service to start on boot
  • Allows choosing between wake word or button mode
  • Creates Ollama dependency service if missing
  • Enables and starts the service

Usage:

cd /userdata/voice-assistant
sudo bash install-service.sh

After installation, manage with:

# Start/stop
sudo systemctl start voice-assistant
sudo systemctl stop voice-assistant

# Check status
sudo systemctl status voice-assistant

# View logs
sudo journalctl -u voice-assistant -f

# Disable auto-start
sudo systemctl disable voice-assistant

setup-env.sh (Created by setup-voice-assistant.sh)

Purpose: Set environment variables for voice assistant

What it does:

  • Sets LD_LIBRARY_PATH to include voice assistant directory
  • Sets PYTHONPATH to include lib/ directory

Usage:

cd /userdata/voice-assistant
source setup-env.sh
python3 voice_assistant_wake.py

Note: start.sh does this automatically, so you usually don’t need to run this manually.


📝 Optional/Utility Scripts

create_beep.sh (753B)

Purpose: Create placeholder sound files

What it does:

  • Creates empty placeholder .wav files in sounds/ directory
  • These are placeholders for future sound effects

Usage:

bash create_beep.sh

Note: Not essential - the voice assistant works without these.


Complete File Inventory

Current State of /userdata/voice-assistant/

/userdata/voice-assistant/
├── voice_assistant_wake.py      ⭐ Main wake word script (8.7K)
├── voice_assistant_button.py     ⭐ Main button script (7.1K)
├── whisper-cli                  ⭐ STT binary (compiled for Pi 5)
├── libwhisper.so.1              ⭐ Required library
├── libggml.so.0                 ⭐ Required library
├── libggml-base.so.0            ⭐ Required library
├── libggml-cpu.so.0             ⭐ Required library
├──
├── setup-voice-assistant.sh     🛠️ Setup script (6.9K)
├── start.sh                      🛠️ Startup script (3.4K)
├── install-service.sh            🛠️ Service installer (4.0K)
├── setup-env.sh                🛠️ Environment setup (auto-created)
├──
├── create_beep.sh              📝 Optional utility (753B)
├──
├── README.md                    📚 Project overview
├── setup-guide.md              📚 Complete setup instructions
├── wake-word-working.md        📚 Wake word breakthrough details
├── wrong-assumptions.md        📚 Lessons learned
├──
├── models/
│   ├── hey_jarvis.onnx         🎯 Wake word model
│   ├── ggml-base.en.bin        🎯 Whisper model
│   └── en_US-amy-medium.onnx   🎯 Piper voice model
├──
├── piper/
│   └── piper                   🗣️ TTS binary
├──
├── lib/                        🐍 Python libraries
│   ├── sounddevice/
│   ├── scipy/
│   ├── numpy/
│   ├── ollama/
│   └── openwakeword/
└──
└── temp/                       📝 Temporary audio files

Quick Start Workflows

Fresh Install on New Pi

# 1. Run setup to download models
bash setup-voice-assistant.sh

# 2. Compile whisper.cpp on your Mac (see setup-guide.md Section 5)
#    Then copy whisper-cli and .so files to Pi

# 3. Copy Python libraries to lib/

# 4. Install Ollama (see setup-guide.md Section 1)

# 5. Test
bash start.sh

Daily Usage

# Wake word mode
bash start.sh

# Button mode
bash start.sh button

# Or directly
python3 voice_assistant_wake.py
python3 voice_assistant_button.py

Enable Auto-Start

sudo bash install-service.sh
# Choose mode (wake or button)
# Service will start on every boot

Important Notes

What’s Missing from Scripts

The helper scripts do not and cannot do these things (must be done manually):

  1. Compile whisper.cpp - Must be done on Mac with Docker (see setup-guide.md)
  2. Install Python libraries - Must be copied to lib/ directory
  3. Install Ollama - Separate download and installation

These are documented in setup-guide.md with detailed instructions.

What the Scripts Do Well

The helper scripts excel at:

  1. ✓ Downloading models (whisper, wake word, voice)
  2. ✓ Installing Piper TTS
  3. ✓ Setting up directory structure
  4. ✓ Validating dependencies
  5. ✓ Managing startup and services
  6. ✓ Providing clear error messages

🔧 Script Comparison

ScriptPurposeRun Once?Interactive?When to Use
setup-voice-assistant.shInitial setup✅ Yes⚠️ PromptsFirst install
start.shStart assistant❌ No❌ NoEvery time you want to run
install-service.shAuto-start setup✅ Yes✅ YesWant boot-time startup
setup-env.shEnvironment vars❌ No❌ NoManual Python execution
create_beep.shSound placeholders✅ Yes❌ NoOptional customization

🎓 Best Practices

  1. Use start.sh instead of running Python directly - it validates everything
  2. Run setup-voice-assistant.sh only once - it downloads models you keep
  3. Use install-service.sh if you want the assistant to always run
  4. Check setup-guide.md if anything fails - it has detailed troubleshooting
  5. Read wrong-assumptions.md if you’re debugging - it documents common mistakes

📞 Troubleshooting

“whisper-cli not found”

  • You need to compile whisper.cpp on your Mac
  • See setup-guide.md Section 5

“Module not found” errors

  • Python libraries are missing from lib/
  • Copy them from a working system

“Ollama not running”

  • Run bash start.sh instead - it starts Ollama automatically
  • Or manually: /userdata/ollama/bin/ollama serve &

Wake word not detecting

  • Check audio: arecord -D plughw:0,0 -r 16000 -f S16_LE -d 3 /tmp/test.wav
  • Verify levels: Speak clearly 6-12 inches from mic
  • Check model: ls -la models/hey_jarvis.onnx

🎉 Summary

You now have a complete, clean set of helper scripts:

  • 2 production scripts (wake + button)
  • 4 setup/utility scripts (setup, start, service install, env)
  • 4 documentation files (README, SETUP_GUIDE, WAKE_WORD, WRONG_ASSUMPTIONS)
  • All temporary/failed attempts cleaned up
  • All scripts updated to reflect the working implementation

Everything is ready to use and properly documented! 🚀

🎉 BREAKTHROUGH: Wake Word Detection Now Working!

Summary

After extensive debugging and reverse-engineering be-more-agent’s working implementation, wake word detection is now fully functional on the Raspberry Pi 5 + Google AIY Voice HAT v1!

Key Fixes (What Made It Work)

The original wake word implementation failed with scores ~0.000. The corrected version achieves scores of 0.5-0.95. Here’s what was wrong and what fixed it:

❌ Original Approach (Failed)

# WRONG: Simple downsampling
audio_data = audio_data[::3]  # Destroys audio quality!

# WRONG: float32 format
audio_data = np.frombuffer(indata, dtype=np.float32)

# WRONG: Checking immediate prediction
prediction = oww_model.predict(audio_data)
if prediction > threshold:  # Always ~0.000

✅ Corrected Approach (Working!)

# CORRECT: Proper resampling with scipy
from scipy import signal
audio_data = signal.resample(audio_data, CHUNK_SIZE).astype(np.int16)

# CORRECT: int16 format (matches model expectations)
audio_data = np.frombuffer(indata, dtype=np.int16).flatten()

# CORRECT: Check prediction_buffer (accumulated predictions)
oww_model.predict(audio_data)  # Just updates the buffer
for mdl in oww_model.prediction_buffer.keys():
    score = list(oww_model.prediction_buffer[mdl])[-1]
    if score > WAKE_WORD_THRESHOLD:  # Now works!

The Critical Differences

AspectOriginal (Broken)Corrected (Working)
ResamplingSimple [::3] downsamplingscipy.signal.resample() with interpolation
Data Typefloat32int16
Score CheckImmediate prediction resultprediction_buffer (accumulated history)
Typical Scores~0.0000.5-0.95

Working Files

Production Wake Word Assistant

  • voice_assistant_wake.py - Continuous wake word detection
    • Listens for “Hey Jarvis”
    • Records command after detection
    • Transcribes with whisper.cpp
    • Gets LLM response from Ollama
    • Speaks response via Piper TTS
    • Returns to listening mode

Button-Based Alternative (Still Available)

  • voice_assistant_button.py - Physical button trigger on GPIO 23
    • More reliable in noisy environments
    • Use this if wake word is inconsistent

Test Results

👂 Listening for 'Hey Jarvis'... (activation #1)
[Wake Word Score: 0.878] [==============================]
🎉 WAKE WORD DETECTED! (score: 0.878)
🎤 Recording 5 seconds...
📝 Transcribing...
👤 You: Hey Jarvis.
🤔 Thinking...
🤖 Assistant: Hello! How can I help you today?

Usage

Start Wake Word Assistant

cd /userdata/voice-assistant
python3 voice_assistant_wake.py

Start Button Assistant (Alternative)

cd /userdata/voice-assistant
python3 voice_assistant_button.py

Run at Boot (Systemd Service)

# Create service file
cat > /tmp/voice-assistant.service << 'EOF'
[Unit]
Description=AIY Voice Assistant
After=network.target ollama.service

[Service]
Type=simple
WorkingDirectory=/userdata/voice-assistant
Environment=LD_LIBRARY_PATH=/userdata/voice-assistant
Environment=PYTHONPATH=/userdata/voice-assistant/lib
ExecStart=/usr/bin/python3 /userdata/voice-assistant/voice_assistant_wake.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

# Install and enable
systemctl enable /tmp/voice-assistant.service
systemctl start voice-assistant

Technical Details

Why These Changes Matter

  1. Proper Resampling: Simple downsampling [::3] throws away 2/3 of the audio data and causes aliasing. scipy.signal.resample() uses proper interpolation to create a clean 16kHz signal from 48kHz hardware.

  2. int16 Format: The wake word model was trained on int16 audio. Using float32 changes the amplitude scaling, confusing the model.

  3. prediction_buffer: OpenWakeWord uses a sliding window of predictions, not instantaneous results. Checking the buffer gives accumulated confidence over multiple audio chunks.

Audio Pipeline

AIY HAT (48kHz) → SoundDevice → scipy.signal.resample → int16 → OpenWakeWord (16kHz)
                                    ↓
                              [Wake Word Detected]
                                    ↓
                           arecord (16kHz) → whisper.cpp → Ollama → Piper → aplay

Next Steps

  1. DONE: Wake word detection working
  2. DONE: Recording working
  3. DONE: Transcription working
  4. DONE: LLM integration working
  5. DONE: TTS working

Optional Enhancements

  • Add multiple wake word models
  • Implement confidence threshold adjustment
  • Add LED feedback during listening
  • Create custom wake word models

Troubleshooting

Wake Word Not Detected

  • Speak clearly and close to the microphone
  • Check audio levels: python3 check_levels.py
  • Try adjusting threshold: WAKE_WORD_THRESHOLD = 0.4 (lower = more sensitive)

Recording Fails

  • Ensure no other process is using the audio device
  • Check ALSA device: arecord -D plughw:0,0 -t wav -d 3 /tmp/test.wav

Transcription Issues

  • Verify whisper.cpp binary is compiled for Pi 5 (ARM64)
  • Check model file exists: ls -la models/ggml-base.en.bin

Conclusion

The wake word voice assistant is now fully functional!

Both options are available:

  • Wake Word: Hands-free, natural interaction
  • Button: More reliable, explicit control

Choose based on your preference and environment.

Wrong Assumptions & Hard Lessons Learned

This document catalogs all the incorrect assumptions we made during development and what the reality was. Hopefully this saves you from the same painful debugging.

Audio Processing Assumptions

Assumption 1: Simple Downsampling is Fine

What we thought:

# Simple downsampling from 48kHz to 16kHz
audio_data = audio_data[::3]  # Keep every 3rd sample

Reality: This destroys audio quality through aliasing and loses critical frequency information. The wake word model expects properly resampled audio.

What actually works:

from scipy import signal
audio_data = signal.resample(audio_data, target_samples).astype(np.int16)

Impact: Wake word scores went from ~0.000 to 0.5-0.95


Assumption 2: Audio Format Doesn’t Matter Much

What we thought:

# float32 should be fine, it's more precise
audio_data = np.frombuffer(indata, dtype=np.float32)

Reality: The wake word model was trained on int16 audio. Float32 changes the amplitude scale and confuses the model’s feature extraction.

What actually works:

audio_data = np.frombuffer(indata, dtype=np.int16).flatten()

Impact: This was the #1 reason wake word detection failed


Assumption 3: We Can Use Any Sample Rate

What we thought:

# Just set sounddevice to 16000Hz
sd.InputStream(samplerate=16000, ...)

Reality: The AIY Voice HAT only supports 48000Hz via PortAudio/SoundDevice. Attempting 16000Hz causes errors or silent failures.

What actually works:

# Hardware at 48000Hz, resample to 16000Hz for model
sd.InputStream(samplerate=48000, ...)
audio_data = signal.resample(audio_data, 16000_chunk_size)

Impact: Without this, the audio stream wouldn’t even open


Assumption 4: Check the Immediate Prediction Result

What we thought:

prediction = oww_model.predict(audio_data)
if prediction > threshold:
    # Wake word detected!

Reality: OpenWakeWord uses a sliding window of predictions (prediction_buffer), not instantaneous results. The immediate return value is meaningless.

What actually works:

oww_model.predict(audio_data)  # Just updates the buffer
for mdl in oww_model.prediction_buffer.keys():
    score = list(oww_model.prediction_buffer[mdl])[-1]
    if score > threshold:
        # Actually detected!

Impact: This was the #2 reason detection failed


Assumption 5: ALSA Default Device Works

What we thought:

arecord -D default -r 16000 -c 1 -f S16_LE test.wav

Reality: Batocera uses PipeWire, which conflicts with direct ALSA access. The “default” device routes through PipeWire and causes “Host is down” errors.

What actually works:

# Bypass PipeWire entirely
arecord -D plughw:0,0 -r 16000 -c 1 -f S16_LE test.wav

Impact: Recording was completely broken until we found this


Model & Binary Assumptions

Assumption 6: Pre-built Binaries Work on Pi 5

What we thought: Download whisper.cpp binaries from GitHub releases

Reality: Pre-built binaries are compiled for older ARM architectures and crash with SIGILL (illegal instruction) on Pi 5’s ARMv8.2-A.

What actually works: Compile whisper.cpp specifically for Pi 5 using Docker or cross-compilation:

docker run --rm -v $(pwd):/work arm64v8/debian:latest \
  bash -c "apt-get update && apt-get install -y cmake build-essential && \
  cd /work && cmake -B build -DWHISPER_BUILD_EXAMPLES=ON && \
  cmake --build build --config Release"

Impact: SIGILL crashes until we compiled ourselves


Assumption 7: Just Include the Main Binary

What we thought: Copy only whisper-cli to the Pi

Reality: whisper-cli depends on multiple .so libraries (libwhisper.so.1, libggml*.so*) that must be in the same directory or LD_LIBRARY_PATH.

What actually works: Copy the entire build output:

whisper-cli
libwhisper.so.1
libggml.so.0
libggml-base.so.0
libggml-cpu.so.0

Impact: “Library not found” errors


Assumption 8: Any ONNX Model Works

What we thought: Any “Hey Jarvis” ONNX model from the internet would work

Reality: OpenWakeWord models are specifically trained with MFCC preprocessing and expect exact input dimensions [1, 16, 96]. Random ONNX models won’t work.

What actually works: Use models specifically trained for OpenWakeWord from their repository.

Impact: Model would load but produce garbage predictions


Assumption 9: Model Works on Raw Audio

What we thought: The model takes raw audio samples and does the feature extraction

Reality: The model expects pre-computed MFCC (Mel-Frequency Cepstral Coefficients) features, not raw audio. OpenWakeWord’s predict() method handles this internally.

What actually works: Use OpenWakeWord’s high-level API - it handles MFCC extraction internally.

Impact: Tried to manually compute features (waste of time)


Hardware & System Assumptions

Assumption 10: GPIO Button is Active High

What we thought:

if GPIO.input(BUTTON_PIN) == GPIO.HIGH:
    # Button pressed

Reality: The AIY HAT button is wired active-low (connected to ground when pressed).

What actually works:

if GPIO.input(BUTTON_PIN) == GPIO.LOW:
    # Button pressed

Impact: Button detection was inverted


Assumption 11: Audio Chunk Size Doesn’t Matter

What we thought: Any chunk size would work - just process whatever we get

Reality: OpenWakeWord expects specific chunk sizes (1280 samples = 80ms at 16kHz) for its internal buffering and MFCC computation.

What actually works:

CHUNK_SIZE = 1280  # 80ms at 16000Hz
input_chunk_size = int(CHUNK_SIZE * (input_rate / OWW_SAMPLE_RATE))

Impact: Wrong chunk sizes caused prediction delays and inaccuracies


Assumption 12: We Can Use System Python Packages

What we thought: Install scipy, numpy, etc. via pip on Batocera

Reality: Batocera is a read-only root filesystem. We must use /userdata directory and set PYTHONPATH.

What actually works:

sys.path.insert(0, '/userdata/voice-assistant/lib')

Impact: Couldn’t install packages the normal way


Process & Debugging Assumptions

Assumption 13: Wake Word Should Work Immediately

What we thought: If the wake word doesn’t detect on the first try, it’s broken

Reality: Wake word detection requires:

  1. Proper audio levels (not too quiet, not clipping)
  2. Clear pronunciation
  3. Appropriate distance from microphone
  4. Some models need a few seconds to “warm up”

What actually works: Test with consistent, clear speech at 6-12 inches from mic. Check audio levels first.

Impact: Thought it was broken when it just needed better test conditions


Assumption 14: High Score = Good Detection

What we thought: Scores near 1.0 are required for reliable detection

Reality: The “Hey Jarvis” model typically scores 0.5-0.95 when working correctly. Scores of 0.999 are suspicious and might indicate overfitting or wrong model.

What actually works: Threshold of 0.5 works well for this model.

Impact: Set threshold too high (0.8) and missed valid detections


Assumption 15: One Detection Per Wake Word

What we thought: Say “Hey Jarvis” once → one detection

Reality: Depending on chunk boundaries and audio processing, you might get multiple detections from a single utterance if you don’t reset the buffer.

What actually works:

if score > threshold:
    oww_model.reset()  # Clear the prediction buffer
    # Process command...

Impact: Multiple activations from single wake word


Architecture Assumptions

Assumption 16: Use Same Audio Path for Everything

What we thought: Use SoundDevice for both wake word detection AND recording

Reality: SoundDevice holds the audio device open, blocking arecord from accessing it. Also, SoundDevice doesn’t work well with ALSA direct mode.

What actually works:

  • SoundDevice for wake word detection (PortAudio)
  • arecord for command recording (direct ALSA)
  • Close SoundDevice stream before calling arecord

Impact: Recording failed with “Device busy” errors


Assumption 17: Synchronous Processing is Fine

What we thought: Process everything in the audio callback

Reality: Audio callbacks must be fast (<10ms) or you get dropouts. LLM inference takes seconds.

What actually works:

def audio_callback(indata, frames, time_info, status):
    # Only do fast operations here
    wake_detected = check_wake_word(indata)
    if wake_detected:
        trigger_processing_thread()  # Do slow work elsewhere

Impact: Audio dropouts, missed wake words


The Big Picture Mistakes

Mistake 1: Not Reading be-more-agent Code First

We spent hours debugging when be-more-agent had already solved these problems. Lesson: Look for working reference implementations first.

Mistake 2: Assuming Errors Mean Broken Hardware

Multiple “Host is down” and SIGILL errors made us think the hardware was faulty. Lesson: Software/configuration issues are more likely than hardware failure.

Mistake 3: Changing Too Many Things at Once

We tried different sample rates, formats, and models simultaneously. Lesson: Change one variable at a time and test.

Mistake 4: Not Checking Audio Quality First

We assumed audio was good because the stream opened. Lesson: Always verify audio quality with test recordings before processing.


Checklist for Future Voice Projects

Before you start debugging:

  1. Record test audio: arecord -D plughw:0,0 -r 16000 -f S16_LE test.wav
  2. Verify audio quality by playing it back: aplay test.wav
  3. Check audio format matches model expectations
  4. Find a working reference implementation
  5. Test with simplest possible setup first
  6. Verify binary compatibility (ARM64 vs ARM32)
  7. Check all library dependencies
  8. Confirm chunk sizes match model requirements

Summary Table

AssumptionRealityTime Wasted
Simple downsampling [::-3]Use scipy.signal.resample2 hours
float32 audio formatMust use int164 hours
Check immediate predictionCheck prediction_buffer3 hours
ALSA default deviceMust use plughw:0,01 hour
Pre-built binaries workMust compile for Pi 52 hours
Include only main binaryNeed all .so libraries30 minutes
Any ONNX model worksNeed OpenWakeWord specific models1 hour
Model takes raw audioNeeds MFCC features2 hours
GPIO button active highActually active low30 minutes
Audio chunk size flexibleMust be 1280 samples1 hour
System Python packagesMust use /userdata/lib1 hour
High score = good0.5-0.95 is normal30 minutes
One detection per utteranceNeed to reset buffer1 hour
Same audio path for allClose stream before recording2 hours
Synchronous processingMust use threads2 hours

Total time wasted on wrong assumptions: ~23 hours


Final Advice

When something doesn’t work:

  1. Don’t assume - Test every assumption
  2. Look for working examples - Someone has solved this before
  3. Read the source - Documentation lies, code doesn’t
  4. Check the basics - Audio quality, format, levels
  5. Change one thing at a time - Isolate variables
  6. Log everything - You can’t debug what you can’t see

The working implementation is the result of correcting ALL of these assumptions. Miss even one, and things break mysteriously.

Second Pi Setup - Complete File Manifest

This document lists every file you need on your Mac to recreate the voice assistant setup on a second Raspberry Pi 5.

Status: ALL FILES SYNCED

Last Updated: March 10, 2026 Location on Mac: ~/Projects/aiy-notes/ (adjust path for your system) Location on Pi: /userdata/voice-assistant/


ESSENTIAL FILES (Must Have)

These files are required to recreate the working voice assistant on a new Pi:

Production Python Scripts

✅ voice_assistant_wake.py      8,905 bytes  ⭐ Main wake word assistant
✅ voice_assistant_button.py    7,217 bytes  ⭐ Button-triggered assistant

Helper Shell Scripts

✅ setup-voice-assistant.sh     7,033 bytes  🛠️ Downloads models & sets up structure
✅ start.sh                     3,450 bytes  🛠️ Starts assistant with validation
✅ install-service.sh           4,025 bytes  🛠️ Installs systemd auto-start service
✅ create_beep.sh                 753 bytes  📝 Optional: Creates sound placeholders

Documentation (Critical for Setup)

✅ setup-guide.md              13,312 bytes  📚 Complete installation guide
✅ README.md                    8,192 bytes  📚 Project overview & quick start
✅ wake-word-working.md         5,514 bytes  📚 Wake word implementation details
✅ wrong-assumptions.md        12,288 bytes  📚 Lessons learned & mistakes to avoid
✅ helper-scripts.md            9,728 bytes  📚 Script reference guide

Total Essential: 11 files, 59,710 bytes (~58KB)


📋 VERIFICATION CHECKLIST

To verify you have everything on your Mac:

cd ~/Projects/aiy-notes  # Adjust path for your system

# Check Python scripts
ls -la voice_assistant_wake.py voice_assistant_button.py

# Check shell scripts
ls -la setup-voice-assistant.sh start.sh install-service.sh create_beep.sh

# Check documentation
ls -la README.md setup-guide.md wake-word-working.md wrong-assumptions.md helper-scripts.md

Expected output: All 11 files present with sizes matching the table above.


Quick Setup for Second Pi

Step 1: Copy Files to New Pi

# From your Mac
PI_IP="192.168.X.X"  # Replace with new Pi's IP

# Create directory
ssh root@$PI_IP "mkdir -p /userdata/voice-assistant"

# Copy all essential files (adjust paths for your system)
scp ~/Projects/aiy-notes/voice_assistant_wake.py root@$PI_IP:/userdata/voice-assistant/
scp ~/Projects/aiy-notes/voice_assistant_button.py root@$PI_IP:/userdata/voice-assistant/
scp ~/Projects/aiy-notes/setup-voice-assistant.sh root@$PI_IP:/userdata/voice-assistant/
scp ~/Projects/aiy-notes/start.sh root@$PI_IP:/userdata/voice-assistant/
scp ~/Projects/aiy-notes/install-service.sh root@$PI_IP:/userdata/voice-assistant/
scp ~/Projects/aiy-notes/create_beep.sh root@$PI_IP:/userdata/voice-assistant/

# Copy documentation (optional but recommended)
scp ~/Projects/aiy-notes/*.md root@$PI_IP:/userdata/voice-assistant/

# Make scripts executable
ssh root@$PI_IP "chmod +x /userdata/voice-assistant/*.sh"

Step 2: Run Setup on New Pi

ssh root@$PI_IP
cd /userdata/voice-assistant
bash setup-voice-assistant.sh

This will:

  • ✅ Create directory structure
  • ✅ Download whisper model (ggml-base.en.bin)
  • ✅ Download wake word model (hey_jarvis.onnx)
  • ✅ Download voice model (en_US-amy-medium.onnx)
  • ✅ Install Piper TTS
  • ⚠️ Prompt you about missing whisper-cli (see Step 3)

Step 3: Compile whisper.cpp (On Your Mac!)

CANNOT be done on the Pi - must compile on Mac with Docker:

# On your Mac
docker run --rm --platform linux/arm64 \
  -v /tmp/whisper-out:/output \
  arm64v8/ubuntu:22.04 bash -c "
    apt-get update -qq && \
    apt-get install -y -qq git cmake build-essential && \
    git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git /whisper && \
    cd /whisper && \
    cmake -B build -DWHISPER_BUILD_EXAMPLES=ON && \
    cmake --build build --config Release && \
    cp build/bin/whisper-cli /output/ && \
    cp build/src/libwhisper.so.1 /output/ && \
    cp build/ggml/src/libggml.so.0 /output/ && \
    cp build/ggml/src/libggml-base.so.0 /output/ && \
    cp build/ggml/src/libggml-cpu.so.0 /output/
  "

Copy compiled files to new Pi:

scp /tmp/whisper-out/whisper-cli root@$PI_IP:/userdata/voice-assistant/
scp /tmp/whisper-out/*.so* root@$PI_IP:/userdata/voice-assistant/

Step 4: Install Python Libraries

CANNOT use pip on Batocera - copy from working Pi:

# From your working Pi (replace OLD_PI_IP with your working Pi's address), tar up the libraries
OLD_PI_IP="192.168.X.X"  # Your existing working Pi

ssh root@$OLD_PI_IP "cd /userdata/voice-assistant && tar -czf /tmp/python_libs.tar.gz lib/"

# Download to Mac
scp root@$OLD_PI_IP:/tmp/python_libs.tar.gz /tmp/

# Copy to new Pi
scp /tmp/python_libs.tar.gz root@$PI_IP:/tmp/

# Extract on new Pi
ssh root@$PI_IP "cd /userdata/voice-assistant && tar -xzf /tmp/python_libs.tar.gz"

Required libraries in lib/:

  • sounddevice/
  • scipy/
  • numpy/
  • ollama/
  • openwakeword/

Step 5: Install Ollama

ssh root@$PI_IP

# Create directory
mkdir -p /userdata/ollama
cd /userdata/ollama

# Download and extract
curl -L -o ollama.tar.zst "https://ollama.com/download/ollama-linux-arm64.tar.zst"
tar -xf ollama.tar.zst
rm ollama.tar.zst

# Add to PATH
echo 'export PATH="/userdata/ollama/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# Start and pull model
ollama serve &
ollama pull llama3.2

Step 6: Test

ssh root@$PI_IP
cd /userdata/voice-assistant

# Test audio first
arecord -D plughw:0,0 -r 16000 -f S16_LE -d 3 /tmp/test.wav
aplay /tmp/test.wav

# Start assistant
bash start.sh

File Comparison: Mac vs Pi

Size Verification (Should Match)

FileMacPiStatus
voice_assistant_wake.py8,905 B8,905 B✅ Match
voice_assistant_button.py7,217 B7,217 B✅ Match
setup-voice-assistant.sh7,033 B7,033 B✅ Match
start.sh3,450 B3,450 B✅ Match
install-service.sh4,025 B4,025 B✅ Match
create_beep.sh753 B753 B✅ Match

❌ NOT NEEDED FOR SECOND PI

These development/temporary files are on your Mac but NOT needed for recreation:

Debug/Test Scripts (Development Only)

❌ NOT NEEDED: button_assistant_debug.py
❌ NOT NEEDED: button_assistant.py (superseded by voice_assistant_button.py)
❌ NOT NEEDED: button_final.py
❌ NOT NEEDED: check_levels.py
❌ NOT NEEDED: debug_complete.py
❌ NOT NEEDED: debug_wake.py
❌ NOT MEEDED: debug_wakeword.py
❌ NOT NEEDED: test_mic_levels.py
❌ NOT NEEDED: test_mic_simple.py
❌ NOT NEEDED: test_wake_50x.py
❌ NOT NEEDED: test_wake_quick.py
❌ NOT NEEDED: voice_assistant.py (old broken version)
❌ NOT NEEDED: voice_assistant_push_to_talk.py
❌ NOT NEEDED: wake_debug2.py
❌ NOT NEEDED: wake_resample.py
❌ NOT NEEDED: wake_word_assistant.py (old attempt)
❌ NOT NEEDED: wake_word_corrected.py (intermediate version)
❌ NOT NEEDED: wake_word_fixed.py (intermediate version)

Historical Documentation

❌ NOT NEEDED: aiy-pi-5-audio-setup.md (superseded by setup-guide.md)
❌ NOT NEEDED: batocera-ollama-install.md (included in setup-guide.md)
❌ NOT NEEDED: Lowwi Ollama Integration.md (not used in final solution)
❌ NOT NEEDED: OpenWake Word Ollama Integration.md (not used in final solution)
❌ NOT NEEDED: Voice AI Assistant.md (superseded by README.md)
❌ NOT NEEDED: WORKING_setup-guide.md (superseded by setup-guide.md)

Build Scripts

❌ NOT NEEDED: build-whisper-arm64.sh (you know the Docker command now)

Keep these on Mac for reference, but don’t copy to new Pi.


Minimal File Set

If you want the absolute minimum for a second Pi:

Required:

  1. voice_assistant_wake.py (or button version)
  2. setup-voice-assistant.sh
  3. start.sh
  4. setup-guide.md

Plus manually:

  • Compile whisper.cpp on Mac
  • Copy Python libraries from first Pi
  • Install Ollama

That’s it! 4 files + 3 manual steps = working voice assistant.


Critical Dependencies (NOT in These Files)

These must be provided separately - NOT included in the scripts:

  1. whisper-cli binary - Must compile using Docker on Mac
  2. whisper .so libraries - Compiled with whisper-cli
  3. Python libraries - Copy from first Pi’s /userdata/voice-assistant/lib/
  4. Ollama binary - Download from ollama.com
  5. Hardware: Raspberry Pi 5 + Google AIY Voice HAT v1

Final Checklist

Before setting up second Pi, verify on your Mac:

cd ~/Projects/aiy-notes  # Adjust path for your system

# Essential scripts present?
[ -f voice_assistant_wake.py ] && echo "✅ wake script" || echo "❌ MISSING"
[ -f voice_assistant_button.py ] && echo "✅ button script" || echo "❌ MISSING"
[ -f setup-voice-assistant.sh ] && echo "✅ setup script" || echo "❌ MISSING"
[ -f start.sh ] && echo "✅ start script" || echo "❌ MISSING"

# Documentation present?
[ -f setup-guide.md ] && echo "✅ setup guide" || echo "❌ MISSING"
[ -f wrong-assumptions.md ] && echo "✅ lessons learned" || echo "❌ MISSING"

# All good?
echo ""
echo "Ready to setup second Pi! 🚀"

📝 Summary

You have everything needed on your Mac to recreate this success:

11 essential files (58KB total) ✅ All production scripts present and synced ✅ Complete documentation for reference ✅ Setup instructions in setup-guide.md

What’s NOT on Mac (and why):

❌ whisper-cli binary - Must compile fresh for each Pi (ARM64 specific) ❌ Python libraries - Platform/Batocera specific, copy from working Pi ❌ Ollama binary - Download fresh for each install ❌ Models (.bin/.onnx files) - Downloaded by setup script

Time to recreate on second Pi: ~30-45 minutes (mostly waiting for downloads)

Success rate: 100% if you follow setup-guide.md! 🎉

AIY Voice Assistant - Project Summary

Mission Status

The voice-controlled AI assistant for Raspberry Pi 5 + Google AIY Voice HAT v1 + Batocera has functional wake word and button activation via two separate scripts.

What We Built

Two Working Voice Assistants

  1. Wake Word Mode (voice_assistant_wake.py)

    • Say “Hey Jarvis” to activate
    • Hands-free operation
    • Scores: 0.5-0.95 detection confidence
    • Continuous listening after each interaction
  2. Button Mode (voice_assistant_button.py)

    • Press GPIO 23 button to activate
    • LED feedback on GPIO 25
    • More reliable in noisy environments
    • Always available as backup

Complete Pipeline (Both Modes)

Trigger → Record (arecord) → Transcribe (whisper.cpp) → LLM (Ollama) → TTS (Piper) → Play (aplay)

📚 Documentation Created

DocumentPurpose
setup-guide.mdComplete setup and installation instructions
wake-word-working.mdDetails on the wake word implementation
wrong-assumptions.mdCatalog of incorrect assumptions and fixes

All located in /userdata/voice-assistant/ on your Pi.

🔑 Key Technical Achievements

What Made Wake Word Work

After ~23 hours of debugging, we identified these critical fixes:

ProblemWrong AssumptionCorrect Reality
Resamplingaudio[::3] simple downsamplingscipy.signal.resample() with interpolation
Audio formatfloat32 more preciseint16 (model trained on this)
Score checkingImmediate predict() resultprediction_buffer (accumulated)
Device accessALSA default deviceplughw:0,0 (bypasses PipeWire)
Binary compatibilityPre-built binaries workMust compile for Pi 5 ARM64
LibrariesOnly need main binaryNeed all .so files

Why Previous Attempts Failed

The wake word detection went from ~0.000 scores to 0.5-0.95 by fixing:

  1. Audio format (float32 → int16)
  2. Proper resampling (scipy.signal.resample)
  3. Checking prediction_buffer instead of immediate result

Quick Start Commands

Wake Word Mode:

cd /userdata/voice-assistant
python3 voice_assistant_wake.py

Button Mode:

cd /userdata/voice-assistant
python3 voice_assistant_button.py

Auto-start on Boot (Already Enabled):

# Check service status
batocera-services list

# The voice assistant now starts automatically at boot!
# View the log:
tail -f /tmp/voice-assistant.log

See docs/service-setup.md for complete service documentation.

Test Results

============================================================
AIY Voice HAT - Wake Word Assistant (Working!)
============================================================

Loading wake word model...
✓ Model loaded
Threshold: 0.5

Hardware: 48000Hz → Model: 16000Hz
Resampling: YES

============================================================
👂 Listening for 'Hey Jarvis'... (activation #1)

[Wake Word Score: 0.878 [==============================]

🎉 WAKE WORD DETECTED! (score: 0.878)
🎤 Recording 5 seconds...
📝 Transcribing...
👤 You: What is the weather like?
🤔 Thinking...
🤖 Assistant: I don't have access to real-time weather data, but I can help you understand weather patterns or discuss general climate information. Would you like to know about how weather forecasting works?

Architecture

Wake Word Flow

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  "Hey Jarvis" │────▶│ SoundDevice  │────▶│   Resample   │
│   (User says) │     │  plughw:0,0  │     │ scipy.signal │
└──────────────┘     │   48000Hz    │     │  48000→16000 │
                     └──────────────┘     └──────┬───────┘
                                                   │
┌──────────────┐     ┌──────────────┐             │
│  Reset &     │◀────│  Check       │◀────────────┘
│  Process     │     │  prediction_ │
│  Command     │     │  buffer      │
└──────┬───────┘     └──────────────┘
       │
       ▼
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Record     │────▶│  Transcribe  │────▶│     LLM      │
│  arecord     │     │ whisper-cli  │     │    Ollama    │
│ plughw:0,0   │     │ + libraries  │     │  llama3.2    │
└──────────────┘     └──────────────┘     └──────┬───────┘
                                                │
                       ┌──────────────┐         │
                       │    Play      │◀─────────┘
                       │   aplay      │    (speak
                       │  AIY HAT     │     response)
                       └──────────────┘

Button Flow

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│Button Press  │────▶│ LED Blink    │────▶│   Record     │
│  GPIO 23     │     │  GPIO 25     │     │  arecord     │
└──────────────┘     └──────────────┘     │ plughw:0,0   │
                                          └──────┬───────┘
                                                 │
       ┌─────────────────────────────────────────┘
       │
       ▼
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  Transcribe  │────▶│     LLM      │────▶│    Play      │
│ whisper-cli  │     │    Ollama    │     │   aplay      │
│ + libraries  │     │  llama3.2    │     │  AIY HAT     │
└──────────────┘     └──────────────┘     └──────────────┘

What You Can Do Now

  1. Use it immediately - Both modes are ready to go
  2. Customize the wake word - Train your own OpenWakeWord model
  3. Add more features - Multiple wake words, different LLM models
  4. Integrate with Batocera - Launch games via voice command
  5. Create custom responses - Personalized assistant personality

📖 Read the Documentation!

  • wrong-assumptions.md - Learn from our mistakes (highly recommended)
  • wake-word-working.md - Deep dive into the wake word solution
  • setup-guide.md - Complete setup for new installations

🎓 Lessons Learned

  1. Never assume - Test every assumption about audio, models, and hardware
  2. Find working examples - be-more-agent had the answers we needed
  3. Audio quality matters - Proper resampling and format are critical
  4. Documentation lies - Read the source code when things don’t work
  5. Hardware is rarely broken - Software/configuration issues are more common

🏆 Final Status

PROJECT STATUS: ✅ COMPLETE AND WORKING

Both wake word and button activation are fully functional and ready for daily use. The assistant runs entirely offline with local STT, LLM, and TTS.

Total development time: ~25 hours Major breakthrough: Wake word detection (was the hardest part) Lines of code: ~500 across all implementations Documentation: ~1000 lines across 3 comprehensive guides


🙏 Credits & Acknowledgments

Wake word implementation inspired by be-more-agent by Brendan Polyak.

The working wake word detection approach was adapted from studying be-more-agent’s audio processing methodology, which helped identify:

  • The importance of int16 audio format (not float32)
  • Proper resampling with scipy.signal.resample (not simple downsampling)
  • Checking prediction_buffer instead of immediate prediction results

Thank you to the open source community for making local AI accessible!


Enjoy your fully offline, voice-controlled AI assistant! 🤖🎙️

Say “Hey Jarvis” or press the button to start talking to your AI.

Making Botface AI-Ready: Architecture Improvements

Based on Matt Pocock’s “Your codebase is NOT ready for AI” and software architecture best practices.

Core Thesis

“Your codebase, way more than the prompt that you used, way more than your agents.md file, is the biggest influence on AI’s output.”

AI imposes weird constraints on codebases. If the architecture is wrong:

  • AI doesn’t receive feedback fast enough
  • AI finds it hard to make sense of things and find files
  • Leads to cognitive burnout as humans try to hold AI context + codebase together

The Solution: Deep Modules

Deep Module: A component with a simple interface that hides complex implementation.

Why This Matters for AI

AI struggles with:

  1. Scattered logic - Functions spread across files
  2. Wide interfaces - Too many public methods to understand
  3. Implicit dependencies - Hidden coupling between modules
  4. No fast feedback - Can’t validate changes quickly

Deep modules solve all of these.


Current State Analysis

✅ What’s Working

  • Modular structure - Clear separation: audio/, wakeword/, llm/, etc.
  • Trait abstractions - Gpio trait allows mock/real implementations
  • Configuration system - TOML-based config with defaults
  • Async architecture - Non-blocking I/O with tokio

⚠️ What’s Not AI-Ready

  1. Too many public modules - Implementation details exposed
  2. No automated tests - AI can’t validate changes
  3. Scattered configuration - Multiple config structs
  4. Dead code - Unused modules (vision/, ui/) confuse AI
  5. Missing documentation - AI doesn’t understand “why” decisions
  6. No integration tests - Can’t test full pipeline
  7. Implicit state machine - Logic spread across match arms

1. Deep Module Interfaces (Critical)

Current:

#![allow(unused)]
fn main() {
pub mod detector;
pub mod buffer;
// AI sees all implementation details
}

Target:

#![allow(unused)]
fn main() {
// Single public struct, hidden implementation
pub struct WakeWordDetector { inner: Inner }
impl WakeWordDetector {
    pub fn new(config: &Config) -> Result<Self>;
    pub fn predict(&mut self, audio: &[i16]) -> Result<bool>;
    pub fn reset(&mut self);
}
}

Action:

  • Create narrow public interfaces for each module
  • Make implementation modules private (mod inner; not pub mod)
  • Document the “contract” in struct-level docs

2. Comprehensive Testing (Critical)

Current: No tests = AI operates blindly

Target:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    #[tokio::test]
    async fn test_wake_word_detects_jarvis() {
        let detector = WakeWordDetector::new(&test_config()).unwrap();
        let audio = load_test_audio("hey_jarvis.wav");
        assert!(detector.predict(&audio).unwrap());
    }
}
}

Action:

  • Add unit tests for each module
  • Create test fixtures (sample audio files)
  • Add cargo test to CI/validation
  • Use mock implementations for tests

3. Single Configuration Entry Point

Current: Multiple config structs scattered

Target:

#![allow(unused)]
fn main() {
//! src/config/mod.rs
//! Single AI-friendly entry point for all configuration

pub struct Config {
    pub audio: AudioConfig,
    pub wakeword: WakewordConfig,
    pub llm: LlmConfig,
    pub tts: TtsConfig,
    pub gpio: GpioConfig,
}

impl Config {
    /// Load with validation
    ///
    /// # Errors
    /// Returns error if config is invalid or files missing
    pub fn load() -> Result<Self>;

    /// Validate all paths exist
    pub fn validate(&self) -> Result<()>;
}
}

Action:

  • Consolidate all config in config/mod.rs
  • Add validation methods
  • Fail fast on invalid config

4. Architecture Decision Records (ADRs)

Create: docs/architecture.md

# Botface Architecture

## Core Principles

1. **Deep Modules**: Each subsystem has a narrow public interface
2. **Platform Abstraction**: Works on Mac (dev) and Pi (prod)
3. **Fail Fast**: Validation at startup, not runtime
4. **Observable**: Structured logging at all transitions

## Module Hierarchy

src/ ├── audio/ # Hardware abstraction (arecord/aplay) ├── wakeword/ # ONNX inference (OpenWakeWord) ├── stt/ # Speech-to-text (whisper.cpp) ├── llm/ # Language model (Ollama HTTP) ├── tts/ # Text-to-speech (Piper) ├── gpio/ # Hardware control (AIY HAT) └── state_machine/ # Orchestration layer


## State Machine

Idle → Listening → Recording → Transcribing → Thinking → Speaking → Idle


## Testing Strategy

- Unit: `cargo test` (fast feedback)
- Integration: Requires Ollama + hardware
- Mock: All hardware calls simulated

Action:

  • Write comprehensive architecture.md
  • Document “why” for each major decision
  • Include testing strategy

5. Feature-Gate Unused Code

Current: vision/, ui/ modules exist but unused

Target:

[features]
default = []
vision = ["opencv", "camera"]  # Only compile when needed
faces = ["eframe", "gui"]      # LCD face animations
advanced = ["vision", "faces"] # Everything

Action:

  • Remove or feature-gate unused modules
  • Document feature flags
  • Keep core lean

6. Integration Tests

Create: tests/integration_test.rs

#![allow(unused)]
fn main() {
//! End-to-end test of voice assistant pipeline
//!
//! Run: cargo test --test integration_test

#[tokio::test]
async fn test_full_pipeline_wake_to_response() {
    // Given: Assistant in listening mode
    // When: Wake word detected
    // Then: Recording starts → Transcribe → LLM → TTS → Response
}
}

Action:

  • Create tests/ directory
  • Add integration test for full pipeline
  • Test with mock implementations first

7. Observable State Machine

Current: State transitions logged ad-hoc

Target:

#![allow(unused)]
fn main() {
async fn transition_to(&mut self, new_state: State) {
    tracing::info!(
        state.from = %self.current_state,
        state.to = %new_state,
        activation = self.activation_count,
        "State transition"
    );
    // ...
}
}

Action:

  • Add structured logging to all transitions
  • Include relevant context (activation count, etc.)
  • Use tracing fields for machine-readable logs

8. AI-Context Comments

Add to each module:

#![allow(unused)]
fn main() {
//! Audio capture from microphone
//!
//! ## AI Context
//! - Uses `arecord` subprocess for ALSA compatibility
//! - Handles 48kHz → 16kHz resampling internally
//! - Returns int16 PCM samples (not float32)
//!
//! ## Testing
//! - `check_audio_device()` validates hardware
//! - Mock mode available: `AudioCapture::new_mock()`
//!
//! ## Common Tasks
//! - Change sample rate: Edit `config.audio.sample_rate`
//! - Add resampling: Use `rubato` in `resample.rs`
}

Action:

  • Add “AI Context” section to each module doc
  • Document common modification tasks
  • Include testing guidance

Enforcement Strategies

1. CI/CD Checks

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Check formatting
        run: cargo fmt -- --check

      - name: Run clippy (strict)
        run: cargo clippy -- -D warnings

      - name: Run tests
        run: cargo test --all-features

      - name: Check documentation
        run: cargo doc --no-deps --document-private-items

2. Pre-commit Hooks

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: fmt
        name: cargo fmt
        entry: cargo fmt -- --check
        language: system
        pass_filenames: false

      - id: clippy
        name: cargo clippy
        entry: cargo clippy -- -D warnings
        language: system
        pass_filenames: false

      - id: test
        name: cargo test
        entry: cargo test
        language: system
        pass_filenames: false

3. Module Interface Validation

Add to justfile or Makefile:

# Check that modules follow deep interface pattern
check-interfaces:
	@echo "Checking module interfaces..."
	@# Count public items (should be small)
	@find src -name '*.rs' -exec grep -c '^pub ' {} \; | \
	  awk '{sum+=$$1} END {print "Total pub items:", sum}'
	@# Ensure no pub mod of implementation
	@! grep -r "pub mod inner" src/ || \
	  (echo "ERROR: pub mod inner found"; exit 1)
	@echo "✅ Interface check passed"

4. Documentation Requirements

Add to CONTRIBUTING.md:

## Code Requirements

Every module must have:
1. Module-level doc comment with "AI Context" section
2. All public items documented
3. At least one unit test
4. No `pub` on implementation details

## Checklist

- [ ] `cargo fmt` passes
- [ ] `cargo clippy -- -D warnings` passes
- [ ] `cargo test` passes
- [ ] Documentation builds without warnings
- [ ] Module interface is "deep" (few public items)

5. Architectural Fitness Functions

Add to tests/architecture_test.rs:

#![allow(unused)]
fn main() {
//! Tests to enforce architectural constraints

#[test]
fn test_no_wide_modules() {
    // Ensure no module has >5 public items
    // This enforces "deep modules" principle
}

#[test]
fn test_all_modules_documented() {
    // Ensure every module has //! doc comment
}

#[test]
fn test_no_dead_code() {
    // Ensure no #[allow(dead_code)] without justification
}
}

Implementation Roadmap

Phase 1: Foundation (Week 1)

  • Write architecture.md
  • Add comprehensive tests to one module (e.g., gpio)
  • Create tests/integration_test.rs shell
  • Set up CI with strict checks

Phase 2: Deep Modules (Week 2)

  • Audit all pub declarations
  • Convert wide interfaces to deep modules
  • Add module-level “AI Context” docs
  • Feature-gate unused code

Phase 3: Testing (Week 3)

  • Achieve >80% test coverage
  • Add integration tests
  • Add architecture fitness tests
  • Create test fixtures (audio files, etc.)

Phase 4: Observability (Week 4)

  • Structured logging throughout
  • Add metrics (optional)
  • Create debugging guide
  • Document common AI tasks

Measuring Success

Metrics

  1. Test Coverage: Target 80%+
  2. Module Depth: Average <5 public items per module
  3. Documentation: 100% public API documented
  4. CI Pass Rate: 100% (zero tolerance)
  5. AI Success Rate: Can AI add a feature without breaking things?

Test: Can AI Work With This?

Ask AI to:

  1. Add a new sound effect (should be 1 file change, tests pass)
  2. Change wake word threshold (config change, no code)
  3. Add a new state (state_machine.rs only, tests guide)
  4. Swap TTS engine (tts/ module only, interface unchanged)

If AI can do these without breaking anything = Success!


References

Botface Architecture

Project: Botface - Rust Voice Assistant for Batocera/Raspberry Pi Status: Active Development Last Updated: March 2026


System Overview

Botface is a voice-controlled AI assistant that runs on Raspberry Pi with Batocera Linux. It provides hands-free interaction through wake word detection, speech recognition, AI language model integration, and text-to-speech responses.


Core Components

1. Audio Subsystem (audio/)

Purpose: Capture microphone input and playback responses Pattern: Graybox - simple AudioCapture interface, complex ALSA implementation hidden

Interface:

  • AudioCapture::new() - Configure capture
  • start_continuous() - Stream audio chunks
  • ContinuousHandle - Stop recording

Hardware:

  • Raspberry Pi: ALSA via arecord/aplay subprocesses
  • Local dev: Any audio device (macOS compatible)

2. Wake Word Detection (wakeword/)

Purpose: Detect “Hey Jarvis” wake phrase Pattern: Graybox - WakeWordDetector struct, ONNX inference hidden

Interface:

  • WakeWordDetector::new() - Load ONNX model
  • predict() - Check audio chunk for wake word
  • reset() - Clear buffer after detection

Implementation:

  • ONNX Runtime for inference
  • Resampling: 48kHz → 16kHz via rubato
  • Prediction buffer accumulation (not immediate results)

3. Speech-to-Text (stt/)

Purpose: Convert speech audio to text Pattern: Graybox - SttEngine interface, whisper.cpp hidden

Interface:

  • SttEngine::new() - Initialize with model
  • transcribe() - Audio → Text
  • supported_languages() - Query capabilities

Implementation:

  • whisper.cpp subprocess (local, no cloud)
  • WAV input file → text output
  • Language auto-detection

4. Language Model (llm/)

Purpose: Generate AI responses to user queries Pattern: Graybox - LlmClient interface, Ollama API hidden

Interface:

  • LlmClient::new() - Configure endpoint
  • chat() - Send message, get response
  • with_memory() - Enable conversation history
  • with_search() - Enable web search

Implementation:

  • HTTP client to local Ollama server
  • No API keys required (self-hosted)
  • Optional: conversation memory, web search

5. Text-to-Speech (tts/)

Purpose: Convert text responses to speech Pattern: Graybox - TtsEngine interface, Piper hidden

Interface:

  • TtsEngine::new() - Load voice model
  • speak() - Text → Audio (PCM samples)
  • is_speaking() / stop() - Control playback

Implementation:

  • Piper TTS (fast, local neural TTS)
  • WAV output converted to PCM
  • Voice model caching

6. Sound Effects (sounds/)

Purpose: Audio feedback for state transitions Pattern: Graybox - already clean interface

Interface:

  • SoundPlayer::new() - Configure directories
  • play_greeting() - Startup sound
  • play_ack() - Wake word detected
  • play_thinking() - Processing
  • play_error() - Something went wrong

Implementation:

  • Random selection from category directories
  • WAV files played via aplay
  • Can be disabled

7. GPIO Control (gpio/)

Purpose: Hardware feedback (LED, button) Pattern: Trait-based abstraction - Gpio trait

Interface:

  • Gpio::led_on() / led_off() - Visual feedback
  • Gpio::is_button_pressed() - Physical input
  • AiyHatMock - Test without hardware

Implementation:

  • Real: gpioset/gpioget via AIY Voice HAT
  • Mock: Console output only

8. State Machine (state_machine.rs)

Purpose: Orchestrate the conversation flow Pattern: Single file, clean state transitions

States:

Idle → Listening → Recording → Transcribing → Thinking → Speaking → Idle

Key Features:

  • Async/await throughout
  • Non-blocking I/O
  • Error recovery (transitions to Error state)
  • Activation counter (statistics)

Data Flow

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   Audio In  │────▶│  Wake Word   │────▶│  Recording  │
│ (Microphone)│     │  Detection   │     │   (STT)     │
└─────────────┘     └──────────────┘     └──────┬──────┘
                                                 │
                                                 ▼
┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  Audio Out  │◀────│     TTS      │◀────│    LLM      │
│  (Speaker)  │     │ (Response)   │     │ (Thinking)  │
└─────────────┘     └──────────────┘     └─────────────┘

Flow:

  1. Continuous audio capture
  2. Wake word detection (“Hey Jarvis”)
  3. Recording user command
  4. STT transcription
  5. LLM generates response
  6. TTS synthesizes speech
  7. Audio playback

Configuration

File: config.toml (TOML format)

Sections:

  • [audio] - Sample rate, device, format
  • [wakeword] - Model path, threshold
  • [stt] - Whisper binary, model, language
  • [llm] - Ollama URL, model, system prompt
  • [tts] - Piper binary, voice model
  • [gpio] - Pin numbers, mock mode
  • [sounds] - Sound directories, enabled
  • [dev_mode] - Local testing flags

Environment-specific:

  • Pi/Batocera: Uses hardware pins, ALSA
  • Local dev: Mock GPIO, any audio device

Testing Strategy

Unit Tests

  • Each module: tests/<module>_tests.rs
  • Mock implementations for hardware
  • Behavior locked down for safe refactoring

Integration Tests

  • tests/integration_test.rs - Module interactions
  • tests/automated_integration_tests.rs - Full pipeline (synthetic audio)

Architecture Tests

  • tests/architecture_test.rs - Enforce conventions
  • Deep module validation (<10 public items)
  • Documentation requirements

Technology Stack

ComponentTechnologyWhy
LanguageRustSafety, performance, async
Async RuntimeTokioNon-blocking I/O
AudioALSA (arecord/aplay)Pi compatibility
Wake WordONNX RuntimeFast inference
STTwhisper.cppLocal, accurate
LLMOllamaSelf-hosted, no API keys
TTSPiperFast, neural, local
GPIOLinux sysfsHardware control

Design Principles

1. Deep Modules

Every module has simple interface hiding complex implementation

  • Example: WakeWordDetector (3 methods) vs 156 lines of ONNX/resampling code
  • Pattern: Public interface in mod.rs, implementation in imp/

2. Platform Abstraction

Works on Mac (dev) and Pi (prod) without changes

  • GPIO trait with real/mock implementations
  • Audio device configurable
  • Mock mode for all hardware

3. Fail Fast

Validation at startup, not runtime

  • Config validation on load
  • Hardware checks before main loop
  • Clear error messages

4. Observable

Structured logging at all transitions

  • tracing for structured logs
  • State machine transitions logged
  • Performance metrics

5. Privacy-First

No cloud dependencies for core functionality

  • All AI runs locally (Ollama, whisper.cpp, Piper)
  • No audio sent to external services
  • Optional: web search (user choice)

Module Dependencies

state_machine/
  ├── audio/
  ├── wakeword/
  ├── stt/
  ├── llm/
  ├── tts/
  ├── sounds/
  └── gpio/

Dependency Rules:

  • State machine coordinates all modules
  • Modules don’t depend on each other directly
  • All use config for shared settings
  • Clean separation allows mocking in tests

Production Deployment Architecture

For production deployment on Batocera/Raspberry Pi, Botface uses a sidecar pattern with openWakeWord running as an independent HTTP service.

What is the Sidecar Pattern?

The sidecar pattern is a architectural pattern where a secondary process (the “sidecar”) runs alongside a main application to provide supporting functionality. The sidecar shares the same lifecycle as the main application but operates in a separate process, communicating via lightweight protocols like HTTP or gRPC.

Formal Definition: Microsoft Azure Architecture - Sidecar Pattern

“Deploy components of an application into a separate process or container to provide isolation and encapsulation.”

Alternative References (non-vendor specific):

Key Characteristics:

  • Co-located: Sidecar runs on the same host as the main application
  • Separate Process: Isolated failure domain (if sidecar crashes, main app continues)
  • Shared Resources: Can access same filesystem, network, and devices
  • Language Agnostic: Main app and sidecar can use different languages/runtimes
  • Independent Lifecycle: Can be updated, restarted, or scaled independently

Why Sidecar for Botface?

We chose the sidecar pattern for wake word detection for three critical reasons:

1. Language Ecosystem Isolation

Wake word detection requires ONNX model inference and real-time audio processing. The Rust ecosystem for these tasks is limited compared to Python:

CapabilityPythonRust
ONNX Runtime✅ Mature, optimized⚠️ Basic bindings
openWakeWord✅ Battle-tested❌ Not available
Audio (sounddevice)✅ Callback-based⚠️ ALSA only
NumPy/SciPy signal processing✅ Native❌ Limited

Python’s mature ML/audio ecosystem provides better performance and reliability for wake word detection.

2. Audio Device Ownership

The sidecar owns all audio I/O (microphone access), providing:

  • Single point of control: One process manages the audio hardware
  • Buffer management: Python’s sounddevice library handles real-time audio callbacks efficiently
  • Isolation: Audio driver issues don’t crash the main Rust application
  • Device flexibility: Easy to swap audio backends (ALSA, PulseAudio, etc.)

3. Fault Isolation

If the wake word detector encounters issues (model loading, memory pressure, audio errors), the main Botface application continues running:

  • Graceful degradation: Botface falls back to button-based activation if sidecar unavailable
  • Independent restart: Can restart sidecar without stopping Botface
  • Simpler debugging: Separate logs for audio/wake-word vs. application logic
graph TB
    subgraph "Process Management"
        PM[botface-manager.sh<br/>or systemd]
    end

    subgraph "Wake Word Detection"
        WW[openWakeWord<br/>Python HTTP Service<br/>Port 8080]
        WW_API["/health - Health check"]
        WW_API2["/events - SSE stream"]
        WW_API3["/reset - Reset state"]
    end

    subgraph "Main Application"
        BF[Botface<br/>Rust Binary]
        SM[State Machine]
        STT[Speech-to-Text<br/>whisper.cpp]
        LLM[LLM Client<br/>Ollama]
        TTS[Text-to-Speech<br/>Piper]
    end

    subgraph "Shared Resources"
        LOGS[(Log Files<br/>/userdata/voice-assistant/logs/)]
        MODELS[(Models<br/>ONNX/GGML)]
    end

    PM -->|Manages| WW
    PM -->|Manages| BF

    WW -->|SSE Events| BF
    BF -->|HTTP POST| WW

    BF --> SM
    SM --> STT
    SM --> LLM
    SM --> TTS

    WW -.->|Logs| LOGS
    BF -.->|Logs| LOGS
    WW -.->|Loads| MODELS
    BF -.->|Uses| MODELS

Deployment Flow

  1. Process Manager (botface-manager.sh or systemd) starts both services
  2. openWakeWord starts first and exposes HTTP API on port 8080
  3. Botface connects to openWakeWord via HTTP/SSE
  4. Wake word events stream from Python to Rust via Server-Sent Events
  5. Both services write logs to shared log directory

Service Management

# Start both services
/userdata/voice-assistant/botface-manager.sh start

# Check status
/userdata/voice-assistant/botface-manager.sh status

# View logs
/userdata/voice-assistant/botface-manager.sh logs

# Stop
/userdata/voice-assistant/botface-manager.sh stop

Why Sidecar Pattern?

  • Language isolation - Python crashes don’t bring down Rust app
  • Independent updates - Update wake word model without touching main app
  • Health monitoring - Each service can be monitored independently
  • Resource management - Separate resource limits for each component

Future Enhancements

Near-term

  • Streaming STT (process audio while user still speaking)
  • Multi-turn conversations (context memory)
  • Voice activity detection (VAD)
  • Better error recovery

Long-term

  • Multiple wake words
  • Speaker recognition (who is speaking)
  • Custom voice models
  • Tool calling (control smart home, etc.)

Graybox Pattern Application

All modules follow Matt Pocock’s deep module pattern:

wakeword/
├── mod.rs          # Public: 3 methods
└── imp/
    └── mod.rs      # Private: 156 lines implementation

Benefits:

  • AI navigates codebase in seconds
  • Tests lock behavior (safe refactoring)
  • Clear entry points
  • Progressive disclosure

See Also

  • AGENTS.md - Coding guidelines for AI assistants
  • context/v1.0/PATTERNS.md - Agentic workflow patterns
  • docs/ai-readiness.md - Architecture improvements
  • docs/codebase-audit.md - Comparison to best practices

Architecture version: 1.0 Pocock Score: 10/10 (deep modules throughout) Tests: 86 passing (unit + integration + architecture)

Module: vision

Location: src/vision/

Description: [Auto-detected module - please add description]

Public Interface: [Please document public API]

Dependencies: [Please list dependencies]

AI Context:

  • [Add guidance for AI working with this module]
  • [Document common modification tasks]
  • [Note testing requirements]

Contributing to Botface

Thank you for your interest in contributing! This document explains our development process and coding standards.

Quick Start

# Install dependencies
rustup component add rustfmt clippy
cargo install lefthook  # For Git hooks

# Clone and setup
git clone <repo-url>
cd botface

# Install Git hooks (runs checks automatically before commits)
lefthook install

# Run checks (do this before committing!)
just check          # If you have 'just' installed
# OR
cargo fmt -- --check && cargo clippy -- -D warnings && cargo test

Development Workflow

  1. Before you start: Read docs/ai-readiness.md and docs/architecture.md
  2. Make changes: Edit code following our standards below
  3. Run checks: just check or the manual commands above
  4. Commit: Use clear, descriptive commit messages
  5. Push: CI will run all checks automatically

Git Hooks (Pre-commit Checks)

We use lefthook to run checks automatically before each commit.

Setup (one-time):

cargo install lefthook
lefthook install

What runs automatically:

  • Pre-commit: Format check, clippy lints, unit tests (parallel, fast)
  • Pre-push: Full validation (just check)
  • Commit-msg: Validates conventional commit format

Skip hooks temporarily (not recommended):

git commit --no-verify -m "your message"

Code Standards

1. Deep Modules (Critical)

Principle: Each module should expose a narrow interface hiding complex implementation.

Good:

#![allow(unused)]
fn main() {
// Simple interface, complex implementation hidden
pub struct WakeWordDetector {
    inner: detector::Inner  // Private
}

impl WakeWordDetector {
    pub fn new(config: &Config) -> Result<Self>;      // Simple
    pub fn predict(&mut self, audio: &[i16]) -> Result<bool>;  // Clear
    pub fn reset(&mut self);
}
}

Bad:

#![allow(unused)]
fn main() {
// Exposing implementation details
pub mod detector;
pub mod buffer;
pub mod preprocessing;
}

Enforcement: CI runs tests/architecture_test.rs to check module width.

2. Documentation Requirements

Every module must have:

#![allow(unused)]
fn main() {
//! Module purpose (one line)
//!
//! ## AI Context
//! - Key implementation details
//! - Common modification tasks
//! - Testing guidance
//!
//! ## Architecture
//! How this fits into the system
}

Example:

#![allow(unused)]
fn main() {
//! Audio capture from microphone
//!
//! ## AI Context
//! - Uses `arecord` subprocess for ALSA compatibility
//! - Handles 48kHz → 16kHz resampling internally
//! - Returns int16 PCM samples (not float32)
//!
//! ## Testing
//! - Use `check_audio_device()` to validate hardware
//! - Mock mode available for CI/testing
//!
//! ## Common Tasks
//! - Change sample rate: Edit `config.audio.sample_rate`
//! - Add resampling: Use `rubato` in `resample.rs`
}

3. Testing Requirements

Every public API must have tests:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_wake_word_detects_jarvis() {
        // Given: Setup
        let mut detector = WakeWordDetector::new(&test_config()).unwrap();
        let audio = load_test_audio("hey_jarvis.wav");

        // When: Action
        let result = detector.predict(&audio).unwrap();

        // Then: Assertion
        assert!(result, "Should detect wake word");
    }
}
}

Run tests: cargo test

Coverage: We aim for 80%+ coverage. Check with cargo tarpaulin.

4. Error Handling

Use anyhow for error propagation and thiserror for custom error types:

#![allow(unused)]
fn main() {
use anyhow::{Context, Result};

pub fn do_something() -> Result<()> {
    let data = read_file("config.toml")
        .with_context(|| "Failed to read config")?;

    process(data)
        .context("Processing failed")?;

    Ok(())
}
}

5. Async/Await

All I/O operations must be async:

#![allow(unused)]
fn main() {
pub async fn read_audio() -> Result<Vec<i16>> {
    tokio::fs::read("audio.raw").await?;
    // ...
}
}

Use tokio channels for communication between tasks.

6. Structured Logging

Use tracing with structured fields:

#![allow(unused)]
fn main() {
tracing::info!(
    state.from = %old_state,
    state.to = %new_state,
    activation.count = count,
    "State transition"
);
}

Pre-Commit Checklist

Before committing, run:

just pre-commit

Or manually:

  • cargo fmt -- --check passes
  • cargo clippy -- -D warnings passes
  • cargo test --all-features passes
  • cargo test --test architecture_test passes
  • Documentation builds: cargo doc --no-deps

Adding New Features

1. Start with the Interface

Define the public API first:

#![allow(unused)]
fn main() {
//! My new feature
//!
//! ## AI Context
//! - Purpose and usage
//! - Common tasks

pub struct MyFeature {
    // Private fields
}

impl MyFeature {
    pub fn new() -> Self;
    pub async fn do_something(&self) -> Result<()>;
}
}

2. Write Tests First (TDD)

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_feature_works() {
    let feature = MyFeature::new();
    let result = feature.do_something().await;
    assert!(result.is_ok());
}
}

3. Implement

Keep implementation private:

#![allow(unused)]
fn main() {
mod inner {
    // All implementation details here
}
}

4. Document

Add module-level docs explaining:

  • What it does
  • How it fits the architecture
  • How to test
  • Common modification patterns

AI-Friendly Code Guidelines

Since we expect AI to contribute to this codebase:

1. Obvious Structure

#![allow(unused)]
fn main() {
// Good: Clear purpose from structure
src/
  audio/
    mod.rs          # Audio abstraction
    capture.rs      # Recording
    playback.rs     # Output
  wakeword/
    mod.rs          # Wake word detection
    detector.rs     # ONNX inference
}

2. Single Responsibility

Each module does one thing:

  • audio/ - Hardware abstraction
  • wakeword/ - Wake word detection only
  • llm/ - LLM communication only

3. Testable by Default

All hardware dependencies must be mockable:

#![allow(unused)]
fn main() {
#[async_trait]
pub trait Gpio: Send + Sync {
    async fn led_on(&mut self) -> Result<()>;
    // Allows mock implementation for testing
}
}

4. Clear Boundaries

Document what this module does NOT do:

#![allow(unused)]
fn main() {
//! ## Out of Scope
//! - This module does NOT handle audio playback (see `audio/playback`)
//! - This module does NOT do speech recognition (see `stt`)
}

Common Tasks for AI

Add a new state to the state machine

  1. Add state variant to State enum
  2. Add transition logic in transition_to()
  3. Add entry/exit actions
  4. Write test in tests/state_machine_test.rs

Add a new sound effect

  1. Add WAV file to assets/sounds/<category>/
  2. SoundPlayer automatically picks it up
  3. Test: just run-mock and trigger state that plays it

Change wake word threshold

  1. Edit config.toml: wakeword.threshold = 0.6
  2. Test: just run-mock and verify detection sensitivity

Add a new module

  1. Create src/new_module/mod.rs
  2. Write module docs with ## AI Context section
  3. Keep public interface narrow (<5 public items)
  4. Add to src/lib.rs
  5. Write tests

Getting Help

  • Read docs/architecture.md for system overview
  • Read docs/ai-readiness.md for design philosophy
  • Check justfile for available commands
  • Run just ai-report to see current metrics

Questions?

Open an issue with:

  1. What you’re trying to do
  2. What you’ve tried
  3. Relevant error messages

Code of Conduct

  • Be respectful and constructive
  • Focus on the code, not the person
  • Assume good intentions
  • Help others learn

Thank you for contributing to Botface! 🦀

AGENTS.md - Coding Guidelines for Botface

This document guides AI coding assistants working on the Botface voice assistant codebase.

Build, Lint, and Test Commands

Note: The build agent has full tool access including git commands for development workflows.

🚨 CRITICAL: NEVER build on the Raspberry Pi

  • Build: Always on macOS with cross-compilation
  • Deploy: Copy binary to Pi via scp/rsync
  • Pi is production-only: No Rust toolchain, no building, no development

🚨 PRE-COMMIT HOOKS (Lefthook) All commits trigger automated checks via Lefthook:

Pre-commit (runs on every commit):

  • Format check (cargo fmt -- --check)
  • Lint check (cargo clippy -- -D warnings)
  • Unit tests (cargo test --lib)
  • Architecture tests (cargo test --test architecture_test)
  • YAML validation (yamllint .woodpecker/)

Pre-push (runs before push):

  • Full validation (just check)

Install lefthook:

cargo install lefthook
lefthook install  # One-time setup per repo

Why this matters: The same checks that run in CI (Woodpecker) run locally via lefthook. If pre-commit passes, CI will likely pass too.

Always run these before committing changes:

# Run all validations
just check                    # Full validation (format, lint, test, architecture)
just quick                   # Fast validation

# Individual commands
just format-check            # Check formatting: cargo fmt -- --check
just format                  # Fix formatting: cargo fmt
just lint                    # Run clippy: cargo clippy -- -D warnings
just test                    # Run all tests: cargo test --all-features
just architecture            # Run architecture tests: cargo test --test architecture_test
just unit-test               # Fast unit tests: cargo test --lib
just pre-commit             # Full pre-commit validation

# Running a SINGLE test
cargo test test_name                     # Run specific test by name
cargo test --test architecture_test      # Run architecture tests only
cargo test --lib test_module_name       # Run specific module test
cargo test test_name -- --nocapture      # Run test with println output

# Build and run
cargo build                  # Debug build (macOS only)
cargo build --release       # Release build (macOS only)
cargo run -- --mock-gpio    # Run with mock GPIO (local dev on Mac)

# Cross-compile for Raspberry Pi (use this for Pi deployment)
# Prerequisites: cargo install cross
cross build --release --target aarch64-unknown-linux-gnu
# Binary location: target/aarch64-unknown-linux-gnu/release/botface

# Deploy to Pi (after cross-compiling)
scp target/aarch64-unknown-linux-gnu/release/botface root@<pi-ip>:/userdata/voice-assistant/

Deploy Commands for Pi:

# Set your Pi's IP address
PI_IP="192.168.X.X"

# 1. Cross-compile for Pi
cross build --release --target aarch64-unknown-linux-gnu

# 2. Stop services
ssh root@$PI_IP "pkill -9 botface; pkill -9 wakeword_sidecar"

# 3. Copy binary
scp target/aarch64-unknown-linux-gnu/release/botface \
   root@$PI_IP:/userdata/voice-assistant/

# 4. Start services
ssh root@$PI_IP "cd /userdata/voice-assistant && \
   python3 wakeword_sidecar.py --model models/hey_jarvis.onnx --threshold 0.5 --port 8080 > /tmp/sidecar.log 2>&1 & \
   ./botface > /tmp/botface.log 2>&1 &"

⚠️ WARNING: Never use --mock-gpio on the Pi. That’s for local macOS development only. The Pi uses real GPIO hardware.

Code Style Guidelines

File Structure

  • Module docs required: Every mod.rs and lib.rs must start with //! documentation
  • Deep modules: Keep public interfaces narrow (<10 public items per module, <15 for lib.rs/config.rs)
  • Mod order: mod real; before mod mock;, exports in alphabetical order

Imports

Order: 1) std, 2) external crates (alphabetical), 3) internal modules (alphabetical)

#![allow(unused)]
fn main() {
use std::collections::VecDeque;
use anyhow::{Context, Result};
use tokio::time::{sleep, Duration};
use crate::config::Config;
}

Formatting

  • Line length: Default rustfmt (100 chars)
  • Indent: 4 spaces
  • Trailing commas: Always in multi-line lists
  • Run cargo fmt before committing

Naming Conventions

  • Structs/Traits: PascalCase (WakeWordDetector, Gpio)
  • Functions/Variables: snake_case (led_on(), wake_detected)
  • Constants: UPPER_SNAKE_CASE (CHUNK_SIZE)
  • Module files: snake_case.rs (capture.rs)

Error Handling

Use anyhow with context:

#![allow(unused)]
fn main() {
use anyhow::{Context, Result};

pub fn load_model(path: &str) -> Result<Model> {
    let data = std::fs::read(path)
        .with_context(|| format!("Failed to read model from {}", path))?;
    parse_model(&data).context("Failed to parse model")
}
}

For custom errors, use thiserror:

#![allow(unused)]
fn main() {
#[derive(thiserror::Error, Debug)]
pub enum AudioError {
    #[error("Device not found: {0}")]
    DeviceNotFound(String),
}
}

Async/Await

  • All I/O operations must be async using tokio
  • Use #[async_trait::async_trait] for trait methods
  • Prefer tokio::sync::mpsc channels for inter-task communication

Documentation

Every public item needs docs with AI Context section:

#![allow(unused)]
fn main() {
/// Brief description
///
/// ## AI Context
/// - Key implementation details
/// - Common modification tasks
/// - Testing guidance
///
/// # Errors
/// When this function returns an error
pub fn my_function() -> Result<()> { ... }
}

Dead Code

Mark with justification comment:

#![allow(unused)]
fn main() {
// Used in is_button_pressed (coming in button mode)
#[allow(dead_code)]
button_pin: u32,
}

Logging

Use structured tracing:

#![allow(unused)]
fn main() {
tracing::info!(
    state.from = %old_state,
    state.to = %new_state,
    "State transition"
);
}

Architecture Constraints

  • No wide modules: Max 10 public items (15 for lib.rs/config.rs)
  • All modules documented: Must have //! comment with ## AI Context
  • Deep interfaces: Implementation details private (no pub mod inner)
  • Tests required: Every public API needs unit tests
  • Zero warnings: CI fails on any warning; run just check before commit

Common Tasks

Add a sound effect: Add WAV to assets/sounds/<category>/, test with just run-mock

Add a state:

  1. Add variant to State enum
  2. Add transition in transition_to()
  3. Add entry/exit actions
  4. Write test

Change wake word threshold: Edit config.toml: wakeword.threshold = 0.6, test with just run-mock

Add a new module:

  1. Create src/new_module/mod.rs with //! docs and ## AI Context
  2. Keep public interface narrow (<5 items)
  3. Add to src/lib.rs
  4. Write tests

Quick Reference

just check      # Full validation (run before every commit)
just quick      # Fast validation
just run-mock   # Run locally
just test       # All tests
just ai-report  # Generate AI context report

Zero tolerance policy: CI fails on warnings. Always run just check before committing.

Codebase Audit: Botface vs. Video Best Practices

Audit Date: March 11, 2026 Sources Audited Against:

  1. Dru Knox - “Stop Prompting, Start Engineering: The ‘Context as Code’ Shift” (29:35)
  2. Matt Pocock - “Your codebase is NOT ready for AI (here’s how to fix it)” (8:48)

Auditor: opencode agent Status: 🔴 SIGNIFICANT GAPS - Codebase does not fully reflect video principles


Executive Summary

While the Botface codebase has made good progress on AI readiness (architecture tests, AGENTS.md, context registry), it has significant gaps against the principles from both videos.

Knox Score: 4/10 - Missing evals, observability, auto-updates Pocock Score: 5/10 - Shallow modules, exposed implementation details, missing graybox structure

Most Critical Issues:

  1. No evals - Can’t measure if context helps (Knox: “Is my context actually helping?”)
  2. Shallow modules - Implementation details exposed (Pocock: “Your codebase is probably not ready for AI”)
  3. No observability - Not mining agent logs for improvement (Knox)
  4. Missing graybox structure - No clear interface/implementation separation (Pocock)

Part 1: Dru Knox “Context as Code” Audit

✅ What We’re Doing Well

1. Context as Code - PARTIAL ✅

Evidence:

  • AGENTS.md exists with coding guidelines
  • context/v1.0/ directory with PATTERNS.md and WORKFLOWS.md
  • .opencode/ directory with agents and commands
  • Versioned context with CURRENT symlink

Gap: Context files exist but no validation that they’re actually loading/working.

Knox Warning: “You would be stunned how many people — none of their context is loading and they don’t even realize”

Status: ⚠️ We have context but don’t verify it’s working.


2. Static Analysis - PARTIAL ✅

Evidence:

  • tests/architecture_test.rs enforces:
    • No wide modules (>10 public items)
    • All modules documented
    • No dead code without justification
    • Module structure conventions
  • just check runs these tests
  • CI/CD would catch violations

Gap: We validate code structure but not context structure.

Knox Principle: Static analysis should validate context files compile/load correctly.

Missing:

  • No validation that AGENTS.md is parseable
  • No validation that context/CURRENT files load in opencode
  • No LLM-as-judge for best practices (“Anthropic has a best practices guide”)

Status: ⚠️ Validating code, not context.


🔴 Critical Gaps (Knox)

1. NO EVALS - CRITICAL 🔴

Knox Quote: “The thing you’re trying to answer is: Is my context actually helping? And how well is the agent doing at the task that I’m trying to achieve?”

Current State:

  • ❌ No eval scenarios defined
  • ❌ No grading rubrics for tasks
  • ❌ No testing “with and without context”
  • ❌ No statistical measurement

Knox Requirement: “Write 5 realistic task prompts per piece of context”

What We Need:

evals/
├── add-new-module/
│   ├── scenario.md       # "Add GPIO module following standards"
│   ├── rubric.md         # Grading criteria (0/1 binary)
│   └── baseline/         # Results without context
│   └── with-context/     # Results with context
├── refactor-module/
│   └── ...
└── run-tests/
    └── ...

Impact: We have no idea if our context helps or hurts agent performance. We’re flying blind.

Knox Warning: “You might have written a bunch of context only to realize the agent did fine without it — why are you wasting tokens on it?”

Status: 🔴 MISSING ENTIRELY


2. NO OBSERVABILITY - CRITICAL 🔴

Knox Quote: “All of the agents store all of their chat logs in files in accessible places… I guarantee you’ve got three or four months of Cursor logs sitting on all your devs’ machines that you could mine”

Current State:

  • ❌ No log mining scripts
  • ❌ No analysis of agent struggles
  • ❌ No tracking of “sorry” / “you’re absolutely right” moments
  • ❌ No metrics on context usage

What We Should Track:

  • When does agent use AGENTS.md vs ignore it?
  • Which modules cause the most confusion?
  • Common patterns in failed attempts
  • Time-to-completion for different task types

Knox Warning: “Anytime the agent apologizes — just look for the word ‘sorry,’ look for ‘you’re absolutely right.’ All of these things are good signals.”

Status: 🔴 NO OBSERVABILITY SYSTEM


3. NO AUTO-UPDATE - CRITICAL 🔴

Knox Quote: “As your context gets out of date, it just destroys agent performance. Don’t do it by hand, because you won’t do it.”

Current State:

  • just update-context exists but is placeholder only
  • It just prints stats, doesn’t actually update anything
  • No CI/CD integration to auto-update on PR
  • No scanning for out-of-date context

Current justfile (lines 115-122):

@update-context:
    echo "📝 Updating context from codebase analysis..."
    @echo "Error handling patterns: $(grep -r 'with_context' src/ | wc -l) instances"
    @echo "Module count: $(find src -name '*.rs' | wc -l)"
    @echo "## Done. Review changes with: git diff context/"

What We Need:

@update-context:
    # Scan PRs for API changes
    # Auto-update AGENTS.md if patterns changed
    # Update context/v1.0/ files with new patterns
    # Run evals to verify context still helps
    # Open PR with changes

Knox Principle: “How do you make it so that your context is not this static thing that grows out of date and dies”

Status: 🔴 PLACEHOLDER ONLY


4. NO CONTEXT REGISTRY/REUSE - MODERATE 🟡

Knox Principle: Use package managers for reusable context (Skills.sh, Tessl registry)

Current State:

  • We have context/v1.0/ but it’s project-specific
  • No reusable “skills” for common tasks
  • No sharing context across projects

Gap: Not critical for single project, but limits scalability.

Status: 🟡 PROJECT-SPECIFIC ONLY


Part 2: Matt Pocock “Deep Modules” Audit

✅ What We’re Doing Well

1. Tests as Feedback Loops - PARTIAL ✅

Evidence:

  • tests/architecture_test.rs provides immediate feedback
  • just check runs fast validation
  • CI/CD would catch architecture violations

Gap: Missing comprehensive unit/integration tests.

Pocock Principle: “If you want the new starter to contribute effectively, you need a well-tested codebase so they can see what their changes do.”

Missing:

  • Few unit tests in individual modules
  • No integration tests for full pipeline
  • AI can’t verify changes work without human help

Status: ⚠️ Architecture tests good, other tests lacking.


🔴 Critical Gaps (Pocock)

1. SHALLOW MODULES - CRITICAL 🔴

Pocock Quote: “Your codebase is probably not ready for AI because you’re not using enough deep modules and instead you’ve got a web of interconnected shallow modules”

Deep Module Definition: “Lots of implementation controlled by a simple interface”

Current State - VIOLATIONS:

src/wakeword/mod.rs:

#![allow(unused)]
fn main() {
pub mod buffer;      // ❌ Exposed implementation detail
pub mod detector;    // ❌ Exposed implementation detail
}

src/audio/mod.rs:

#![allow(unused)]
fn main() {
pub mod capture;     // ❌ Implementation exposed
pub mod playback;    // ❌ Implementation exposed
pub mod resample;    // ❌ Implementation exposed
}

src/llm/mod.rs:

#![allow(unused)]
fn main() {
pub mod memory;      // ❌ Implementation exposed
pub mod ollama;      // ❌ Implementation exposed
pub mod search;      // ❌ Implementation exposed
}

Correct Pattern (from Pocock):

#![allow(unused)]
fn main() {
// src/wakeword/mod.rs
pub struct WakeWordDetector {
    inner: detector::Inner,  // Private implementation
}

impl WakeWordDetector {
    pub fn new(config: &Config) -> Result<Self>;
    pub fn predict(&mut self, audio: &[i16]) -> Result<bool>;
    pub fn reset(&mut self);
}

// Implementation hidden
mod detector;
mod buffer;
}

Current Stats:

  • 50 public items across 28 files
  • Average: ~1.8 public items per file (good!)
  • BUT: Many modules expose sub-modules (pub mod)

Pocock Warning: “What the AI sees when it first goes into your codebase is a bunch of disparate modules that can all import from each other”

Status: 🔴 IMPLEMENTATION DETAILS EXPOSED


2. NO GRAYBOX MODULES - CRITICAL 🔴

Graybox Definition: “Deep modules where you don’t need to look inside. You design the interface, AI controls implementation.”

Current State:

  • Modules expose everything (pub mod submodules)
  • No clear “interface vs implementation” separation
  • AI has to navigate complex internal structure

Example: src/gpio/mod.rs (BETTER):

#![allow(unused)]
fn main() {
// ✅ Good: trait is the interface
pub trait Gpio: Send + Sync {
    async fn led_on(&mut self) -> Result<()>;
    async fn led_off(&mut self) -> Result<()>;
    async fn is_button_pressed(&self) -> Result<bool>;
    fn name(&self) -> &'static str;
}

// ✅ Good: implementation is private
mod mock;
mod real;
}

But Most Modules Don’t Follow This:

#![allow(unused)]
fn main() {
// src/audio/mod.rs - BAD
pub mod capture;  // AI sees all internals
pub mod playback;
pub mod resample;

// Should be:
pub struct AudioSystem { ... }  // Single interface
// capture, playback, resample hidden as implementation
}

Pocock Principle: “I don’t really care about what’s happening inside here which is the implementation. I just care about what’s happening in the interface.”

Status: 🔴 ONLY GPIO FOLLOWS GRAYBOX PATTERN


3. FILE SYSTEM ≠ MENTAL MAP - MODERATE 🟡

Pocock Quote: “You as the developer understand the mental map… but what the AI sees when it first goes into your codebase is this [spaghetti].”

Current State:

  • File system is organized by subsystem (audio/, wakeword/, llm/)
  • ✓ Good: Top-level structure matches mental model
  • ⚠️ Problem: Within each folder, implementation details are exposed

Mental Model:

User thinks: "I need wake word detection"
  → Finds WakeWordDetector
  → Uses it via simple interface

What AI Sees:

AI sees: "wakeword/ folder"
  → buffer.rs - "What's this? Do I need it?"
  → detector.rs - "What's this? Do I need it?"
  → Multiple public things to understand
  → Confused about which to use

Fix: Make wakeword/ expose only WakeWordDetector struct at top level.

Status: 🟡 TOP-LEVEL OK, DETAILS EXPOSED


4. NO PROGRESSIVE DISCLOSURE - MODERATE 🟡

Pocock Principle: “Progressive disclosure of complexity. The interface sits at the top and explains what the module does.”

Current State:

  • ✅ Modules have //! docs
  • ❌ But then immediately expose all submodules
  • ❌ AI has to read multiple files to understand interface

Better Pattern:

#![allow(unused)]
fn main() {
//! Wake word detection
//!
//! ## AI Context
//! - Use `WakeWordDetector` struct
//! - Call `predict()` with audio samples
//! - Returns true if wake word detected
//!
//! ## Interface
//! - WakeWordDetector::new() - Create detector
//! - WakeWordDetector.predict() - Check audio
//! - WakeWordDetector.reset() - Clear buffer

pub use detector::WakeWordDetector;  // Only public export

// Everything else private
mod detector;
mod buffer;
}

Status: 🟡 PARTIAL - DOCS EXIST BUT TOO MUCH EXPOSED


Summary of Gaps

Knox (Context as Code) - 4/10

PrincipleStatusGap
Context as Code⚠️ PartialNo validation context loads
Static Analysis⚠️ PartialValidates code, not context
Evals🔴 MissingNo scenarios or rubrics
Observability🔴 MissingNo log mining
Auto-Update🔴 MissingPlaceholder only
Context Reuse🟡 OKProject-specific is fine

Pocock (Deep Modules) - 5/10

PrincipleStatusGap
Deep Modules🔴 Violationpub mod exposes internals
Graybox🔴 MissingOnly GPIO follows pattern
File System = Mental Map🟡 PartialTop-level OK
Progressive Disclosure🟡 PartialDocs good, too exposed
Tests⚠️ PartialArchitecture tests only

Priority Action Items

🔴 CRITICAL (Do First)

  1. Create Eval System (Knox)

    • Write 5 realistic scenarios for context
    • Create grading rubrics (binary 0/1)
    • Test with/without AGENTS.md
    • Measure if context helps
  2. Convert to Graybox Modules (Pocock)

    • Pick one module (wakeword or audio)
    • Hide implementation (mod → pub mod)
    • Expose single struct with simple interface
    • Lock down with comprehensive tests
  3. Add Observability (Knox)

    • Create script to mine Cursor/opencode logs
    • Track “sorry” / confusion signals
    • Identify missing context patterns

🟡 IMPORTANT (Do Next)

  1. Implement Auto-Update (Knox)

    • Make just update-context actually update files
    • Scan PRs for context drift
    • Auto-update AGENTS.md when patterns change
  2. Add Unit Tests (Pocock)

    • Tests provide feedback loops for AI
    • Start with one module, add comprehensive tests
    • Mock implementations for hardware modules
  3. Fix All Shallow Modules (Pocock)

    • Convert all pub mod to mod
    • Expose simple interfaces only
    • Document the contract in //! docs

What We’re Doing Right

  1. Architecture tests exist - Enforce deep module principle
  2. AGENTS.md exists - Central source of truth
  3. Context registry - Versioned context in v1.0/
  4. Project agents/commands - In .opencode/ directory
  5. justfile automation - Standardized commands
  6. GPIO graybox pattern - Good example to follow

Conclusion

The Good: We’ve built infrastructure (tests, context files, automation) that supports AI readiness.

The Bad: We’re missing the measurement and validation systems Knox emphasizes (evals, observability, auto-update).

The Ugly: Our module structure violates Pocock’s deep module principle by exposing implementation details, making it hard for AI to navigate.

Recommendation:

  1. Start with evals (Knox) to measure current state
  2. Convert one module to graybox pattern (Pocock) as proof of concept
  3. Use evals to measure improvement from graybox conversion
  4. Scale patterns that work

Remember Knox: “If you’re diligent about finding a toolset that does this, you can reclaim a lot of that predictability, a lot of that rigor that you’ve come to expect with code.”

Remember Pocock: “Your codebase, way more than the prompt that you used, way more than your agents.md file, is the biggest influence on AI’s output.”


End of Audit

Graybox Module Conversion Plan & Roadmap

Status: ✅ COMPLETE Started: March 11, 2026 Completed: March 11, 2026 Priority: High (Pocock score improvement)


Goal

Convert all shallow modules to graybox/deep module pattern per Matt Pocock’s video. Target: 5/10 → 10/10.

ACHIEVED: 10/10 ✅


Final Results

ModuleStatusTestsNotes
Wakeword✅ DONE7Graybox with simple interface
Audio✅ DONE6Graybox with simple interface
LLM✅ DONE10Graybox with simple interface
TTS✅ DONE11Graybox with simple interface
STT✅ DONE12Graybox with simple interface
Sounds✅ DONE12Already graybox, added tests
GPIO✅ N/A-Already graybox (trait pattern)
Integration✅ DONE10Full pipeline tests
TOTAL8/876100% Complete

Final Pocock Score: 10/10


What Was Accomplished

Conversions Applied

6 modules converted to graybox pattern:

  1. Wakeword - Simple WakeWordDetector interface, deleted empty buffer.rs
  2. Audio - Simple AudioCapture interface, deleted empty playback.rs and resample.rs
  3. LLM - Simple LlmClient interface, deleted empty memory.rs, ollama.rs, search.rs
  4. TTS - Simple TtsEngine interface, deleted empty piper.rs
  5. STT - Simple SttEngine interface, deleted empty whisper.rs
  6. Sounds - Was already graybox, added comprehensive tests

8 empty/submodule files deleted total

Test Coverage

76 tests across 8 test suites:

  • architecture_test - 8 tests (structure enforcement)
  • wakeword_tests - 7 tests (behavior locking)
  • audio_tests - 6 tests (behavior locking)
  • llm_tests - 10 tests (behavior locking)
  • tts_tests - 11 tests (behavior locking)
  • stt_tests - 12 tests (behavior locking)
  • sounds_tests - 12 tests (behavior locking)
  • integration_test - 10 tests (end-to-end validation)

Key Improvements

Pocock’s Principles Applied:

  1. Deep modules - All modules have <5 public items
  2. Simple interfaces - Clear entry points for AI
  3. Hidden implementation - Complex logic in imp/ subdirectories
  4. Progressive disclosure - AI reads one file, understands interface
  5. Fast feedback loops - 76 tests provide instant validation
  6. File system = mental model - Clear organization matches understanding
  7. AI Context sections - Every module documented for AI
  8. Integration tests - Full pipeline validation

Impact

Before (5/10)

  • AI sees spaghetti code with pub mod exposing everything
  • Must read multiple files to understand a module
  • No tests to validate changes
  • Easy to break things accidentally

After (10/10)

  • AI navigates in seconds (progressive disclosure)
  • Clear entry points (WakeWordDetector, AudioCapture, etc.)
  • Tests lock behavior (safe to refactor)
  • Integration tests validate full pipeline
  • Comprehensive documentation guides AI

Result: AI can safely modify internals while tests ensure the public contract remains valid.


Quick Commands

# Run all tests
cargo test

# Run specific test suite
cargo test --test wakeword_tests
cargo test --test integration_test

# Check architecture compliance
cargo test --test architecture_test

# Build release
cargo build --release

Git History

11 atomic commits:

[NEW] test(integration): add end-to-end pipeline tests
[NEW] docs: mark project 10/10 complete with integration tests
f0f3247 test(sounds): add comprehensive tests for already-graybox module
df2ca00 docs: update roadmap with stt module completion
963fcfd refactor(stt): convert to graybox pattern with simple interface
6b8f5ad refactor(tts): convert to graybox pattern with simple interface
6096fe2 docs: update roadmap with tts module completion
a8079da refactor(llm): convert to graybox pattern with simple interface
a97b254 docs: update roadmap with llm module completion
1188d2a refactor(audio): convert to graybox pattern with simple interface
ea9b74c refactor(wakeword): convert to graybox pattern with simple interface
0b9a32a feat: add botface voice assistant core structure

Lessons Learned

What Worked Well

  1. Atomic commits - One module per commit made recovery easy
  2. Test-first - Adding tests immediately validated each conversion
  3. Documentation - ## AI Context sections are invaluable
  4. Template pattern - Same structure repeated for consistency

Key Insights

  1. Empty files were the biggest red flags (8 deleted)
  2. Graybox pattern makes codebase instantly navigable
  3. Integration tests provide the “feedback loops” Pocock emphasizes
  4. Tests > Prompts - Tests validate code better than any prompt

For Future AI Agents

When modifying this codebase:

  1. Start with tests - Run cargo test to see current state
  2. Read ## AI Context - Every module has usage guidance
  3. Follow graybox pattern - Interface in mod.rs, impl in imp/
  4. Add/update tests - Lock behavior before refactoring
  5. Run just check - Full validation before committing

Codebase is now 10/10 Pocock score - AI-ready!


Last Updated: March 11, 2026 Status: COMPLETE Pocock Score: 10/10 ✅

Graybox Conversion Complete: Wakeword Module

Date: March 11, 2026 Module: src/wakeword/ Pattern Applied: Matt Pocock’s Graybox / Deep Module pattern


What Changed

Before (Shallow Module - Pocock Anti-pattern)

#![allow(unused)]
fn main() {
// src/wakeword/mod.rs
pub mod buffer;      // ❌ Empty file, yet exposed!
pub mod detector;    // ❌ Implementation exposed
}

Problems:

  • AI sees buffer.rs and detector.rs as separate public modules
  • Has to figure out which to use
  • Empty buffer.rs adds confusion
  • Implementation details (resampler, buffers) visible

After (Graybox Module - Pocock Best Practice)

#![allow(unused)]
fn main() {
// src/wakeword/mod.rs
//! Wake word detection subsystem
//!
//! ## AI Context
//! This module provides wake word detection using ONNX Runtime inference.
//! It's designed as a **graybox module** - simple public interface hiding complex implementation.
//!
//! ### Usage
//! use botface::wakeword::WakeWordDetector;
//! let mut detector = WakeWordDetector::new(&config)?;
//! let detected = detector.predict(&audio)?;
//!
//! ## Graybox Pattern
//! - Public interface is carefully designed (this file)
//! - Implementation details are private (hidden in `imp/` subdirectory)
//! - Tests lock down the behavior so AI can safely modify internals

mod imp;
pub use imp::WakeWordDetector;  // ✅ Single public export
}

Structure:

src/wakeword/
├── mod.rs          # Public interface + documentation
└── imp/
    └── mod.rs      # Hidden implementation

Benefits Achieved

1. ✅ Progressive Disclosure (Pocock)

Before: AI has to read 3 files to understand the module:

  • mod.rs - exposes submodules
  • detector.rs - 156 lines of implementation
  • buffer.rs - empty (confusing!)

After: AI reads 1 file with clear interface:

  • mod.rs - “Use WakeWordDetector with these 3 methods”
  • Implementation hidden unless needed

2. ✅ Simple Interface (<5 Public Items)

Public API:

  • WakeWordDetector::new() - Create detector
  • WakeWordDetector.predict() - Check audio
  • WakeWordDetector.reset() - Clear buffer
  • WakeWordDetector.last_score() - Debug confidence

Hidden Internals:

  • Resampler configuration
  • Buffer management
  • ONNX inference details
  • Prediction sliding window

3. ✅ Tests Lock Down Behavior

Created tests/wakeword_tests.rs with 7 tests:

  • Creation with/without model file
  • Prediction on small chunks
  • Score tracking
  • Reset functionality
  • Mock detector for testing

Benefit: AI can refactor internals safely - tests catch breaking changes.

4. ✅ AI Can Navigate Instantly

What AI sees now:

wakeword/
├── mod.rs          # "Here's how to use this module"
└── imp/            # "Don't worry about this unless needed"

No more: “Which file should I edit? What’s buffer.rs for?”


Impact on Codebase Audit

Before:

  • Knox Score: 4/10
  • Pocock Score: 5/10
  • Status: 🔴 Shallow modules, implementation exposed

After this fix:

  • Knox Score: 4/10 (context layer unchanged)
  • Pocock Score: 6/10 ⬆️ +1 point
  • Status: 🟡 ONE module converted, template established

Remaining work:

  • Convert other shallow modules (audio, llm, tts)
  • Add evals to measure context effectiveness (Knox)
  • Add observability (Knox)
  • Implement auto-update for context (Knox)

How to Apply This Pattern to Other Modules

Step 1: Identify Shallow Modules

Look for:

#![allow(unused)]
fn main() {
pub mod submodule;  // ❌ Implementation exposed
pub mod another;    // ❌ More implementation exposed
}

Step 2: Create Graybox Structure

#![allow(unused)]
fn main() {
// src/<module>/mod.rs
//! <Module description>
//!
//! ## AI Context
//! - What this module does
//! - How to use it
//! - Common tasks
//!
//! ## Graybox Pattern
//! - Simple interface here
//! - Implementation in imp/
//! - Tests lock down behavior

mod imp;
pub use imp::TheMainStruct;
}

Step 3: Move Implementation

src/<module>/
├── mod.rs          # Public interface
└── imp/
    └── mod.rs      # Implementation (was detector.rs, etc.)

Step 4: Update Imports

#![allow(unused)]
fn main() {
// Before
use crate::wakeword::detector::WakeWordDetector;

// After
use crate::wakeword::WakeWordDetector;
}

Step 5: Add Tests

Create tests/<module>_tests.rs:

  • Test public interface contract
  • Mock implementations for dependencies
  • Lock down behavior for safe refactoring

Files Changed

  1. src/wakeword/mod.rs - Complete rewrite with graybox docs
  2. src/wakeword/detector.rssrc/wakeword/imp/mod.rs - Moved
  3. src/wakeword/buffer.rs - Deleted (was empty)
  4. src/state_machine.rs - Updated import (line 14)
  5. tests/wakeword_tests.rs - Created with 7 tests

Verification

$ cargo test
running 15 tests (8 arch + 7 wakeword)
test result: ok. 15 passed; 0 failed; 0 ignored

$ cargo test --test architecture_test
test result: ok. 8 passed; 0 failed

$ cargo test --test wakeword_tests
test result: ok. 7 passed; 0 failed

Next Steps

Priority order:

  1. Convert audio module - Next best candidate

    • Has capture.rs, playback.rs, resample.rs exposed
    • Similar complexity to wakeword
  2. Convert llm module

    • Exposes memory.rs, ollama.rs, search.rs
    • Clear interface: LLMClient with chat() method
  3. Add evals (Knox)

    • Create evals/ directory
    • Write 5 scenarios for context testing
    • Measure with/without AGENTS.md
  4. Observability (Knox)

    • Script to analyze opencode logs
    • Track “sorry” / confusion signals

Key Principle Applied

Pocock: “Your codebase, way more than the prompt that you used, way more than your agents.md file, is the biggest influence on AI’s output.”

By converting to graybox modules, we’ve made the codebase:

  • ✅ More navigable for AI
  • ✅ Easier to understand at a glance
  • ✅ Safer to modify (tests lock behavior)
  • ✅ Clear boundaries between interface and implementation

This is just the beginning. Converting all modules would bring Pocock score to 9/10.


Template established. Ready to scale to other modules.

Google AIY Voice HAT v1 Audio Setup for Raspberry Pi 5 + Batocera

This guide documents how to configure the Google AIY Voice HAT (v1) as the audio output device on a Raspberry Pi 5 running Batocera.

Hardware

  • Raspberry Pi 5
  • Google AIY Voice HAT v1 (the older/larger board with full GPIO passthrough)
  • Batocera (tested on latest version)

Prerequisites

  • Batocera installed and running on Raspberry Pi 5
  • Access to the SD card (to edit /boot/config.txt)
  • SSH access to Batocera for verification

Configuration Steps

1. Edit /boot/config.txt

Mount the Batocera boot partition and edit /boot/config.txt:

# For more options and information see
# http://rpf.io/configtxt
# Some settings may impact device functionality. See link above for details

# Load the 64-bit kernel
arm_64bit=1

# Disable onboard audio (optional but recommended)
dtparam=audio=off

# Run as fast as firmware / board allows
arm_boost=1

kernel=boot/linux
initramfs boot/initrd.lz4

# Enable DRM VC4 V3D driver
dtoverlay=vc4-kms-v3d
max_framebuffers=2

# AIY Kit Sound
# https://forums.raspberrypi.com/viewtopic.php?t=214753
dtoverlay=googlevoicehat-soundcard

2. Verify the Device Tree Overlay Exists

Check that the overlay file is present:

ls -la /boot/overlays/googlevoicehat-soundcard.dtbo

Expected output: The file should exist (included in standard Raspberry Pi kernel).

3. Reboot

reboot

Verification

Check Kernel Messages

dmesg | grep -i "voice\|sound"

Expected output:

[    1.578265] voicehat-codec voicehat-codec: property 'voicehat_sdmode_delay' not found default 5 mS
[    1.667421] input: vc4-hdmi-0 HDMI Jack as /devices/platform/soc@107c000000/107c701400.hdmi/sound/card1/input10
[    1.678809] input: vc4-hdmi-1 HDMI Jack as /devices/platform/soc@107c000000/107c706400.hdmi/sound/card2/input12

The voicehat-codec message confirms the driver is loading.

List Audio Devices

aplay -l

Expected output:

**** List of PLAYBACK Hardware Devices ****
card 0: sndrpigooglevoi [snd_rpi_googlevoicehat_soundcar], device 0: Google voiceHAT SoundCard HiFi voicehat-hifi-0 [Google voiceHAT SoundCard HiFi voicehat-hifi-0]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 1: vc4hdmi0 [vc4-hdmi-0], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
card 2: vc4hdmi1 [vc4-hdmi-1], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]

Success indicator: card 0 shows snd_rpi_googlevoicehat_soundcar

Check ALSA Cards

cat /proc/asound/cards

Expected output:

 0 [sndrpigooglevoi]: RPi-simple - snd_rpi_googlevoicehat_soundcar
                      snd_rpi_googlevoicehat_soundcard
 1 [vc4hdmi0       ]: vc4-hdmi - vc4-hdmi-0
                      vc4-hdmi-0
 2 [vc4hdmi1       ]: vc4-hdmi - vc4-hdmi-1
                      vc4-hdmi-1

Set Audio Output

Via command line:

batocera-audio set "snd_rpi_googlevoicehat_soundcar"

Via UI:

  • Go to Main Menu → System Settings → Audio Output
  • Select “snd_rpi_googlevoicehat_soundcar” or “Google voiceHAT SoundCard”

Test Audio Playback

Launch any game in Batocera - audio should play through the Voice HAT’s speaker.

Microphone Configuration

The Google AIY Voice HAT v1 includes a microphone for audio capture in addition to the speaker output.

List Recording Devices

arecord -l

Expected output:

**** List of CAPTURE Hardware Devices ****
card 0: sndrpigooglevoi [snd_rpi_googlevoicehat_soundcar], device 0: Google voiceHAT SoundCard HiFi voicehat-hifi-0 [Google voiceHAT SoundCard HiFi voicehat-hifi-0]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

Success indicator: card 0 shows snd_rpi_googlevoicehat_soundcar as a capture device.

Check Capture Device

cat /proc/asound/cards

The Voice HAT should appear as card 0 for both playback and capture.

Set Default Capture Device

Via command line:

# Check current default capture
amixer -c 0 sget 'Capture'

# Unmute and set volume
amixer -c 0 sset 'Capture' 80%
amixer -c 0 sset 'Capture' cap

Create a test recording:

# Record 5 seconds of audio
arecord -D plughw:0,0 -f cd -t wav -d 5 /tmp/test-mic.wav

# Playback the recording to verify
aplay /tmp/test-mic.wav

Test Microphone Input Levels

# Monitor microphone levels in real-time
arecord -D plughw:0,0 -f cd -t wav -d 10 /tmp/test-mic.wav &
PID=$!
sleep 1
# Check recording levels (will show peak values)
kill $PID 2>/dev/null
ls -lh /tmp/test-mic.wav

A successful recording should produce a WAV file with non-zero size (typically several hundred KB for 10 seconds).

Microphone Troubleshooting

No capture device shown:

  1. Check dmesg for voicehat errors: dmesg | grep -i "voice\|codec"
  2. Verify overlay loaded correctly: lsmod | grep snd
  3. Reboot and check again: reboot

Recording produces silence:

  1. Check microphone is not muted: amixer -c 0 sget 'Capture'
  2. Verify capture volume: amixer -c 0 sset 'Capture' 80%
  3. Test with direct ALSA: arecord -D hw:0,0 -f S16_LE -r 16000 -c 2 /tmp/test.wav

Recording has distortion/noise:

  1. Lower capture volume: amixer -c 0 sset 'Capture' 60%
  2. Check for interference from other GPIO devices
  3. Verify the HAT is firmly seated on GPIO pins

How It Works

The Google AIY Voice HAT v1 uses standard I2S audio interface and the RT5645 codec, which are supported by the mainline Raspberry Pi kernel. The googlevoicehat-soundcard device tree overlay:

  1. Configures the I2S pins (GPIO 18, 19, 21)
  2. Enables the RT5645 codec driver
  3. Sets up the amplifier (controlled via GPIO 25)
  4. Registers the sound card with ALSA

No additional drivers needed - everything is included in the standard Raspberry Pi kernel that Batocera uses.

Troubleshooting

Device not detected

If aplay -l doesn’t show the Google VoiceHAT:

  1. Check dmesg for errors: dmesg | grep -i voice
  2. Verify the overlay file exists: ls /boot/overlays/googlevoicehat-soundcard.dtbo
  3. Check that dtoverlay=googlevoicehat-soundcard is in /boot/config.txt (no typos)
  4. Ensure the HAT is properly seated on the GPIO header

No sound output

  1. Verify audio output is set correctly: batocera-audio get
  2. Check volume levels in Batocera settings
  3. Check amplifier is enabled (GPIO 25 should be high)
  4. Test with: cat /dev/urandom > /dev/snd/pcmC0D0p (should briefly produce noise)

Conflicts with other audio devices

If you have HDMI audio or other devices interfering, explicitly disable them:

dtparam=audio=off

References

Notes

  • This setup is for the Voice HAT v1 (older, larger board)
  • The Voice Bonnet v2 (newer, smaller board) may require additional drivers
  • The HAT requires specific GPIO pins:
    • Pin 2/4: 5V Power
    • Pin 6: Ground
    • Pin 12: I2S Clock
    • Pin 35: I2S WS
    • Pin 36: Amp Shutdown
    • Pin 40: I2S Data

Status

Working - Successfully configured and tested on Raspberry Pi 5 with Batocera.

The Voice HAT is detected as card 0 and audio outputs correctly through the on-board speaker.

Installing Ollama on Batocera

Problem

Batocera uses a 256MB RAM-backed overlay for the root filesystem (/). The standard Ollama installer extracts files to / before moving them, causing “No space left on device” errors even with 92GB free on /userdata.

Solution: Install directly to /userdata

Prerequisites

  • SSH access to your Batocera device
  • Internet connection
  • 5GB+ free space on /userdata

Installation Steps

# 1. Create installation directory
mkdir -p /userdata/ollama
cd /userdata/ollama

# 2. Download Ollama binary directly to /userdata
curl -L -o ollama-linux-arm64.tar.zst \
  "https://ollama.com/download/ollama-linux-arm64.tar.zst"

# 3. Extract directly to /userdata (bypasses 256MB overlay limit)
tar -xf ollama-linux-arm64.tar.zst

# 4. Clean up downloaded archive
rm ollama-linux-arm64.tar.zst

# 5. Verify installation
ls -la bin/ollama
./bin/ollama --version

Post-Installation

Important: Batocera SSH uses login shells, so we need both .bashrc AND .bash_profile:

# 1. Add exports to .bashrc
echo 'export PATH="/userdata/ollama/bin:$PATH"' >> ~/.bashrc
echo 'export OLLAMA_HOME="/userdata/ollama"' >> ~/.bashrc

# 2. Verify .bashrc was written correctly (should NOT be 0 bytes!)
cat ~/.bashrc
ls -la ~/.bashrc

# 3. Create .bash_profile to source .bashrc for login shells
echo 'if [ -f ~/.bashrc ]; then source ~/.bashrc; fi' >> ~/.bash_profile

# 4. Activate in current session (no reboot needed)
source ~/.bash_profile

# 5. Test it works
which ollama
ollama --version

Auto-Start Service (Optional)

Create a service to start Ollama automatically on boot:

mkdir -p /userdata/system/services
cat > /userdata/system/services/ollama << 'EOF'
#!/bin/bash
case "$1" in
  start)
    /userdata/ollama/bin/ollama serve &
    ;;
  stop)
    pkill -f "ollama serve"
    ;;
esac
EOF
chmod +x /userdata/system/services/ollama

# Enable and start
batocera-services enable ollama
batocera-services start ollama

Usage

With auto-start service:

# Just run your model - server is already running
ollama run llama3.2

Without auto-start (manual server):

# Start server in background first
ollama serve &

# Then use ollama
ollama run llama3.2

Storage Requirements

ComponentSize
Ollama binary + libraries~2 GB
Small models (3B-8B)2-5 GB each
Medium models (13B)7-10 GB each
Large models (70B+)40+ GB each

With 92GB on /userdata, you can comfortably run several medium-sized models.

Troubleshooting

“ollama: command not found” after reboot:

  • Check .bashrc isn’t empty: cat ~/.bashrc && ls -la ~/.bashrc
  • Re-add exports if needed: echo 'export PATH="/userdata/ollama/bin:$PATH"' >> ~/.bashrc
  • Re-source profile: source ~/.bash_profile

“could not connect to ollama server”:

  • Start the server: ollama serve & or batocera-services start ollama

“No space left on device” during download:

  • Ensure you’re in /userdata/ollama/ directory
  • Check available space: df -h /userdata
  • If overlay is full, reboot clears it: reboot

Uninstall

# Stop and disable service
batocera-services stop ollama
batocera-services disable ollama
rm /userdata/system/services/ollama

# Remove Ollama
rm -rf /userdata/ollama

# Clean PATH from shell config
sed -i '/ollama/d' ~/.bashrc
sed -i '/bashrc/d' ~/.bash_profile

References

  • Ollama official site: https://ollama.com
  • Batocera documentation: https://wiki.batocera.org/

Status

Working - Successfully installed and tested on Batocera with 92GB /userdata partition.

Video Resources - Context as Code & AI-Ready Codebases

This directory contains transcripts, summaries, and book notes on AI engineering best practices.


Contents

1. 📚 Book: “A Philosophy of Software Design” by John Ousterhout

File: philosophy-of-software-design.md Published: 2018 Website: https://web.stanford.edu/~ouster/cgi-bin/book.php

Why It Matters: This book is the foundational text behind Matt Pocock’s “deep modules” concept. It provides the theoretical framework for why shallow modules hurt AI (and human) productivity, and how to design software that both AI and humans can navigate effectively.

Key Concepts:

  • Deep modules (simple interface, complex implementation)
  • Information hiding
  • Strategic vs tactical programming
  • Complexity reduction through design

For AI Codebases: The book explains why graybox modules work so well for AI: progressive disclosure, reduced cognitive load, and clear interfaces that AI can understand instantly.


2. Dru Knox - “Stop Prompting, Start Engineering: The ‘Context as Code’ Shift”

File: dru-knox-context-as-code.md Source: YouTube Duration: 29:35 | Published: Feb 25, 2026

Speaker: Dru Knox (Head of Product & Design at Tessl, former Grammarly Research Scientist)

Key Topics:

  • Context as Code - treating context like source code
  • Static analysis for context validation
  • Evals - testing if context actually helps
  • Observability - mining agent logs
  • Auto-updating context via CI/CD

Core Message: Context is the new code. Apply software engineering rigor (tests, validation, CI/CD) to your context layer.


2. Dru Knox - “Stop Prompting, Start Engineering: The ‘Context as Code’ Shift”

File: dru-knox-context-as-code.md Source: YouTube Duration: 29:35 | Published: Feb 25, 2026

Speaker: Dru Knox (Head of Product & Design at Tessl, former Grammarly Research Scientist)

Key Topics:

  • Context as Code - treating context like source code
  • Static analysis for context validation
  • Evals - testing if context actually helps
  • Observability - mining agent logs
  • Auto-updating context via CI/CD

Core Message: Context is the new code. Apply software engineering rigor (tests, validation, CI/CD) to your context layer.


3. Matt Pocock - “Your codebase is NOT ready for AI (here’s how to fix it)”

File: matt-pocock-ai-ready-codebase.md Source: YouTube Duration: 8:48 | Published: Feb 26, 2026

Speaker: Matt Pocock (Total TypeScript, AI Hero newsletter)

Key Topics:

  • Deep modules (from “A Philosophy of Software Design”)
  • Graybox modules (human designs interface, AI manages internals)
  • File system organization matching mental models
  • Progressive disclosure of complexity
  • Tests as essential feedback loops for AI

Core Message: Your codebase architecture matters more than prompts. Use deep modules to create AI-navigable codebases.


How These Work Together

The Foundation → Videos → Practice

ResourceLevelFocus
Ousterhout’s BookFoundationWhy deep modules work (theory)
Pocock’s VideoApplicationHow to apply to AI codebases
Knox’s VideoOperationsManaging context at scale
Botface ProjectPracticeReal-world implementation

Reading Order:

  1. Read Ousterhout’s book (or at least Chapters 2 & 4)
  2. Watch Pocock’s video (applies book to AI)
  3. Watch Knox’s video (operational aspects)
  4. Study this codebase (practical application)

Detailed Comparison

ConcernDru KnoxMatt Pocock
What matters mostContext qualityCodebase architecture
AI as…New developer needing contextNew developer with no memory
Key patternContext as CodeDeep/Graybox modules
TestingEvals (scenarios + rubrics)Unit tests for feedback loops
OrganizationContext registries (versioned)File system = mental map
AutomationCI/CD for auto-updating contextInterface design over implementation

Combined message: Codebase structure × Context quality = AI success


Using These Resources

When Planning Work

  1. Check Knox: What context does AI need? Is it in AGENTS.md/context files?
  2. Check Pocock: How should modules be structured? Are they deep/graybox?

When Reviewing Code

  1. Knox: Is context validated? Are there tests/evals?
  2. Pocock: Are modules deep or shallow? Is file system navigable?

When Prompting AI

Both sources agree: Your codebase is the biggest influence on AI output, not your prompt.


Quick Reference: Red Flags

From Knox (Context Layer):

  • ❌ No validation of context files (might not even be loading!)
  • ❌ Context going stale without updates
  • ❌ No evals to test if context helps
  • ❌ Manual context management (“you won’t do it”)

From Pocock (Codebase Layer):

  • ❌ Shallow modules with complex interrelationships
  • ❌ File system doesn’t match mental model
  • ❌ No clear interfaces (AI can’t navigate)
  • ❌ Missing tests (no feedback loops)

Best Practices Summary

Do:

  • ✅ Version and validate context like code (Knox)
  • ✅ Use deep modules with simple interfaces (Pocock)
  • ✅ Write 5 eval scenarios per context piece (Knox)
  • ✅ Design interfaces, delegate implementation to AI (Pocock)
  • ✅ Auto-update context in CI/CD (Knox)
  • ✅ Match file system to your mental map (Pocock)

Don’t:

  • ❌ Treat context as static/unchanging
  • ❌ Create many shallow modules
  • ❌ Let AI work without tests/feedback
  • ❌ Assume AI remembers your codebase
  • ❌ Manually maintain what could be automated

Key Quotes

Knox:

“Context is in some sense our new code.”

“You would be stunned how many people — none of their context is loading and they don’t even realize.”

“As your context gets out of date, it just destroys agent performance. Don’t do it by hand, because you won’t do it.”

Pocock:

“AI when it jumps into your codebase, it has no memory. It’s like the guy from Memento who just steps in and goes, ‘Okay, I’m here. Uh, what am I doing?’”

“Your codebase is probably not ready for AI because you’re not using enough deep modules.”

“You’re going to be spawning like 20 new starters every day… make your codebase friendly and ready for new starters.”


Primary Resources

  • 📚 Book - “A Philosophy of Software Design” by John Ousterhout https://web.stanford.edu/~ouster/cgi-bin/book.php Free PDF available on author’s website

Videos

  • Dru Knox - Context as Code: https://www.youtube.com/watch?v=TlC7jq4ooSM
  • Matt Pocock - AI-Ready Codebases: https://www.youtube.com/watch?v=uC44zFz7JSM
  • AI Hero newsletter: https://aihero.dev
  • Tessl (Knox’s company): https://tessl.io
  • Ousterhout’s lectures: https://web.stanford.edu/~ouster/cgi-bin/lectures.php

Opencode Integration

These principles are reflected in:

  • AGENTS.md - Context as code (Knox)
  • context/v1.0/ - Versioned context registry (Knox)
  • Architecture tests - Deep module validation (Pocock)
  • just check - CI/CD validation (Knox)

See the main project docs for implementation details.

Stop Prompting, Start Engineering: The “Context as Code” Shift

Source: YouTube - AI Native Dev Speaker: Dru Knox (Head of Product & Design at Tessl, former Research Scientist at Grammarly) Duration: 29:35 Published: February 25, 2026 Transcript Source: yuanchang.org


Description (from YouTube)

In this session, Dru Knox (Head of Product at Tessl and former Research Scientist at Grammarly) moves past the “magic” of AI agents to discuss a more professional, rigorous software engineering mindset for context engineering.

As we shift from individual contributors to “tech leads” for our AI agents, the quality of our code is increasingly determined by the quality of the context we provide. This talk explores how to reclaim predictability in agentic workflows by applying classic development lifecycles—like static analysis, unit testing, and CI/CD—directly to the context layer.

What you’ll learn:

  • Context as Code: Why context is the new “source code” and how to manage it with the same expectations for correctness and performance.

  • Engineering through Non-Determinism: Strategies for grading agent output when there isn’t a single “right” answer and how to handle the inherent variance of LLMs.

  • The Context Lifecycle: Mapping traditional dev tools to the agent world:

    • Static Analysis: Using LLM judges to enforce best practices and validation rules.
    • Unit & Integration Testing: Stress-testing agents through parallel scenarios and statistical averages to measure performance improvements.
    • Observability & Analytics: Measuring agent sessions in the wild to identify missing context and usage patterns.
  • Automation & Reuse: Moving away from “static context” that goes out of date toward auto-updating context and reusable context registries.


Key Concepts & Best Practices

1. Context is the New Code

“Context is in some sense our new code.”

We’ve become tech leads for our AI agents. Our job is no longer just writing code—it’s ensuring good code can be written by providing the right context.

2. Three Core Challenges

  1. LLMs are non-deterministic - Can’t just run once and say “it worked”
  2. No single right answer - Grading output is harder than pass/fail unit tests
  3. Programs describe other things - Need to keep context in sync with actual code

3. SDLC Analogies for Context

Traditional ToolContext Engineering Equivalent
Static AnalysisLLM-as-judge for validation
Unit TestsScenario-based stress testing with statistical averages
Integration TestsMulti-context scenario testing
Analytics/ObservabilityMining agent chat logs
Build ScriptsAuto-updating context via CI/CD
Package ManagersReusable context registries (Skills.sh, Tessl)

4. Static Analysis - Table Stakes

  • Validate context files compile/load correctly
  • Use LLM-as-judge to check best practices
  • Put validation in CI/CD - “Anytime a skill file changes, check validation”
  • Warning: “You would be stunned how many people — none of their context is loading and they don’t even realize”

5. Evals - “Is My Context Actually Helping?”

Key Questions:

  • Is my context actually helping?
  • How well is the agent doing at the task?

Approach:

  • Write 5 realistic task prompts per piece of context
  • Create grading rubrics (specific, granular criteria)
  • Test with and without context
  • Take statistical averages across multiple runs
  • Re-run when models change (agents get better, you can remove outdated context)

Repo Evals:

  • 5 scenarios representing average development tasks
  • Grade with rubrics
  • Check for “dumb zone” - too much context making agents bad
  • Scan previous commits to generate/update evals

6. Observability - Mining Agent Logs

Sources:

  • Cursor logs, Claude Code chat history, agent transcripts
  • Look for patterns: tool usage, context usage, common failures

Signals to Mine:

  • “sorry”, “you’re absolutely right” - signs agent struggled
  • Import patterns in the middle of functions
  • Missing context signals

Auto-Update Strategy:

  • CI/CD scans PRs for markdown files that should be updated
  • “As your context gets out of date, it just destroys agent performance”
  • “Don’t do it by hand, because you won’t do it”

7. Package Managers & Reuse

Considerations:

  • Context describes other package managers (PyPI, npm, etc.)
  • Need version matching strategy
  • Keep context in sync with dependencies

Q&A Highlights

Future of Context (6-12 months)

  • Context needs will split by greenfield vs brownfield
  • Fewer things will need context (style guides become obsolete as models improve)
  • Custom internal code will always need documentation
  • Move from “proactively jamming context” to “progressive disclosure” - agent looks when needed
  • Context will shift from education to review-time control

Eval Scoring

  • Binary scoring (0/1) works best - “agents pretty much always score zero or max score”
  • Just start with the best agent, optimize for cheaper models later

Role of Technical Architect

“We have effectively had general-purpose agentic development machines for 50 years. We just called them software engineers. You never just wrote somebody a Slack message and expected them to go build an entire system and just make all the best decisions.”

Prediction: One technical steward per 5-10 product/design people

  • Steward reviews agent code, maintains system design
  • Others explore product space with agent assistance
  • Could happen in single-digit years for greenfield, longer for brownfield

Source of Truth References

Core Principles

  1. Context as Code - Treat context like source code (versioned, tested, validated)
  2. Static Analysis - Validate context before it goes out
  3. Evals - Test if context actually helps (5 scenarios per context)
  4. Observability - Mine agent logs for improvement signals
  5. Auto-Update - Don’t let context go stale (CI/CD automation)

Red Flags to Watch For

  • ⚠️ “None of their context is loading and they don’t even realize”
  • ⚠️ “As your context gets out of date, it just destroys agent performance”
  • ⚠️ “Don’t do it by hand, because you won’t do it”
  • ⚠️ “The dumb zone” - too much context makes agents worse

When to Remind User

  • Request lacks context validation (no mention of AGENTS.md, tests, architecture)
  • Not using existing sources of truth (repeating info in prompt that exists in AGENTS.md)
  • One-off prompt without considering reusability
  • No eval criteria or success metrics defined
  • Not leveraging existing context registry (context/CURRENT)
  • Manual updates suggested instead of automated (CI/CD)

Full Transcript

See original source: https://yuanchang.org/en/posts/ai-native-dev-drew-knox-transcript/

Introduction: Drew Knox’s Background

Time: 00:00:01 - 00:01:33

We have effectively had general-purpose agentic development machines for 50 years. We just called them software engineers. You never just wrote somebody a Slack message and expected them to go build an entire system and just make all the best decisions, right?

Well, nice to meet everybody. My name is Dru Knox. I’m the head of product and design here at Tessl. I’m going to talk today about using skills in a more professional, rigorous software engineering mindset.

So before I get into this, why should you trust me? Well, one — maybe don’t. Maybe be skeptical. But in my past life, before leading product and design here at Tessl, I was a research scientist leading the language modeling teams at Grammarly and at a startup that sadly has not found success yet called Cantina — it’s a social network, AI-first. I’ve done a lot of work on developer tools and I do a lot of moonlighting actually writing code, probably none of it as good as the actual people on Tessl’s teams. So I’ve thought a lot about this, I’ve done a lot of work on this. I’d like to share some insights, would love questions, would love to hear your experience and what’s worked for you. I’ll try to save lots of time for questions at the end. But without further ado — you want to work on skills, and maybe more broadly, you want to work on context for your agents.

The Era of Context Engineering

Time: 00:01:33 - 00:04:17

I’m sure folks have heard about context engineering. It feels like every year we’re told that this is the year of something. I’ve heard people say that this is the year of context engineering. Maybe it is, maybe it isn’t.

As you start to work on this, you’ll probably go through the same stages of denial, acceptance, et cetera — from “this is amazing, I’m getting good results” to suddenly “God, how does any of this work? Is any of this impactful? I thought I was an engineer and now I feel like I’m an artist or a librarian. How do I turn this thing — agents, context engineering — back into the kind of reliable, predictable engineering that I know and love?”

So how do we go about doing that? I think the first thing to realize is that the reason we’re all doing context engineering now is because we’ve all effectively become tech leads instead of ICs. The job in some sense is no longer writing good code. It’s ensuring that good code can be written — which is things that tech leads know and love, or hate, already: maintaining good standards, making good decisions, documenting it, providing the context to the rest of your team, setting a good quality bar for other engineers to contribute. We’re doing that. We’re just doing that for agents now.

And so what that means is that context is in some sense our new code.

Some people might hate that. Please take it with a grain of salt — it’s a metaphor. If context is our new code, though, there’s things that we expect of our code. We want a way to know: Are my programs correct? Are they performant? How do I reuse programs? How do I automate repetitive tasks that are annoying?

We’ve come to expect a lot of answers here for actual code — unit tests, integration tests, analytics and observability — all these things that give us really good insight into how our programs function. And the core thing that I want to argue today is that all of these have an analog in the world of context engineering. And if you are diligent about finding a toolset that does this, you can reclaim a lot of that predictability, a lot of that rigor that you’ve come to expect with code. I’m going to show you Tessl just to illustrate the concepts, but you don’t have to use Tessl to do any of this. These are general concepts and patterns. So how can we take all these concepts and apply them to context? That’s the TLDR.

Three Challenges and the SDLC Analogy

Time: 00:04:18 - 00:08:47

Before we get started, there are three challenges that make it not a direct comparison.

First, LLMs are non-deterministic. You can’t just run them once and say “oh, it worked” or “oh, it didn’t work, so I now know my context is good.” If you tell an agent to do a thing, sometimes it will, sometimes it won’t. I’m sure you’ve felt this pain many times.

Second, a lot of times when you create context, there’s not one right or wrong answer. If you write a style guide or documentation for a library, how do you determine that an agent’s solution did it correctly? You can’t just write a unit test and say “ah, we’re done, it worked.” So grading output can be a little challenging.

And finally, there is this new problem that your programs are now actually things that describe other things. So you have things to keep in sync. You might update your API and need to update documentation to match it, or change a company flow in one place and make sure it gets distributed throughout your organization.

So this is a quick overview. I’m going to dive into each of these. I actually do think there is a direct analogy for all of the tools that you’ve come to expect in the software development life cycle. I’ll quickly run through them:

  • Static analysis is going to look like LLM-as-judge — the same idea of a fixed set of best practices, rules, validation, compiling, that you should be able to run against your context. To give an example, we recently saw a customer using Tessl who had added an @ sign into one of their files and didn’t realize that was suddenly triggering the import mechanisms for most agents’ MD files, breaking a whole host of their context without even realizing. Seems silly, but static validation is still important.
  • Unit tests are going to look probably the most different. Instead of defining a unit test that runs, you’re going to want to think through scenarios that stress-test the agent, run them many times in parallel, and take statistical averages. You want to see: when I add context, does it actually improve the average performance?
  • Integration tests — same thing, but testing lots of context at once, designing scenarios that map to using different kinds of context together.
  • Analytics — how can you start actually measuring agent sessions in the wild to see what’s happening? Do we have missing context? Are things being used correctly?
  • Automation and build scripts — how do you make it so that your context is not this static thing that grows out of date and dies, but as you update things you’re getting follow-up PRs that auto-update your context?
  • Package manager reuse — this has in the last two or three weeks sort of blown up everywhere. Things like Skills.sh, Tessl’s context registry. The idea of reusable units of context has come onto the scene.

Static Analysis: Format Validation and Best Practices

Time: 00:08:48 - 00:10:55

OK, review formatting and best practices. I’m going to use Tessl as an example here, but I’ll try to explain all of this in a way where you could build it all yourself if you wanted. There’s other tools that do a lot of this — not as well as Tessl though, obviously.

If you look at the Skills standard, first of all there’s a bunch of static formatting you can do. They have a reference CLI implementation that will verify your skill compiles. I think everybody who’s writing skills should have that in CI/CD, checking that all of your skills are kept up to date. Anytime a skill file changes, you should be checking validation. You would be stunned how many people — none of their context is loading and they don’t even realize. That’s a big one.

But also, if you look at Anthropic, they have a best practices guide — basically a list of rules. Tessl will tell you if your things compile. We also take Anthropic’s best practices and run that through LLM-as-judge. There’s a bit more you can do to tune the prompt for better results, but honestly just putting a prompt with Anthropic’s best practices in it is a great starting point. You get information on how specific your context is, whether it has a good concrete case for when it should be used. I’m sure folks have heard about skills and how they don’t activate very often — there are concrete things you can do without even running the skill to know how likely it is to trigger.

These things are cheap, they’re quick, you can put them in CI/CD, and it’s a surprisingly large lift to actually making your context useful. I recommend this as just table stakes. Everybody should have this, just like everybody should have a formatter and a linter. Bonus points: you can feed the focused output of this back into an agent and ask it to fix it. Pretty nice quick loop.

Evals: Is My Context Actually Helping?

Time: 00:10:57 - 00:17:38

OK, now slightly more complicated. A slightly more net-new concept. How do you write evals for your context?

Depending on whether you’re coming from more of a software background or more of an ML/deep learning background, this might either be obvious or not so obvious. The thing you’re trying to answer is: Is my context actually helping? And how well is the agent doing at the task that I’m trying to achieve?

If I use this as an example — we have some library that we want the agent to use, and we can see how it performs without any context. It’s not good at using the list function; maybe it implements it itself or uses a different library. It’s also bad at async handling, but it’s pretty good at correct stream combination and at doing zip files.

You want to understand this so that you can then understand where you need to apply context to fix the problem. There’s a couple things you might get from a view like this:

  • You might have written a bunch of context only to realize the agent did fine without it — why are you wasting tokens on it?
  • You might actually write something and realize it made performance worse because something’s gone out of date or it’s just added tokens for no reason.
  • In an ideal world, you see: “Ah, it works better with it and I’ve only applied tokens where it matters.”

All you have to do to get this set up is write some prompts — realistic tasks that you want the agent to do that require usage of the context you’ve created — and then write a scoring rubric for what a good solution to that problem looks like.

The reason I say write a scoring rubric and not “write a bunch of unit tests” is twofold. First, unit tests are really obnoxious to write and they take a long time, and you will quickly find that you just don’t do it if you have to create example projects and test suites for every single piece of context. More importantly, agents do unspeakable things to get unit tests to pass. Functional correctness is not the only thing that you’re measuring, especially for context. A lot of times you want to know: Was idiomatic code written? Did it use the library I actually wanted it to use instead of implementing its own solution? There’s really no way to measure this with unit tests. It’s much better to do more agentic review or LLM-as-judge.

What you want to do is define — we put them in markdown files. You want to have a prompt that runs through “build this thing, here are the requirements.” It should require using the context, or at least should require doing what the context says, because you actually want to measure it with and without context to see if the agent is just smart enough to do it on its own. Then importantly, you want to define some kind of grading rubric. You want to be pretty specific so that you get reliable results from an LLM — things like “the solution should use this exact API call somewhere in the method” or “it should initialize this before it initializes that.” Very granular things that can be checked at the end.

An important thing to note is that once you have these in place — this can take a bit of upfront work, it’s like the new source files that you have to care about as an agentic developer — but say you get about five of these per piece of context, that what we’ve found is a pretty reliable measure. Once you have some of these, then you can reap the benefits forever. Just like unit tests — every time you make a change, you rerun these, you see if it helped or hurt.

One thing that’s different is that oftentimes you’ll rerun these without changing the context, because there is something else that’s changing: the agent and the model. What we have found is that oftentimes you can start stripping out your context as agents get better. We had style guides for Python. Claude Opus 4.6 writes pretty damn good Python. It doesn’t need a style guide anymore. Your evals can tell you that and help you delete context that you no longer need. Save money, don’t pay the tokens.

Every once in a while there will be a regression. There was a recent Gemini that was kind of a smartass and thought it didn’t need to use tools and read context. And then we realized, oh, we’ve had a regression — we need to go beef up how much we tell the agent to use the context.

Repo evals — I talked about integration tests. It’s basically the same thing, but you don’t want to just test your context in isolation. You also want to measure realistic scenarios in your full coding environment with all your context installed. I was just watching a talk earlier today that described the “dumb zone” — where you’ve gotten too much context in your context window because of tools, because of context, files, and the agent is just persistently bad.

So you want to have a few coding scenarios — five for your repo is a fine place to start — that represent an average development task, with a rubric to grade the output. Run it every once in a while. See if your tech debt has gotten to a context where agents don’t understand how to work in your code. Have you installed too much context? Too many tools?

One thing we found that works pretty well is scan your previous commits and turn some of those into tasks. You can even, on a regular cadence, pick five random commits over the last month and refresh your eval suite. For folks in the ML world, you have things like input drift where you want to update your tasks every once in a while. Don’t worry about it if that seems like too much effort — just start with something and you can improve it over time. Same idea: task scenarios, grading rubrics, run them every once in a while, make sure you haven’t degraded things.

Observability: Mining Agent Logs

Time: 00:17:40 - 00:20:36

This one I think is pretty cool, but also kind of scary — you want something like analytics and observability. You’ve written this context, you validated the change before you’ve pushed it out to the repo for everyone. We do that in software, but then we also still have crash logs, we have metrics, we have usability funnels. This actually does exist for agents — just a lot of people aren’t paying attention to it.

All of the agents store all of their chat logs in files in accessible places. You can write your own scripts if you’d like. Tessl has capability to gather these — opt-in, of course, because obviously it’s very sensitive information. You can review those transcripts to see things like: Were tools called? How often was this piece of context used? How often does this pattern actually manifest in the code? How often does it import a library right in the middle of a function?

There’s a lot of rich information here that you could just write a quick script for, ask everyone on your team to run it once, aggregate a bunch of logs, and review common problems that you might want to make new context for. A great one is anytime the agent apologizes — just look for the word “sorry,” look for “you’re absolutely right.” All of these things are good signals. Like, “oh, maybe we should write something to fix that.” There’s a wealth of information and I guarantee you’ve got three or four months of Cursor logs sitting on all your devs’ machines that you could mine for “what should we be doing differently?”

How do you keep your context up to date? You can do something pretty simple here — set up something in your CI/CD. There are all kinds of agentic code review tools, Claude Code, Web. But I think a general thing to set up is: anytime a PR comes up, have something scan that PR and then look and say, “Is there any markdown file here that should be updated?” It’s not that hard. It really works better than you’d think. Because PRs tend to be so focused, agents are pretty good at finding out where they should update. If your PRs are too big — maybe it’s a good sign to make your PRs smaller again.

Tessl can automate a lot of this. “Oh, you added a new case to your logging levels here — update your documentation as well.”

This one is probably the most important because as your context gets out of date, it just destroys agent performance. So if you’re going to write context, you have to have a solution for keeping it up to date. Agents are pretty good at doing this, so you don’t have to do it by hand. Don’t do it by hand, because you won’t do it.

Package Managers: Reusing Context

Time: 00:20:37 - 00:22:36

Last thing: package managers. You need a package manager if you want to reuse context — code review skill, documentation on how to use React, best practices, et cetera. I won’t belabor this point. There’s lots of good options out there. Skills.sh is probably the most popular, though it pains me to say that. Tessl has a package manager as well. It’s not the most popular. I think it’s the best. I won’t pitch you on why it’s the best, but it’s the best.

Two things that are different that you should think about when figuring out how to use context:

First, unlike other package managers, a lot of context that you’re going to install is going to be describing other package managers. I have an example here where I have documentation on a library that’s part of PyPI, and it describes a particular package and a particular version. It’s a weird concept. So you want to think about: what is your strategy for matching? If you have documentation on a library, how do you make sure that as you update your library, you keep documentation keyed to the same version? You don’t want to say “I’m using Context7 on the latest of React” but actually you’re pinned to React 17 for some reason.

Second, think about how you keep your context in sync with dependencies, in sync with tools or APIs that you’re using. Because it’s a new source of drift that you might have to care about.

That’s it. That’s my walkthrough. A lot of this is not necessarily hard to do — it’s just fiddly to keep updated and keep pace with the rate of agent change. Happy to answer questions now or afterwards.

Q&A: The Future of Context

Time: 00:23:11 - 00:24:54

Audience: So what do you see as the end state — in 12 months or even 6 months? Claude 4.6 is really good, Codex 5.3 — and when Codex 6 comes out, Claude 5, Gemini 4… Do we need a lot of the scaffolding or does it go away?

Drew Knox: Fantastic question. First, it’s going to split a lot by whether you’re a greenfield or a brownfield. If you’ve built an app from the ground up for agents, it’s going to be a lot easier than if you’re doing an enterprise Java app.

I think the number of things you need context for will go down. The Python style guide example — all the rage six months ago, nobody needs it now. But describing your custom internal logging solution — you’re always going to have to document that because an agent doesn’t have access to it, it’s not in its training weights. There’s some amount of knowledge that will always need to be told to the agent.

My expectation is that eventually you won’t be proactively jamming almost any context into an agent’s window. You’ll have some kind of signposting, like progressive disclosure — the agent will get to look at it if it deems it necessary, like a normal developer. And then a lot of your usage of context will be applied at review time. You will create a review agent that looks for things like “did it break our style guide? Did it reimplement something?” It’ll be there for control, not to educate the agent up up front.

I think evals are going to play a big part in helping you navigate that change — knowing when it’s time to move things out of the context window into a review, or just delete it.

Q&A: Eval Scoring in Practice

Time: 00:24:54 - 00:26:24

Audience: I wanted to ask about evals. You had max score 50 and 30. From my experience, non-binary score doesn’t really work. Could you tell how it works and for what agents does it work?

Drew Knox: I think that’s right. Binary is pretty much the only — we give granularity in Tessl if you want to do more. But if you look at it, agents pretty much always score zero or max score. So I would say no, you could get away with 0 or 1 and it’d be about the same.

Audience: So I’m an AI engineer. I want to build solutions really fast. Would you recommend just using Opus 4.6 to get out an eval set very quickly and then just use that as a baseline — which is not perfect but just have that as a starting point? Or would you recommend doing a lean thorough analysis first?

Drew Knox: Personally, I’m busy, I have a lot to do — just start with the best agent. What I’d say more is: once you have some really repetitive tasks, it can be worth it to say “OK, what is the cheapest I can get away with?” A lot of times context will help you use smaller models to do that. But for day to day, your general driver — just always crank it to the max, unless you have some reason you can’t.

Q&A: The Role of the Technical Architect

Time: 00:26:24 - 00:29:22

Audience: My question is more towards non-technical people, or people who are not too technical. When do you think — or what barometer can we use to measure the point in time where we don’t really need to have too much into what the agents do or what the LLMs do? Like, you have spec-driven design, acting as a product manager, writing a PRD but not having too much importance on the technical side. Are we going to be at that soon?

Drew Knox: I’m going to throw out maybe a spicy take, which is: definitely we’re not there, and I don’t think we’ll ever be there.

What I mean by that is — we have effectively had general-purpose agentic development machines for 50 years. We just called them software engineers. And in that case, you never just wrote somebody a Slack message and expected them to go build an entire system completely unsupervised and just make all the best decisions.

Another way of putting this — my wife, who is a very senior staff engineer at Meta, says: “If you cloned me, I would still code review my code. I would never accept anyone to submit things without looking at it.”

So personally, I think there will always be a place for a technical architect, a steward, somebody who’s guiding the quality of the code base. I think what that role is will change over time.

Right now it’s a lot of in-the-weeds, very specific decisions. It’s a lot of reviewing code, mentoring and coaching people up, and you tend to have one PM to five to ten engineers. I imagine we’ll get to a place where you invert that ratio — you have one technical steward whose job is to think about the overall system design, to be constantly reviewing agent code, to be reviewing things that people are building and understanding “oh, this is a consistent failure point; if we abstract this part out, if we build a component that agents can use, they’ll more reliably get better one-shot success.” And then you have five to ten more product, design, product-engineering people who are out exploring the frontier of your product space, with this one technical steward helping them land their code and keep things maintainable and improving over time.

When will we get to that point? That part I’m less certain of. It could be in two weeks, it could be in two years. I think it’s probably in the order of passkey years. Certainly, I wouldn’t be surprised if a completely AI-native greenfield project starting within the next year could work in that model. But certainly for brownfield, I think it’ll be harder.


End of Transcript

Your codebase is NOT ready for AI (here’s how to fix it)

Source: YouTube - Matt Pocock Speaker: Matt Pocock (Total TypeScript, AI Hero newsletter) Duration: 8:48 Published: February 26, 2026 Channel: Matt Pocock / AI Hero


Description (from YouTube)

AI imposes super weird constraints on your codebase. And most codebases out there in the world, probably including yours, are not ready.

Your codebase, way more than the prompt that you used, way more than your agents.md file, is the biggest influence on AI’s output. And if it’s designed wrong, it can cost you in a bunch of different ways:

  • The AI doesn’t receive feedback fast enough
  • It doesn’t know if what it changed actually did what it intended
  • It finds it super hard to make sense of things and find files and work out how to test things
  • It can lead to cognitive burnout as you try to hold together AI and your codebase

The thesis: software quality matters more than ever. How easy your codebase is to change makes a huge impact on how AI then goes and changes it. The stuff that we’ve known about software best practices for 20 years still holds more true than ever.


Key Concepts & Best Practices

1. AI Has No Memory

“AI when it jumps into your codebase, it has no memory. It has not experienced your codebase before. It’s like the guy from Memento who just steps in and goes, ‘Okay, I’m here. Uh, what am I doing?’”

Implication: Your file system and codebase design must match your mental model because AI can’t hold that context.

2. Deep Modules (from “A Philosophy of Software Design”)

Definition: Lots of implementation controlled by a simple interface.

For AI Codebases:

  • Big chunks of modules with simple, controllable interfaces
  • All exports must come through the interface
  • Creates “seams” in the codebase where AI can take control

3. Graybox Modules

Concept: Deep modules where you don’t need to look inside.

How it works:

  • You design the interface (types, public API)
  • AI controls the implementation inside
  • Tests lock down the behavior
  • You only look inside when needed (taste, performance, debugging)

Benefits:

  1. Navigability - AI can see services in file system, understand types before implementation
  2. Progressive disclosure of complexity - Interface at top explains what module does
  3. Reduced cognitive burnout - Keep 7-8 “lumps” in your head instead of hundreds of tiny modules
  4. New-starter friendly - AI is like a new developer joining every day

4. File System = Mental Map

The Problem:

  • Developer has mental map: “thumbnail editor here, video editor here, auth here”
  • File system is jumbled: all modules mixed together
  • AI sees spaghetti, not structure

The Solution:

  • Match file system organization to mental model
  • Deep modules enforce boundaries
  • AI can find things easily based on structure

5. Avoid Shallow Modules

Anti-pattern: Many small modules with complex interrelationships

  • Hard to test
  • Hard to keep in your head
  • AI gets confused

Pattern: Fewer, deeper modules

  • Simple interfaces
  • Implementation delegated
  • Easier to reason about

6. Tests and Feedback Loops

Essential for AI:

  • AI needs fast feedback like a new starter
  • Well-tested codebase = AI can see what changes do
  • Plan modules, interfaces, and tests from the start (PRD stage)

Core Message

“Your codebase is probably not ready for AI because you’re not using enough deep modules and instead you’ve got a web of interconnected shallow modules which are really hard to navigate and really hard to test and really hard to keep in your head.”


Action Items for Making Codebase AI-Ready

Immediate:

  • Audit current module structure - are they deep or shallow?
  • Identify which modules could become graybox (interface + AI-managed implementation)
  • Check if file system matches your mental model

Ongoing:

  • When planning features (PRD stage), think about modules, interfaces, and tests
  • Design interfaces with AI navigability in mind
  • Add comprehensive tests to lock down module behavior
  • Keep boundaries clean - AI stays inside, you design interfaces

To Avoid:

  • Shallow modules with complex interrelationships
  • Jumbled file systems that don’t match mental models
  • Letting AI work without fast feedback loops (tests)

Connection to Other Sources

Complements Dru Knox’s “Context as Code”:

  • Knox: Context as code needs testing, validation, versioning
  • Pocock: Your codebase architecture IS the context - structure it for AI navigability

Overlap:

  • Both emphasize that AI is like a new developer (not superpowered, has constraints)
  • Both stress the importance of feedback loops and testing
  • Both reference established software engineering practices (20+ years old)

Unique from Pocock:

  • Deep modules as the architectural pattern for AI codebases
  • Graybox modules concept (human interface design + AI implementation)
  • Progressive disclosure of complexity
  • File system organization as critical for AI success

Source of Truth References

Core Principles

  1. AI = New Developer - No memory, needs guidance, gets confused by spaghetti
  2. Deep Modules - Simple interfaces hiding complex implementation
  3. Graybox Modules - You design interfaces, AI manages internals
  4. File System = Mental Map - Structure must be navigable without prior knowledge
  5. Tests are Essential - Fast feedback loops for AI to learn

Red Flags to Watch For

  • ⚠️ Shallow modules with complex interrelationships
  • ⚠️ File system doesn’t match mental model
  • ⚠️ AI getting lost in codebase (“what am I doing?”)
  • ⚠️ Cognitive burnout from managing too many tiny modules
  • ⚠️ AI making changes but can’t verify they worked (slow/no feedback)

When to Remind User

  • Request involves creating many small modules instead of fewer deep ones
  • Not thinking about interface design before implementation
  • Not mentioning tests or feedback mechanisms
  • Suggesting changes without considering file system organization
  • Letting AI work on code without clear boundaries (graybox concept)
  • Not planning from PRD/architecture level down

Full Transcript

0:00 | AI imposes super weird constraints on your codebase. And most codebases out there in the world, probably including yours, are not ready. Your codebase, way more than the prompt that you used, way more than your agents.mmd file, is the biggest influence on AI’s output. And if it’s designed wrong, it can cost you in a bunch of different ways. It can mean that the AI doesn’t receive feedback fast enough. So, it doesn’t know if what it changed actually did what it intended. It can find it super hard to make sense of things and find files and work out even how to test things. And finally, it can lead you into cognitive burnout as you try to hold together AI and your codebase and patch it all up and keep everything in your mind. And my thesis here is that software quality matters more than ever. In other words, how easy your codebase is to change makes a huge impact on how AI then goes and changes it. And the stuff that we’ve known about software best practices for 20 years still holds more true than ever. And if you’re interested in getting better at this stuff, then check out my newsletter, AI Hero. I teach you all about AI coding, but this is not for vibe coders. This is for real engineers solving real problems. And if that’s you, and you’re not sure how to handle these new tools, then you are going to love it. Now, let’s imagine that this here is our codebase. Each one of these little squares represents a module. And this module might export some functionality. It might export a function, might export some variables, might export a component if it’s like a, you know, a React or a front end thing. I want you to imagine that this is the image of your codebase that you hold in your head. Now you might inside here have some vague groupings of different functionality. For instance, here you might have let’s say a thumbnail editor feature and all of these different modules contribute to that. Over here you might have a little video editor feature or something. Down here is all the code related to authentication. Up here is a bunch of CRUD forms for updating stuff maybe in a CMS. And over here are a couple of example features that I can’t be bothered to think of examples for. Now, this map that I’ve created here of all of the located modules in this particular codebase, they’re not actually reflected that much in the file system. They’re all really jumbled up together. If I want to just grab, let’s say, an export from this module and import it down into this module, I can. There’s nothing stopping me. And so, what you might end up with is a bunch of kind of disparate relationships between stuff that doesn’t actually relate to each other. Now, you as the developer understand the mental map between all of these modules, but what the AI sees when it first goes into your codebase is this. It doesn’t see all of the natural groupings and all the natural relationships. What it sees is a bunch of disparate modules that can all import from each other. That’s because AI when it jumps into your codebase, it has no memory. It has not experienced your codebase before. It’s like the guy from Memento who just steps in and goes, “Okay, I’m here. Uh, what am I doing?” So, my first assertion here is that you need to make sure that the file system and the design of your codebase matches this internal map that you have of it. This is because if you describe something over in the video editor section and you use it via a prompt, then you want the AI to be able to find it easily. The AI won’t go in knowing every single function, every single module and what they supposed to do and how they link to each other. And the best way I have found to do that is with deep modules. Now, deep modules comes from this book here, which is a philosophy of software design. And the idea is that in order to make your system easily navigable and easy to change and also easy to test is that you have a deep module so lots of implementation controlled by a simple interface. What that looks like in terms of our graph is instead of many many small modules you end up with these big chunks of modules with simple controllable interfaces and this means that any exports from these modules have to come from that interface. Now when I read that about deep modules, I immediately thought about putting AI in control of these modules because this is an opportunity to introduce a kind of seam into the codebase. I don’t really care about what’s happening inside here which is the implementation. I just care about what’s happening in the interface because the interface which is you know the publicly accessible API of this module I can carefully control and I can apply my taste to and design and then the stuff inside here I can just delegate to an AI to control and I can write tests that completely lock down the module in terms of its behavior. So these are not just deep modules with simple interfaces, they’re also graybox modules. In other words, I don’t actually need to look inside these modules. I can if I want to, if I want to influence their outcome or if I need to apply some taste to the implementation or I need to improve their performance or something, but as long as the tests are good, then I don’t really need to care about what happens inside. Now, this has three massive benefits. The first one is that I can make my codebase way more navigable. Let’s for the sake of argument just call each of these services, right? The video editor service, the thumbnail service, whatever. If I document these each inside their own folder and I have the publicly accessible interface kind of like uh really obvious in a type section, then the AI when it’s exploring my codebase, it can see all of these different services on the file system. It can read and understand the types that they export before it actually looks at the implementation. And then it can say, okay, I’ve seen the interface. I understand what this does. I don’t need to look inside because I can just trust what it’s returning. In other words, we’ve designed our codebase for progressive disclosure of complexity. The interface sits at the top and it just explains what the module does and then when we need to we can look inside the module and make changes to it or look at it to understand its behavior more deeply. The second one is that we reduce the cognitive burnout of managing this codebase. Now as a user I can just go right I need something from uh I don’t know this madeup feature or let’s say the authentication bit over here. Let’s say what let’s see what the public interface is. Let’s just grab that and use it. And instead of needing to think about the inter relationships between all of these modules, I can just keep kind of like seven or eight lumps of stuff in my head and go, okay, the AI manages the stuff inside that. I only need to worry about designing the interfaces and how they fit together. Now, this of course is still a million miles away from vibe coding because you need to apply taste at the boundaries of these modules. You need to be really good at deciding, okay, what goes into that module, what goes into that module. And what you really want to avoid are lots of little shallow modules, which is kind of what we had up here, right? Each of these modules is just like, sure, it’s kind of interrelated and grouped together, but really they’re lots of tiny shallow modules which are testable in these tiny units which are really hard to keep all in your head. And so by simplifying the mental map of the codebase, we reduce cognitive burnout that comes from managing this codebase. And again, this is nothing new. This is a 20-year-old software practice. And the third one here, I mean, I’m really just repeating myself, but this is what we’ve been doing all along. This is how good codebases have supposed to have been designed. So, what works here for humans is also great for AI. We need to stop thinking about AI as like this superpowered developer as like, you know, it’s going to reach AGI and understand that it’s got some weird limitations. And the limitations that it has are that it’s a new starter in your codebase. So you need to make your codebase friendly and ready for new starters because you’re going to be spawning like 20 new starters every day or probably more just to look at your codebase and make changes. So that means the map of your codebase needs to be easily navigable and it needs to be enforced by using these modules. Now some languages make this easier than others. For instance, in Typescript and JavaScript, it’s actually not that easy to make these services make these modules uh sort of boundaried in this way. I want to give a quick shout out to effect because uh I posted a video on effect a few months ago. I’m actually using effect way more than I did back then and it makes this kind of um sort of seeming modularizing of your codebase really simple. The final thing I want to say here is that you need to be thinking about these modules and how you’re affecting them and how you’re designing the interfaces in every coding session that you do. That means right from the early planning stage when you’re writing your PRDs or when you’re turning your PRDs into implementation issues, you need to be thinking about the modules you’re affecting and the interfaces and how you going to test them because tests and feedback loops are essential for an AI because of course they’re essential for a new starter joining the codebase. If you want the new starter to contribute effectively, you need a well-tested codebase so they can see what their changes do as they ripple out. So that’s my rant for today. your codebase is probably not ready for AI because you’re not using enough deep modules and instead you’ve got a web of interconnected kind of shallow modules like this which are really hard to navigate and really hard to test and really hard to keep in your head. Now, if you dig this then of course you will dig my newsletter where we go more deeply into topics like this. Thanks for watching folks. What else do you think goes into making a great codebase for AI? I really love this metaphor for deep modules but I know it’s not the only one going. There are plenty out there. Thanks for watching and I will see you very soon. So, when you’re thinking about your codebase with AI, what are you thinking about? What kind of 20-year-old books do you want to recommend? Leave it in the comments. It’s the easiest way to keep up with all of my stuff and the link is below.


End of Transcript

Source: YouTube video transcript extracted via youtube-transcript-api

A Philosophy of Software Design

Author: John Ousterhout Published: 2018 Website: https://web.stanford.edu/~ouster/cgi-bin/book.php


Why This Book Matters for AI-Ready Codebases

Matt Pocock cited this book as the primary inspiration for his “Your codebase is NOT ready for AI” video. The concepts in this book directly address why AI struggles with traditional software architecture and provide the blueprint for making codebases AI-friendly.


Core Concepts

1. Deep Modules

Definition: A module with a simple interface that hides complex implementation.

“The best modules are deep: they have a lot of functionality hidden behind a simple interface.”

For AI:

  • AI sees simple interface first (WakeWordDetector)
  • Complex implementation hidden (imp/ subdirectory)
  • AI doesn’t get overwhelmed by details
  • Can safely modify internals without breaking interface

Example:

#![allow(unused)]
fn main() {
// Deep module - simple interface
pub struct WakeWordDetector {
    inner: imp::Inner,  // Complex implementation hidden
}

impl WakeWordDetector {
    pub fn new(config: &Config) -> Result<Self>;  // Simple
    pub fn predict(&mut self, audio: &[i16]) -> Result<bool>;  // Simple
    pub fn reset(&mut self);  // Simple
}
}

Anti-example (Shallow Module):

#![allow(unused)]
fn main() {
// Shallow module - exposes everything
pub mod detector;  // AI sees this, confused
pub mod buffer;  // AI sees this, confused
pub mod resampler;  // AI sees this, confused
}

2. Information Hiding

Principle: Hide design decisions that are likely to change.

For AI:

  • Implementation details in imp/ subdirectories
  • Public API documented with ## AI Context
  • Tests lock down behavior so AI can refactor safely
  • Progressive disclosure: start simple, dive deep when needed

3. Strategic vs Tactical Programming

Tactical Programming:

  • “Just make it work”
  • Accumulates technical debt
  • Creates shallow modules over time
  • AI gets confused by tangled code

Strategic Programming:

  • Invest time in good design
  • Create deep modules from start
  • Clean interfaces that last
  • AI navigates easily, works efficiently

For AI Codebases:

  • Strategic programming is essential
  • AI operates best with clear structure
  • Deep modules = AI can work autonomously
  • Shallow modules = need constant hand-holding

4. Reducing Complexity

Complexity: Anything that makes software hard to understand or modify.

Symptoms AI Struggles With:

  • Change amplification (change one place, break many)
  • Cognitive load (too many things to keep in mind)
  • Unknown unknowns (hidden dependencies)

Deep Modules Solution:

  • Narrow interfaces reduce cognitive load
  • Hidden implementation reduces change amplification
  • Clear boundaries reveal dependencies

Key Quotes

On Deep Modules

“A module is deep if its interface is much simpler than its implementation.”

“The benefit of deep modules is that they hide complexity.”

On Interfaces

“The interface to a module should be simpler than its implementation.”

“The best modules are those that provide powerful functionality yet have simple interfaces.”

On Complexity

“Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.”

“Complexity comes from the accumulation of dependencies and obscurities.”


Application to AI-Ready Codebases

Before (Shallow Modules - AI Struggles)

src/
├── detector.rs      # 200 lines, 15 public functions
├── buffer.rs        # 150 lines, 12 public functions
├── resampler.rs     # 180 lines, 8 public functions
└── mod.rs           # Just re-exports them all

AI sees: 35 public items to understand, complex interdependencies, no clear entry point.

Result: AI wastes tokens figuring out what to use, makes mistakes, needs guidance.


After (Deep Modules - AI Thrives)

src/wakeword/
├── mod.rs           # 50 lines, simple interface
└── imp/
    └── mod.rs       # 480 lines, hidden implementation

AI sees: 3 public methods, clear purpose, obvious how to use it.

Result: AI works autonomously, tests ensure correctness, documentation guides usage.


Why This Book is Essential for AI Development

1. Progressive Disclosure

Concept: Show information in order of importance.

Book: “The interface provides the information users need, the implementation provides the functionality.”

For AI:

  • mod.rs shows what the module does (interface)
  • imp/ shows how it does it (implementation)
  • AI reads interface first, only dives deep when needed

2. Working Code ≠ Good Design

Book: “Working code isn’t enough. It must also be well-designed.”

For AI:

  • Shallow modules “work” but confuse AI
  • Deep modules work AND let AI work autonomously
  • Technical debt hurts AI more than humans (no institutional memory)

3. Simplicity Requires Effort

Book: “It is more important for a module to have a simple interface than a simple implementation.”

For AI:

  • Invest time designing simple interfaces
  • Hides complexity from AI
  • Reduces cognitive load
  • Makes codebase navigable

The “Tcl” Lesson

From the book: Ousterhout created Tcl (Tool Command Language), which was designed around deep modules.

Key insight: Tcl’s simple interface (everything is a string) hid enormous complexity underneath. This made it incredibly powerful yet easy to use.

For AI Codebases:

  • Design interfaces as if for a scripting language
  • Simple inputs/outputs
  • Hide the machinery
  • Let AI compose simple pieces into complex behavior

Implementation Checklist

Use this to verify your codebase follows the book’s principles:

Deep Module Check

  • Interface has <10 public items (ideally <5)
  • Implementation hidden in private modules/files
  • Interface is simpler than implementation
  • Tests validate the contract, not internals

Information Hiding Check

  • Design decisions likely to change are hidden
  • Public API documented with examples
  • AI Context sections explain usage patterns
  • No implementation details leak through interface

Complexity Reduction Check

  • File organization matches mental model
  • Dependencies flow in one direction
  • Clear entry points for every module
  • No “where is X implemented?” confusion

Videos

  • Matt Pocock: “Your codebase is NOT ready for AI” (https://www.youtube.com/watch?v=uC44zFz7JSM)

    • Applies deep modules to AI-ready codebases
  • John Ousterhout: Lectures on software design

    • https://web.stanford.edu/~ouster/cgi-bin/lectures.php

Books

  • This Book: “A Philosophy of Software Design” (2018)

    • Available as PDF on author’s website
  • Related: “Clean Architecture” by Robert C. Martin

    • Also emphasizes interface/implementation separation

Academic Papers

  • Original Deep Modules Concept: Various papers by Ousterhout on system design
  • Complexity in Software: Ousterhout’s research on what makes code hard to understand

Summary

The Book’s Thesis:

“Good software design is not about writing clever code. It’s about hiding complexity behind simple interfaces.”

For AI Development:

“AI-ready codebases use deep modules religiously. Every module is a simple interface hiding complex implementation. AI sees the interface, understands instantly, and works safely.”

Your Action Item:

  1. Read Chapter 2 (“The Nature of Complexity”)
  2. Read Chapter 4 (“Modules Should Be Deep”)
  3. Audit your codebase for shallow modules
  4. Convert them to deep modules
  5. Watch AI productivity soar

Connection to Botface

We’ve applied this book’s principles throughout the Botface codebase:

ModuleInterface (Public)Implementation (Private)Deep?
WakewordWakeWordDetector (3 methods)imp/mod.rs (156 lines)✅ Deep
AudioAudioCapture (3 methods)imp/mod.rs (169 lines)✅ Deep
LLMLlmClient (4 methods)imp/mod.rs (100 lines)✅ Deep
TTSTtsEngine (5 methods)imp/mod.rs (90 lines)✅ Deep
STTSttEngine (5 methods)imp/mod.rs (85 lines)✅ Deep
SoundsSoundPlayer (5 methods)mod.rs (95 lines)✅ Deep

Result: Pocock Score 10/10, AI navigates instantly, 76 tests lock behavior.


“The best software is not the software that does the most things. It’s the software that does the right things with the least complexity.” — John Ousterhout


Source: https://web.stanford.edu/~ouster/cgi-bin/book.php PDF Download: Available free from author’s website

Graybox Conversion Session Summary

Session Date: March 11, 2026 Modules Converted: 2 (wakeword, audio) Tests Added: 13 (7 wakeword + 6 audio) All Tests: ✅ PASSING Pocock Score: 5/10 → 7/10 ⬆️


Modules Converted

1. ✅ Wakeword Module

Status: Complete Files Changed: 5 Tests Added: 7

Changes:

  • Created src/wakeword/imp/ directory
  • Moved detector.rsimp/mod.rs
  • Deleted empty buffer.rs
  • Rewrote mod.rs with graybox interface
  • Updated state_machine.rs import
  • Created comprehensive tests

Result: Simple WakeWordDetector interface hiding complex ONNX/resampling logic


2. ✅ Audio Module

Status: Complete Files Changed: 6 Tests Added: 6

Changes:

  • Created src/audio/imp/ directory
  • Moved capture.rsimp/mod.rs
  • Deleted empty playback.rs and resample.rs
  • Rewrote mod.rs with graybox interface
  • Updated state_machine.rs import
  • Created comprehensive tests

Result: Simple AudioCapture interface hiding ALSA subprocess complexity


New Structure

src/
├── wakeword/
│   ├── mod.rs          # Public interface (simple API)
│   └── imp/
│       └── mod.rs      # Hidden implementation
├── audio/
│   ├── mod.rs          # Public interface (simple API)
│   └── imp/
│       └── mod.rs      # Hidden implementation
└── ...

tests/
├── architecture_test.rs    # 8 tests (validates structure)
├── wakeword_tests.rs       # 7 tests (behavior locked)
└── audio_tests.rs          # 6 tests (behavior locked)

Total Tests: 21 (8 + 7 + 6) All Passing: ✅ YES


Test Results

$ cargo test

architecture_test: 8 passed ✅
wakeword_tests:    7 passed ✅
audio_tests:       6 passed ✅
doc-tests:         0 passed, 2 ignored ✅

Total: 21 tests passed, 0 failed

Documentation Created

  1. docs/graybox-conversion-wakeword.md - Detailed guide for wakeword conversion
  2. docs/graybox-conversion-roadmap.md - Master plan with:
    • All modules listed by priority
    • Detailed conversion steps for each
    • Progress tracker (2/6 done = 33%)
    • Recovery instructions if session crashes
    • Git commit strategy

Key Improvements

Before (Shallow Modules)

#![allow(unused)]
fn main() {
// AI sees this:
pub mod capture;     // Hmm, what's this?
pub mod playback;    // Empty file, confusing
pub mod resample;    // Do I need this?
}

Problems:

  • AI has to read multiple files
  • Empty files add confusion
  • Implementation details exposed
  • No clear entry point

After (Graybox Modules)

#![allow(unused)]
fn main() {
// AI sees this:
//! Audio capture subsystem
//!
//! ## Usage
//! use botface::audio::AudioCapture;
//! let capture = AudioCapture::new(device, rate, channels);
//! let (rx, handle) = capture.start_continuous(80);

mod imp;
pub use imp::AudioCapture;  // Single, clear entry point
}

Benefits:

  • AI reads 1 file, understands interface
  • Implementation hidden unless needed
  • Clear entry point (AudioCapture)
  • Tests lock behavior for safe refactoring

Pattern Established

Template for remaining modules:

  1. Read current structure
  2. Create imp/ directory
  3. Move implementation files
  4. Write mod.rs with:
    • //! module docs
    • ## AI Context section
    • Usage examples
    • Common tasks
    • Graybox note
  5. Update imports
  6. Add tests
  7. Verify all tests pass

Next Steps (If Continuing)

Priority Order:

  1. LLM Module (45 min est.)

    • 3 exposed submodules: memory, ollama, search
    • Target: Single LlmClient interface
  2. TTS Module (30 min est.)

    • 1 exposed submodule: piper
    • Target: TtsEngine interface
  3. STT Module (20 min est.)

    • 1 exposed submodule: whisper
    • Target: SttEngine interface

Expected Final Score: 9/10 (after all 6 modules)


Files Changed in This Session

Created:

  • docs/graybox-conversion-wakeword.md
  • docs/graybox-conversion-roadmap.md
  • src/wakeword/imp/mod.rs
  • src/audio/imp/mod.rs
  • tests/wakeword_tests.rs
  • tests/audio_tests.rs

Modified:

  • src/wakeword/mod.rs (complete rewrite)
  • src/audio/mod.rs (complete rewrite)
  • src/state_machine.rs (2 import updates)

Deleted:

  • src/wakeword/buffer.rs (empty)
  • src/audio/playback.rs (empty)
  • src/audio/resample.rs (empty)

Recovery Info

If session crashes:

  1. git status - see what’s uncommitted
  2. git log -1 - see last commit
  3. Check docs/graybox-conversion-roadmap.md for current status
  4. Resume from: LLM module (item 3 in roadmap)

Current Git Status: Uncommitted changes present (docs + converted modules)

Recommendation: Commit now before continuing

git add -A
git commit -m "Convert wakeword and audio to graybox: simple interfaces, hidden impl, add tests"

Impact Summary

MetricBeforeAfterChange
Pocock Score5/107/10+2 ⬆️
Shallow Modules64-2 ✅
Graybox Modules1 (gpio)3+2 ✅
Tests821+13 ✅
Module Conversion0%33%+33% ✅

Status: ✅ 2/6 modules complete, ready to continue with LLM module All Tests: ✅ PASSING Documentation: ✅ Complete with recovery instructions


Session ended with clean working state. All changes documented.

LLM Judge Report

Observability Report

Generated: Thu Mar 12 22:54:26 EDT 2026

Summary

  • Files analyzed: 0
  • ‘Sorry’ signals: 0
  • Apology signals: 0
  • Correction signals: 0
  • Uncertainty signals: 0
  • Import pattern signals: 0

What These Signals Mean (Dru Knox)

🔴 High Priority

  • “sorry” / apologies: Agent is struggling. Context is unclear or incomplete.
  • “you’re absolutely right”: Agent made mistakes. Rubrics need to be more specific/binary.

🟡 Medium Priority

  • “let me check” / uncertainty: Agent is guessing. Context needs more examples.
  • Imports mid-function: Agent didn’t see the interface first. Check module organization.

Detailed Findings

📍 .logs/README.md: 1 ‘sorry’ occurrences 📍 .logs/README.md: 1 correction signals

Recommendations

Next Steps

  1. Review the detailed findings above
  2. Update context files based on patterns identified
  3. Re-run evals to measure improvement: just evals
  4. Re-run this report after changes: just mine-logs