2026-07-26 19:10:14 -06:00
...
2026-07-23 07:55:43 -06:00
2026-07-26 19:10:14 -06:00
2026-07-23 06:25:41 -06:00
...
2026-07-23 07:55:43 -06:00
...
2026-07-23 07:55:43 -06:00
2026-07-23 06:25:41 -06:00
...
2026-07-23 07:55:43 -06:00
2026-07-23 06:25:41 -06:00
2026-07-23 06:25:41 -06:00
2026-07-23 06:25:41 -06:00
2026-07-23 06:25:41 -06:00
2026-07-23 06:25:41 -06:00

Bolt Desktop Pet

A little animated pet that lives on your desktop and is just a face on top of your Bolt server — same brain, memory, tools, and persona as Discord chat and the Linux desk client. It talks to ai/desk_api.py on the server exactly the way desk_client/bolt_desk.py does; this project only adds the on-screen pet and swaps Deepgram/ElevenLabs playback to be cross-platform (no mpv/ffplay/espeak-ng subprocess calls — pure sounddevice).

mic → wake-phrase spotter ("thunderbolt") / hotkey / click → record utterance
    → Deepgram STT (+ the focused window's title, for "what's this error?")
    → POST /desk/converse on your Bolt server → [server may relay a shell
      command back to run on THIS machine, or a `petctl` command that moves
      or emotes the pet] → reply → ElevenLabs streaming TTS → speakers
    → shown in a speech bubble + the pet's sprite state (idle/listening/
      thinking/talking) updates the whole time

Nothing is sent to the server until the wake phrase fires, you press the push-to-talk hotkey, or you click the pet — plus, if you turn them on, the heartbeat and the desktop-notification bridge.

Why a separate project instead of living in the tmn-api repo

This runs on your desktop machine, not the server — same relationship as desk_client/ (Linux) or the Android app, both of which are just clients of the desk API over HTTP. It has no import dependency on the server repo at all, so it can be copied anywhere and configured with its own .env.

Setup

  1. Copy this whole bolt-pet/ folder to the machine you want the pet to run on (if that isn't already this machine).

  2. cp .env.example .env and fill in:

    • BOLT_SERVER_URL + DESK_API_KEY — same as desk_client/.env on the server side. Use the server's master DESK_API_KEY, or mint yourself a personal one via the desk-only api_key_generate marker (see the main repo's CLAUDE.md → "Per-user API keys").
    • DEEPGRAM_API_KEY for STT.
    • ELEVENLABS_API_KEY + ELEVENLABS_VOICE_ID for TTS (optional — falls back to offline TTS via pyttsx3 if omitted or if a request fails).
  3. Run it:

    • macOS/Linux: ./run.sh
    • Windows: run.bat

    Both scripts create a local .venv and install requirements.txt on first run. On Linux you'll also need system packages for audio: sudo apt install libportaudio2 espeak-ng.

The pet appears near the bottom-right of your screen. It wanders off on its own now and then; drag it anywhere and it tucks itself flush against a nearby screen edge. That position isn't saved across restarts (see Known limitations).

Talking to it

  • Say "thunderbolt" — detected fully on-device by a custom-trained openWakeWord model (thunderbolt.onnx, ships in the project root), the same way the server repo's desk_client/bolt_desk.py detects "hey bolt" with bolt.onnx. Matches the DEFAULT_WAKE_WORD already used for Bolt's Discord voice channels, so it's the same word everywhere.
  • Or press Ctrl+Alt+Space (PUSH_TO_TALK_HOTKEY) from anywhere — useful in a noisy room where the wake word misfires. Needs pynput and a session that allows global key hooks; most Wayland sessions don't, in which case it logs why at startup and everything else still works.
  • Or just click the pet once (a drag doesn't count as a click).
  • Talk over it to cut a long answer short — the mic stays live while it speaks, and barging in starts your next turn immediately (BARGE_IN).
  • Right-click the tray icon for Talk now, Mute mic, Nap, Wander around, Click through the pet, History…, Wake word tuning… and Quit — the pet window itself has no title bar or taskbar entry.
  • Click the speech bubble to copy what it just said; the tray's History… window keeps the last HISTORY_LIMIT turns.

What it does on its own

  • Wanders the desktop while idle (PET_WANDER), stands still while listening/thinking/talking or while a bubble is up.
  • Moves and emotes on command. Bolt can relay petctl move top-left, petctl emote wave|hop|spin|nod|shake, petctl say ..., petctl wander on|off, petctl nap on|off. These are intercepted here and never reach a shell.
  • Naps during QUIET_HOURS (e.g. 23:00-08:00) or while a fullscreen app is focused (DND_ON_FULLSCREEN) — it dims, stops wandering, and makes no proactive noise. It still answers when you speak to it.
  • Reacts to desktop notifications if you turn on NOTIFICATION_BRIDGE (Linux/D-Bus) and set a NOTIFICATION_FILTER regex — matching notifications get forwarded to the server, so it can tell you the deploy went green. Off by default: each one costs a round trip.

Wake-word detection

bolt_pet/audio/wake_word.py feeds every mic frame into thunderbolt.onnx via the openWakeWord runtime (ONNX inference) and treats any class score at or above WAKE_WORD_THRESHOLD (default 0.5) as a detection — the exact same per-frame predict()/reset() pattern as desk_client/bolt_desk.py's main loop. Point WAKE_MODEL_FILE in .env at a different .onnx model to change the wake phrase later without touching any other code.

If it keeps ignoring you (or firing at the TV), open Wake word tuning… from the tray: it shows the peak score while you talk and a rolling list of near misses — frames that scored just under the threshold — and the slider takes effect immediately, mid-listen. Set the threshold just below the peak you can hit reliably, then write it into .env as WAKE_WORD_THRESHOLD.

Project layout

bolt_pet/
  config.py          .env loading (same pattern as desk_client/bolt_desk.py)
  state.py            PetState enum + a small transition-checked state machine
  server_client.py    /desk/converse, /desk/tool_result, /desk/report_status
  controller.py        the pipeline: wake word -> STT -> server -> TTS, on a QThread
  speech_text.py        strips markdown/emoji/URLs so the voice never says "asterisk"
  pet_actions.py         petctl move/emote/say/wander/nap parsing
  screen_context.py       active-window title + fullscreen detection
  quiet.py                 quiet-hours schedule
  notifications.py          desktop notification bridge (Linux/D-Bus)
  history.py                 rolling conversation transcript
  hotkey.py                   global push-to-talk (pynput, optional)
  audio/
    mic.py             input stream + energy-based VAD utterance capture
    wake_word.py        openWakeWord thunderbolt.onnx detection (see above)
    stt.py               Deepgram
    tts.py                ElevenLabs streaming PCM, offline pyttsx3 fallback
    barge_in.py            "you started talking" detector, to cut playback short
  ui/
    app.py              wires QApplication + window + tray + controller thread together
    pet_window.py         frameless/translucent/always-on-top sprite window + speech bubble
    sprite.py              frame animation loader (see assets/sprites/README.md)
    tray.py                 system tray menu
    history_window.py        conversation scrollback (copyable)
    wake_tuner.py             live wake-word threshold + near-miss log
  assets/sprites/        Kenney robot-pack art (CC0) — see assets/sprites/README.md
scripts/
  slice_spritesheet.py  cuts a grid sprite sheet into the per-frame convention
tests/                  pure-logic unit tests (state machine, wake-phrase
                         matching, HTTP client against mocks) — nothing here
                         needs real audio hardware or a display

Security notes

Same as desk_client/bolt_desk.py: the server can relay a shell command back to this machine ("full desktop control" — "open firefox", "how full is my disk", etc.), which this client executes as your desktop user with a 30-second timeout (COMMAND_TIMEOUT_SECONDS). That's the same trust model as the Linux desk client and the Android app — commands only ever originate from your own voice/click requests in your own session. Keep DESK_API_KEY private; don't expose the desk API port to the open internet.

petctl commands (move/emote/say/wander/nap) are handled inside the pet and never reach a shell, so that channel can't run anything.

Two features widen what leaves this machine, both off-switchable in .env: SCREEN_CONTEXT=true (default) appends the focused window's title to what you say — titles often contain file paths, document names or email subjects — and NOTIFICATION_BRIDGE=false (default) can forward matching desktop notifications. No screenshots or images are ever sent.

Known limitations / not-yet-done

  • Pet screen position isn't persisted across restarts.
  • Wandering is a straight walk to a random point — no Shimeji-style physics, wall-climbing or falling.
  • Push-to-talk and the notification bridge are platform-limited: the hotkey needs a session that allows global key hooks (most Wayland setups don't), and the notification bridge is Linux/D-Bus only.
  • Barge-in listens through the same mic that hears the pet's own voice. It wants headphones or a decent gap between speaker and mic; if playback interrupts itself, raise BARGE_IN_RMS_THRESHOLD or set BARGE_IN=false.
  • Screen context is the window title only — the desk API takes text, so there's no screenshot understanding.
S
Description
No description provided
Readme 4.6 MiB
v0.2.3 Latest
2026-07-28 16:20:51 -06:00
Languages
Python 99.9%