A desktop robot that listens for a wakeword, transcribes your speech, and responds through GIFs, short video clips, servo movements, and LED animations. Powered by an LLM agent that picks media and text in real time.
Peeqo Agent is a fork of @shekit's original Peeqo app code with a lot of updates to replace defunct APIs and now support LLMs. It is an active work in progress, hop in the Discord if you have questions or want to contribute!
Major changes from the original code:
- Wakeword support for "Peeqo" is included in the repo and uses OpenWakeWord
- Google's speech-to-text + LLM response replaces the old, strict conditional intent flow
- "Intents" have now become "tools" that the LLM can choose to use
Target hardware: Raspberry Pi 3B running Pi OS Bullseye (11), with a Seeed 2-mic voicecard (WM8960).
Dev machines: Mac or Linux.
- Wakeword detected (openWakeWord / Python subprocess) → alert sound + servo wiggle
- Google Cloud Speech-to-Text transcribes your speech via a streaming gRPC connection
- Your message is transcribed and passed to a simple agent loop which includes extra context for the LLM to decide what to do
- An LLM agent (Claude via Anthropic API or OpenRouter) decides how to respond using tools:
findRemoteGif— searches Giphy and displays an MP4-backed GIFfindRemoteVideo— searches YouTube and streams a short clip (or full music video)showWebPage— displays a web page full-screen, e.g. Google Image search, Fast.com speed test (beta)getWeather— fetches current conditions via OpenWeathersetTimer— countdown timer with GIF responsechangeGlasses— cycles Peeqo's glasses
- A rolling history of your conversation is maintained so you can follow up on requests or expand on things
Notes:
- Media plays on screen; mic is paused during playback and re-armed afterward
- Long videos (music, background play) are interruptible — say the wakeword to pause, speak a command or stay silent to resume
- The "1" button on top of Peeqo can be used to reset at any time, including to close media
- Node 20 via nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash . "$HOME/.nvm/nvm.sh" nvm install 20
brew install sox # mic recording + audio resampling
brew install yt-dlp # YouTube URL resolutionYou will need to be running Raspberry Pi OS Bullseye (v11), and install the following packages:
sudo apt install -y libopenblas-dev fonts-noto sox # sox needed for WM8960 16kHz resampling
pip3 install yt-dlp # YouTube URL resolutionPython wakeword dependencies:
# armv7l (32-bit) only — no official PyPI wheel for this platform:
pip3 install "https://github.com/nknytk/built-onnxruntime-for-raspberrypi-linux/raw/master/wheels/bullseye/onnxruntime-1.16.0-cp39-cp39-linux_armv7l.whl"
# All platforms:
pip3 install -r python/requirements.txtDownload openWakeWord built-in models (one-time, required even when using a custom model):
python3 -c "from openwakeword.utils import download_models; download_models()"If you built Peeqo using the original assembly guide, your Pi is likely running Raspbian Buster (or older) with Node 8–10 and the old Dialogflow + Snowboy stack. Here's how to get to the right baseline before following the setup steps above.
The target OS is Pi OS Bullseye (11), which ships Python 3.9 (required for the armv7l onnxruntime wheel).
Run apt update && apt full-upgrade on the current version, update /etc/apt/sources.list to the next release codename, then apt update && apt dist-upgrade. Repeat for each version. You may still run into dependency issues that you'll need to resolve, but I managed with the help of Claude.
A clean flash is the probably the most reliable path:
- Download Raspberry Pi Imager
- Choose Raspberry Pi OS (Legacy, 32-bit) — select the "Bullseye" release
- Flash to SD card; boot and configure WiFi, SSH, etc.
Bookworm (12) is not yet supported on 32-bit Pi 3B — its default Python 3.11 is incompatible with the onnxruntime 1.16 armv7l wheel. Bullseye is the tested baseline.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20Add nvm use 20 to your ~/.bashrc so it persists across reboots.
The WM8960 driver needs to match your new kernel. Follow the seeed-voicecard instructions — the original Respeaker repo is unmaintained; use the HinTak fork which tracks current Pi kernels.
Once you're on Bullseye + Node 20, follow the standard prerequisite steps from the top of this README.
Start by copying the example config:
cp electron/app/config/config.example.js electron/app/config/config.jsThen fill in your keys in config.js. This file is gitignored — your keys won't be committed.
| Key | Where to get it | Notes |
|---|---|---|
anthropic.apiKey or openrouter.apiKey |
console.anthropic.com or openrouter.ai/keys | Anthropic is preferred (faster, caching, streaming) |
| Google Cloud service account JSON | console.cloud.google.com | Enables Speech-to-Text and YouTube search; save as electron/app/config/dialogflow.json |
giphy.key |
developers.giphy.com | Free tier is fine |
| Key | Where to get it | Enables |
|---|---|---|
openweather.key |
openweathermap.org/api | Weather skill |
Set one of anthropic.apiKey or openrouter.apiKey in config.js. If both are set, Anthropic is used (it's faster — direct API, prompt caching, streaming tool dispatch). OpenRouter lets you swap to any supported model.
anthropic: {
apiKey: "sk-ant-...", // preferred: faster, supports caching + streaming
model: "claude-haiku-4-5-20251001",
},
openrouter: {
apiKey: "sk-or-v1-...", // fallback; set apiKey to "" to use Anthropic
model: "anthropic/claude-sonnet-4-5",
},Place your service account JSON file at electron/app/config/dialogflow.json. The same key is used for both Speech-to-Text and the YouTube Data API — no separate credentials needed.
cd electron
npm install
npm start # normal mode
npm run debug # with DevTools forced open (useful on Pi)
npm run rebuild # rebuild native modules after Electron version changeOn Mac/non-ARM Linux, DevTools always open. On Pi, only when NODE_ENV=debug.
No wakeword hardware? Pass OS=unsupported to show a clickable wakeword button instead:
OS=unsupported npm start# Sync project files (rsync, not scp — avoids nested duplicate directories)
rsync -av --exclude='node_modules' /path/to/peeqo-personal/electron/ pi@<ip>:~/peeqo/electron/
# Rebuild native modules on the Pi after syncing
cd ~/peeqo/electron && npm run rebuildNever run
npm install <pkg>directly. Add packages toelectron/package.jsonby hand and letnpm installpick them up after sync.
sudo raspi-config # → System Options → Wireless LAN| Key | Description |
|---|---|
anthropic.apiKey / anthropic.model |
Direct Anthropic API (preferred) |
openrouter.apiKey / openrouter.model |
OpenRouter fallback |
speech.dialogflowKey |
Filename of Google service account JSON in app/config/ |
speech.wakewordModel |
.onnx model file in app/config/ — null for openWakeWord built-ins |
speech.wakewordThreshold |
Detection sensitivity (default 0.65; raise to reduce false positives) |
speech.audioStartDelayMs |
ms from wakeword to STT audio start (default 50) |
youtube.maxVideoDuration |
Default max clip duration in seconds (default 10) |
giphy.key |
Giphy API key |
openweather.key / openweather.city |
OpenWeather API key and default city |
- Add a tool definition to
electron/app/js/intent-engines/claude.js(TOOLS_ANTHROPIC) — this is what the LLM calls - Add a handler function exported from
electron/app/js/intent-engines/skills.js— must match the tool name exactly - Optionally add a response definition to
electron/app/js/responses/responses.jsif the skill needs a canned local response - Add local media to
electron/app/media/responses/<skillName>/if usingtype: 'local'
- Discord: bit.ly/2HLtxez
- Assembly instructions: wiki