From 80bef6f524feb441ea42d6b34e2868c99ca24762 Mon Sep 17 00:00:00 2001 From: themajesticmagician Date: Thu, 23 Jul 2026 06:25:41 -0600 Subject: [PATCH] Upload Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/settings.local.json | 32 + .env.example | 110 ++++ .gitignore | 5 + .python-version | 1 + CLAUDE.md | 199 ++++++ README.md | 175 ++++++ bolt_pet/__init__.py | 0 bolt_pet/__main__.py | 8 + bolt_pet/assets/sprites/README.md | 45 ++ bolt_pet/assets/sprites/error/frame_00.png | Bin 0 -> 5768 bytes bolt_pet/assets/sprites/idle/frame_00.png | Bin 0 -> 1386 bytes .../assets/sprites/listening/frame_00.png | Bin 0 -> 2372 bytes .../assets/sprites/listening/frame_01.png | Bin 0 -> 2360 bytes bolt_pet/assets/sprites/talking/frame_00.png | Bin 0 -> 1386 bytes bolt_pet/assets/sprites/talking/frame_01.png | Bin 0 -> 4060 bytes bolt_pet/assets/sprites/thinking/frame_00.png | Bin 0 -> 1783 bytes bolt_pet/assets/sprites/thinking/frame_01.png | Bin 0 -> 2036 bytes bolt_pet/audio/__init__.py | 0 bolt_pet/audio/barge_in.py | 62 ++ bolt_pet/audio/mic.py | 111 ++++ bolt_pet/audio/stt.py | 42 ++ bolt_pet/audio/tts.py | 163 +++++ bolt_pet/audio/wake_word.py | 185 ++++++ bolt_pet/config.py | 192 ++++++ bolt_pet/controller.py | 348 +++++++++++ bolt_pet/history.py | 62 ++ bolt_pet/hotkey.py | 105 ++++ bolt_pet/notifications.py | 166 +++++ bolt_pet/pet_actions.py | 137 +++++ bolt_pet/quiet.py | 78 +++ bolt_pet/screen_context.py | 184 ++++++ bolt_pet/server_client.py | 125 ++++ bolt_pet/speech_text.py | 130 ++++ bolt_pet/state.py | 71 +++ bolt_pet/ui/__init__.py | 0 bolt_pet/ui/app.py | 103 ++++ bolt_pet/ui/history_window.py | 83 +++ bolt_pet/ui/pet_window.py | 582 ++++++++++++++++++ bolt_pet/ui/sprite.py | 111 ++++ bolt_pet/ui/tray.py | 133 ++++ bolt_pet/ui/wake_tuner.py | 118 ++++ requirements.txt | 37 ++ run.bat | 11 + run.sh | 12 + scripts/slice_spritesheet.py | 50 ++ tests/__init__.py | 0 tests/test_barge_in.py | 65 ++ tests/test_controller.py | 195 ++++++ tests/test_controller_features.py | 243 ++++++++ tests/test_history_and_hotkey.py | 92 +++ tests/test_mic.py | 114 ++++ tests/test_notifications.py | 90 +++ tests/test_pet_actions.py | 71 +++ tests/test_pet_window_features.py | 181 ++++++ tests/test_quiet.py | 54 ++ tests/test_screen_context.py | 56 ++ tests/test_server_client.py | 86 +++ tests/test_speech_text.py | 59 ++ tests/test_state.py | 73 +++ tests/test_tts_stream.py | 82 +++ tests/test_wake_word.py | 148 +++++ tests/test_wander.py | 89 +++ thunderbolt.onnx | Bin 0 -> 822569 bytes 63 files changed, 5674 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 bolt_pet/__init__.py create mode 100644 bolt_pet/__main__.py create mode 100644 bolt_pet/assets/sprites/README.md create mode 100644 bolt_pet/assets/sprites/error/frame_00.png create mode 100644 bolt_pet/assets/sprites/idle/frame_00.png create mode 100644 bolt_pet/assets/sprites/listening/frame_00.png create mode 100644 bolt_pet/assets/sprites/listening/frame_01.png create mode 100644 bolt_pet/assets/sprites/talking/frame_00.png create mode 100644 bolt_pet/assets/sprites/talking/frame_01.png create mode 100644 bolt_pet/assets/sprites/thinking/frame_00.png create mode 100644 bolt_pet/assets/sprites/thinking/frame_01.png create mode 100644 bolt_pet/audio/__init__.py create mode 100644 bolt_pet/audio/barge_in.py create mode 100644 bolt_pet/audio/mic.py create mode 100644 bolt_pet/audio/stt.py create mode 100644 bolt_pet/audio/tts.py create mode 100644 bolt_pet/audio/wake_word.py create mode 100644 bolt_pet/config.py create mode 100644 bolt_pet/controller.py create mode 100644 bolt_pet/history.py create mode 100644 bolt_pet/hotkey.py create mode 100644 bolt_pet/notifications.py create mode 100644 bolt_pet/pet_actions.py create mode 100644 bolt_pet/quiet.py create mode 100644 bolt_pet/screen_context.py create mode 100644 bolt_pet/server_client.py create mode 100644 bolt_pet/speech_text.py create mode 100644 bolt_pet/state.py create mode 100644 bolt_pet/ui/__init__.py create mode 100644 bolt_pet/ui/app.py create mode 100644 bolt_pet/ui/history_window.py create mode 100644 bolt_pet/ui/pet_window.py create mode 100644 bolt_pet/ui/sprite.py create mode 100644 bolt_pet/ui/tray.py create mode 100644 bolt_pet/ui/wake_tuner.py create mode 100644 requirements.txt create mode 100644 run.bat create mode 100755 run.sh create mode 100755 scripts/slice_spritesheet.py create mode 100644 tests/__init__.py create mode 100644 tests/test_barge_in.py create mode 100644 tests/test_controller.py create mode 100644 tests/test_controller_features.py create mode 100644 tests/test_history_and_hotkey.py create mode 100644 tests/test_mic.py create mode 100644 tests/test_notifications.py create mode 100644 tests/test_pet_actions.py create mode 100644 tests/test_pet_window_features.py create mode 100644 tests/test_quiet.py create mode 100644 tests/test_screen_context.py create mode 100644 tests/test_server_client.py create mode 100644 tests/test_speech_text.py create mode 100644 tests/test_state.py create mode 100644 tests/test_tts_stream.py create mode 100644 tests/test_wake_word.py create mode 100644 tests/test_wander.py create mode 100644 thunderbolt.onnx diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..d5c2937 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,32 @@ +{ + "permissions": { + "allow": [ + "Read(//Main/Docker-Compose/TMN-API/tmn-api/**)", + "Read(//Main/Docker-Compose/TMN-API/tmn-api/desk_client/**)", + "Bash(/Main/Docker-Compose/TMN-API/tmn-api/.venv/bin/python3 *)", + "Bash(python3 -)", + "Bash(.venv/bin/python -c \"import PySide6\")", + "Bash(.venv/bin/pytest tests/test_wake_word.py tests/test_controller.py tests/test_state.py -q)", + "Bash(.venv/bin/pytest tests/test_wake_word.py tests/test_state.py -q)", + "Bash(timeout 20 .venv/bin/python -c \"from bolt_pet.audio import wake_word; print\\('imported ok'\\)\")", + "Bash(timeout 40 env QT_QPA_PLATFORM=offscreen .venv/bin/pytest tests/test_controller.py -q)", + "Bash(timeout 40 env QT_QPA_PLATFORM=offscreen /home/themajesticmagician/Documents/bolt-pet/.venv/bin/pytest /home/themajesticmagician/Documents/bolt-pet/tests/ -q)", + "Bash(.venv/bin/pip show *)", + "Bash(timeout 60 .venv/bin/python -c \"\nimport numpy as np\nfrom bolt_pet.audio import wake_word\n\nmodel = wake_word._default_model\nframe = np.zeros\\(1280, dtype=np.int16\\)\nscores = model.predict\\(frame\\)\nprint\\('scores:', scores\\)\nmodel.reset\\(\\)\nprint\\('OK — real thunderbolt.onnx model loads and predicts'\\)\n\")", + "Bash(timeout 120 .venv/bin/python -m pip install -q \"openwakeword>=0.6\")", + "Bash(.venv/bin/python -c \"import openwakeword; print\\('installed at', openwakeword.__file__\\)\")", + "Bash(timeout 90 .venv/bin/python -c ' *)", + "Bash(cp '/home/themajesticmagician/Documents/kenney_robot-pack/PNG/Side view/robot_greenBody.png' /home/themajesticmagician/Documents/bolt-pet/bolt_pet/assets/sprites/idle/frame_00.png)", + "Bash(cp '/home/themajesticmagician/Documents/kenney_robot-pack/PNG/Side view/robot_greenDrive1.png' /home/themajesticmagician/Documents/bolt-pet/bolt_pet/assets/sprites/listening/frame_00.png)", + "Bash(cp '/home/themajesticmagician/Documents/kenney_robot-pack/PNG/Side view/robot_greenDrive2.png' /home/themajesticmagician/Documents/bolt-pet/bolt_pet/assets/sprites/listening/frame_01.png)", + "Bash(cp '/home/themajesticmagician/Documents/kenney_robot-pack/PNG/Side view/robot_greenDamage1.png' /home/themajesticmagician/Documents/bolt-pet/bolt_pet/assets/sprites/thinking/frame_00.png)", + "Bash(cp '/home/themajesticmagician/Documents/kenney_robot-pack/PNG/Side view/robot_greenDamage2.png' /home/themajesticmagician/Documents/bolt-pet/bolt_pet/assets/sprites/thinking/frame_01.png)", + "Bash(QT_QPA_PLATFORM=offscreen .venv/bin/pytest tests/ -q)", + "Bash(QT_QPA_PLATFORM=offscreen .venv/bin/python -m pytest tests/ -q -p no:cacheprovider)", + "Bash(.venv/bin/python *)", + "Bash(python *)", + "Bash(QT_QPA_PLATFORM=offscreen .venv/bin/python -m pytest tests/test_controller_features.py -q -p no:cacheprovider)", + "Bash(QT_QPA_PLATFORM=offscreen .venv/bin/python -c ' *)" + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6ec5050 --- /dev/null +++ b/.env.example @@ -0,0 +1,110 @@ +# Copy this file to .env and fill in your values. + +# ── Bolt server (required) ────────────────────────────────────────────────── +# The same server URL + key desk_client/bolt_desk.py and the Android app use. +# DESK_API_KEY is the master key from the server's own .env, or a per-user +# key minted via the api_key_generate desk marker (see the main repo's +# CLAUDE.md "Per-user API keys" section). +BOLT_SERVER_URL=http://your-server:5002 +DESK_API_KEY= + +# Unique per machine/install so sessions don't collide. Defaults to +# "pet-" if unset. +#DESK_SESSION_ID=pet-my-desktop + +# ── Wake word ──────────────────────────────────────────────────────────────── +# openWakeWord model trained for "thunderbolt" — thunderbolt.onnx ships in +# the project root next to this file. Point WAKE_MODEL_FILE at a different +# .onnx model to change the phrase (same convention as desk_client's +# WAKE_MODEL_FILE / bolt.onnx in the main repo). +#WAKE_MODEL_FILE=thunderbolt.onnx +#WAKE_WORD_THRESHOLD=0.5 +#WAKE_CHECK_INTERVAL_SECONDS=1.2 + +# ── STT (Deepgram) ────────────────────────────────────────────────────────── +DEEPGRAM_API_KEY= +#DEEPGRAM_MODEL=nova-3 + +# ── TTS (ElevenLabs) — omit to use offline TTS only ───────────────────────── +ELEVENLABS_API_KEY= +ELEVENLABS_VOICE_ID= +#ELEVENLABS_MODEL_ID=eleven_flash_v2 +#TTS_SAMPLE_RATE=24000 + +# ── Audio devices (optional — leave blank for the system default) ────────── +#MIC_DEVICE= +#SPEAKER_DEVICE= + +# ── VAD tuning (optional) ─────────────────────────────────────────────────── +#VAD_RMS_THRESHOLD=300 +#VAD_SILENCE_END_SEC=1.2 +#VAD_MAX_UTTERANCE_SECONDS=15 +#VAD_MIN_UTTERANCE_SECONDS=0.4 + +# ── Pet window (optional) ─────────────────────────────────────────────────── +#PET_SIZE=160 +#PET_START_X= +#PET_START_Y= +#PET_ALWAYS_ON_TOP=true +#IDLE_ANIMATION_FPS=6 + +# ── Wandering (optional) — the pet strolls to a random spot while idle ─────── +#PET_WANDER=true +#PET_WANDER_INTERVAL_SECONDS=45 # average pause between strolls (randomized 0.5x-1.5x) +#PET_WANDER_SPEED=90 # pixels per second +#PET_WANDER_MAX_DISTANCE=600 # cap on a single stroll's length +#PET_WANDER_MARGIN=20 # keep this far off the screen edges + +# ── Mouse behaviour (optional) ────────────────────────────────────────────── +# Shaped input = the square window's transparent corners stop eating clicks. +# Click-through = the pet ignores the mouse entirely (control it from the tray). +#PET_SHAPED_INPUT=true +#PET_CLICK_THROUGH=false +#PET_EDGE_SNAP=true +#PET_SNAP_MARGIN=48 + +# ── Barge-in (optional) — talk over the pet to cut it off ─────────────────── +# Threshold defaults to 4x VAD_RMS_THRESHOLD because the mic also hears the +# pet's own voice out of the speakers. Raise it if playback self-interrupts. +#BARGE_IN=true +#BARGE_IN_RMS_THRESHOLD=1200 +#BARGE_IN_FRAMES=4 + +# ── Streaming TTS (optional) — starts talking on the first chunk ──────────── +#TTS_STREAMING=true + +# ── Screen context (optional) ─────────────────────────────────────────────── +# Sends the focused window's title along with what you said, so "what's this +# error?" has a referent. Text only — no screenshots leave the machine. +#SCREEN_CONTEXT=true + +# ── Quiet hours / do-not-disturb (optional) ──────────────────────────────── +# Comma-separated HH:MM-HH:MM ranges; wrapping past midnight is fine. While +# napping the pet dims, stops wandering and makes no proactive noise — the +# wake word and click-to-talk still work. +#QUIET_HOURS=23:00-08:00 +#DND_ON_FULLSCREEN=true + +# ── Desktop notification bridge (optional, Linux/D-Bus) ──────────────────── +# Forwards matching desktop notifications to the server so Bolt can react to +# them. Off by default: each forwarded notification is a converse() round +# trip. NOTIFICATION_FILTER is a regex over ": ". +#NOTIFICATION_BRIDGE=false +#NOTIFICATION_FILTER=build|deploy|calendar +#NOTIFICATION_MIN_INTERVAL_SECONDS=60 + +# ── Push-to-talk (optional) ───────────────────────────────────────────────── +# Global hotkey; needs pynput and a session that allows global key hooks +# (most Wayland sessions don't). Leave blank to disable. +#PUSH_TO_TALK_HOTKEY=ctrl+alt+space + +# ── Wake-word tuning (optional) ───────────────────────────────────────────── +# Scores within this margin below the threshold show up as "near misses" in +# the tray's wake-word tuner. +#WAKE_NEAR_MISS_MARGIN=0.2 +#WAKE_NEAR_MISS_LIMIT=40 + +# ── Misc (optional) ────────────────────────────────────────────────────────── +#COMMAND_TIMEOUT_SECONDS=30 +#HEARTBEAT_INTERVAL_SECONDS=60 +#HISTORY_LIMIT=100 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6664e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.venv/ +__pycache__/ +*.pyc +.env +.pytest_cache/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4fd785c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,199 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A desktop pet (PySide6 window) that is a voice/click UI on top of an external +Bolt server's desk API — same brain, memory, tools, and persona as that +server's Discord bot and Linux desk client. This repo has **no import +dependency** on the server repo; it's a standalone HTTP client configured via +its own `.env`. + +Pipeline: `mic → openWakeWord ("thunderbolt", on-device) / push-to-talk / +click → record utterance → Deepgram STT → + active-window context → POST +/desk/converse → [server may relay a shell command to run on this machine, or +a `petctl` pseudo-command that moves/emotes the pet instead] → reply → +ElevenLabs streaming TTS (or offline pyttsx3 fallback) → speakers`, with the +pet sprite/speech bubble reflecting state throughout, and playback +interruptible by talking over it (barge-in). + +Side channels that let the pet act between turns: the heartbeat (proactive +announcements), the desktop notification bridge, and autonomous wandering — +all suppressed while it's napping (quiet hours / fullscreen DND). + +## Commands + +```bash +# Setup + run (creates .venv and installs requirements.txt on first run) +./run.sh # macOS/Linux +run.bat # Windows + +# Run tests (no pytest config file — tests self-insert repo root via sys.path). +# QT_QPA_PLATFORM=offscreen avoids a QApplication segfault on headless/no-display hosts. +QT_QPA_PLATFORM=offscreen .venv/bin/pytest tests/ +.venv/bin/pytest tests/test_state.py::test_happy_path_transitions # single test + +# Convert a grid sprite sheet into the per-frame-PNG convention sprite.py expects +python scripts/slice_spritesheet.py path/to/sheet.png assets/sprites/idle --cols 6 --rows 1 +``` + +There is no lint/build step configured beyond pytest. `cp .env.example .env` +and fill in `BOLT_SERVER_URL` / `DESK_API_KEY` (+ `DEEPGRAM_API_KEY`, +`ELEVENLABS_API_KEY`) before running — without server config the controller +logs a missing-config message and exits its thread instead of starting. + +## Architecture + +- **`config.py`** — loads `.env` from the project root (not via python-dotenv; + a small hand-rolled parser matching the server repo's `desk_client/bolt_desk.py` + convention) into module-level constants. Everything else reads config from + here, never `os.environ` directly. +- **`state.py`** — `PetStateMachine`, pure logic with no Qt/audio imports (kept + that way deliberately for cheap unit testing). Enforces a transition table; + notably `IDLE -> TALKING` is legal directly (no LISTENING/THINKING leg) + because the heartbeat can make the pet speak proactively/unprompted. +- **`controller.py`** — `PetController(QObject)`, the pipeline orchestrator. + Runs on a background `QThread` (wired in `ui/app.py`) so audio I/O/network + never blocks the Qt event loop; communicates with the UI only through Qt + signals (`state_changed`, `said`, `log`, `action`, `napping`), never touches + a `QWidget` directly. Also drives the periodic heartbeat + (`_maybe_heartbeat`, gated by `HEARTBEAT_INTERVAL_SECONDS`) which lets the + server push proactive spoken announcements between user turns, and on the + same tick re-evaluates nap state and drains queued desktop notifications. + It owns the live wake-word threshold (`wake_threshold()` is passed to + `listen_for_wake_word` as a *callable* so the tray slider takes effect + mid-listen) and the conversation `history`. +- **`server_client.py`** — HTTP client for the desk API, dependency-free + beyond `requests` so it's easy to mock in tests. `converse()` loops relaying + server-issued shell commands (`run_local_command`, executed via + `subprocess.run(shell=True)` as the desktop user, 30s default timeout) via + `/desk/tool_result` until the server sends a final `reply` (capped at + `_MAX_RELAY_HOPS`). This is the same "full desktop control" trust model as + the server repo's other desk clients — commands only ever originate from + the user's own voice/click requests in their own session. +- **`audio/`** — `mic.py` (energy-based VAD utterance capture, ported from the + server repo's `bolt_desk.py`), `wake_word.py` (openWakeWord `thunderbolt.onnx` + detection + `NearMissLog` for threshold tuning — see below), `stt.py` + (Deepgram), `tts.py` (ElevenLabs, streaming by default — `stream_pcm()` + + `play_stream()` start playback on the first chunk; `chunks_to_int16()` + carries odd bytes across HTTP chunk boundaries, without which everything + after the first split sample plays as static — falling back to whole-clip + PCM then offline `pyttsx3`), `barge_in.py` (`BargeInDetector`: N consecutive + loud mic frames while the pet is talking cuts playback and starts the next + turn; threshold is deliberately ~4x the VAD one because the mic hears the + pet's own voice). Each accepts an injectable stream/model/protocol so tests + don't need real audio hardware or a display. +- **`pet_actions.py`** — `petctl` pseudo-commands (`petctl move top-left`, + `petctl emote wave`, `say`/`wander`/`nap`). The desk API has no "move the + pet" payload type and this repo can't change the server, so these ride the + existing shell-command relay: `controller._handle_command` parses them and + they never reach `subprocess`; anything else is a real shell command exactly + as before. Pure parsing; the UI half is `PetWindow.apply_action`. +- **`screen_context.py`** — active-window title (xprop/xdotool, Win32, + osascript) appended to each utterance via `context_for()`, plus + `is_fullscreen_active()` for do-not-disturb. Text only — the desk API takes + no images. Every probe is best-effort and returns None/False rather than + raising; the parsing is split into pure functions that are tested without a + display server. +- **`quiet.py`** — quiet-hours spec parsing (`23:00-08:00`, wraps midnight, + comma-separated). Napping suppresses *proactive* noise and wandering only; + wake word / click / push-to-talk still work. +- **`notifications.py`** — Linux/D-Bus notification bridge: tails + `dbus-monitor`, parses Notify calls (pure `iter_notifications()`), filters + and rate-limits them (`NotificationGate`), and the controller forwards + survivors through `converse()`. Off by default — each one is a round trip. +- **`history.py`** — rolling transcript (`HISTORY_LIMIT` turns) behind the + tray's History window and click-to-copy on the bubble. +- **`hotkey.py`** — global push-to-talk via `pynput`; soft-fails with a logged + reason (Wayland, missing package, macOS permissions) since the wake word is + the primary trigger. +- **`speech_text.py`** — sanitizes server replies before they're heard/shown. + `for_speech()` (called inside `tts.speak()`, so every path to the speakers is + covered) strips markdown, emoji, URLs and stray symbols the voice would read + literally ("asterisk asterisk"), turns bullet lists into full sentences, and + words a few symbols (`&` → "and"). `for_display()` is the looser version for + the speech bubble — markdown syntax gone, emoji kept. Pure string logic, no + Qt/audio imports. +- **`ui/`** — `app.py` wires `QApplication` + `PetWindow` + `PetTray` + the + history/tuner windows + the push-to-talk hotkey + the controller thread + together; `pet_window.py` is the frameless/translucent/always-on-top sprite + window + speech bubble (non-square frames are centered in the square + `PET_SIZE` window, see `paintEvent`), and also owns: + - **wandering** — a ~30fps timer walks the window toward a random on-screen + target every `PET_WANDER_INTERVAL_SECONDS` (randomized), suppressed + whenever the pet is non-IDLE, napping, dragged, or has a bubble up. A + commanded `petctl move` overrides all of that except the drag. + - **emotes** — `emote_transform()` is pure maths (dx, dy, rotation, scale + from a 0..1 progress) kept out of `paintEvent` so the curves are unit + tested; every emote must return to the identity transform at progress 1.0 + or the pet ends up permanently askew. + - **shaped input / click-through** — `PET_SHAPED_INPUT` masks the window to + the sprite's opaque pixels so the square window's transparent corners stop + eating clicks (mask rebuilt only when the frame changes, and pinned to the + resting position so a bob/spin doesn't thrash it); `PET_CLICK_THROUGH` + makes the pet ignore the mouse entirely. + - **edge snapping** (`PET_EDGE_SNAP`) after a drag or a stroll, and **nap + dimming** (`set_napping`). + `sprite.py` loads `assets/sprites//*.png` (filename-sorted, looping — + currently Kenney's CC0 robot pack, see `assets/sprites/README.md`) and falls + back to a procedurally-drawn placeholder blob per state if a folder has no + frames; `tray.py` is the system tray menu (talk now / mute / nap / wander / + click-through / history / wake-word tuning / quit) — the pet window has no + title bar or taskbar entry; `history_window.py` and `wake_tuner.py` are the + two dialogs it opens. + +### Wake-word detection + +`audio/wake_word.py` uses a custom-trained openWakeWord model, +`thunderbolt.onnx` (ships in the project root), the same way the server +repo's `desk_client/bolt_desk.py` uses `bolt.onnx` for "hey bolt" — same +runtime (openWakeWord, ONNX inference framework), same per-frame +`predict()`/`reset()` loop. Every mic frame is scored; any class score at or +above `WAKE_WORD_THRESHOLD` (default `0.5`, in `.env`) counts as a +detection. Swap `WAKE_MODEL_FILE` to point at a differently-trained `.onnx` +model to change the wake phrase — everything downstream (STT, server call, +TTS) is unaffected. + +The threshold is tunable at runtime: the tray's **Wake word tuning…** window +(`ui/wake_tuner.py`) shows the peak score seen and a rolling list of near +misses (frames within `WAKE_NEAR_MISS_MARGIN` *below* the threshold — i.e. +the times it nearly heard you), and its slider is read per frame because +`listen_for_wake_word` accepts a callable threshold. Set the threshold just +under the peak you can hit reliably, then persist it in `.env`. + +### Testing conventions + +`tests/` covers pure logic only (state machine, wake-word scoring loop, mic +VAD, HTTP client against mocks) — nothing there needs real audio hardware or +a display. Modules under test are written to accept fake streams/models/ +`on_command` callables specifically to keep tests hardware-free; follow that +pattern (inject a `Protocol`-typed collaborator) rather than mocking at the +`sounddevice`/`openwakeword` import boundary when adding new testable logic. +`test_controller.py`, `test_controller_features.py`, `test_wander.py` and +`test_pet_window_features.py` need a `QApplication`, which segfaults without +a display unless run with `QT_QPA_PLATFORM=offscreen`. + +Newer subsystems follow the same rule — the testable part is separated from +the part that needs hardware: dbus-monitor output is parsed by a pure +`iter_notifications(lines)`, xprop output by pure `parse_xprop_*` functions, +HTTP chunk reassembly by `chunks_to_int16`, emote motion by +`emote_transform`. Tests that touch the controller monkeypatch +`screen_context.context_for` / `is_fullscreen_active`, otherwise they shell +out to xprop on a headless box. + +## Security notes + +The server can relay a shell command back to this machine to execute as the +desktop user (see `server_client.run_local_command`). This is intentional +("full desktop control" for things like "open firefox" or disk checks) and +matches the trust model of the server repo's other desk clients. Keep +`DESK_API_KEY` private and don't expose the desk API port to the open +internet. + +Two newer features widen what leaves this machine, both switchable in `.env`: +`SCREEN_CONTEXT` appends the focused window's *title* to each utterance +(titles often contain file paths, document names, or subject lines), and +`NOTIFICATION_BRIDGE` (off by default) forwards matching desktop +notifications to the server. Neither sends screenshots or notification +contents you haven't matched with `NOTIFICATION_FILTER`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9efeee1 --- /dev/null +++ b/README.md @@ -0,0 +1,175 @@ +# 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. diff --git a/bolt_pet/__init__.py b/bolt_pet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bolt_pet/__main__.py b/bolt_pet/__main__.py new file mode 100644 index 0000000..22daeb1 --- /dev/null +++ b/bolt_pet/__main__.py @@ -0,0 +1,8 @@ +"""Entry point: python -m bolt_pet""" + +import sys + +from .ui.app import run + +if __name__ == "__main__": + sys.exit(run()) diff --git a/bolt_pet/assets/sprites/README.md b/bolt_pet/assets/sprites/README.md new file mode 100644 index 0000000..a77b241 --- /dev/null +++ b/bolt_pet/assets/sprites/README.md @@ -0,0 +1,45 @@ +# Sprite assets + +Art: [Kenney's Robot Pack](https://kenney.nl/assets/robot-pack) (CC0 — no +attribution required, credited here anyway), the green side-view robot. +Source pack lives at `~/Documents/kenney_robot-pack`; only the frames listed +below were copied in. + +Convention the loader (`bolt_pet/ui/sprite.py`) expects: + +``` +assets/sprites/ + idle/ frame_00.png robot_greenBody (standing) + listening/ frame_00.png, frame_01.png robot_greenDrive1/2 (tracks rolling — "leaning in") + thinking/ frame_00.png, frame_01.png robot_greenDamage1/2 (flicker — "processing") + talking/ frame_00.png, frame_01.png robot_greenBody, robot_greenJump (bounce) + error/ frame_00.png robot_greenHurt +``` + +- One subfolder per pet state (matches `bolt_pet.state.PetState`). +- Any `*.png` filenames work — they're played back in alphabetical-sort + order, looping, at `IDLE_ANIMATION_FPS` (see `.env`). +- Frames are scaled to fit within `PET_SIZE` (default 160px), keeping aspect + ratio, and centered in the (square) pet window — the source art here isn't + square, so don't assume it fills the frame edge-to-edge. +- A state directory with no frames in it falls back to a small + procedurally-drawn placeholder blob (see `_placeholder_frames` in + `sprite.py`). + +## Swapping in different art + +Replace any state's PNGs (same alphabetical-order-loops convention) to +change its look — no code changes needed. If your source is a single grid +spritesheet (rows/cols of frames in one PNG) rather than one-file-per-frame, +use `scripts/slice_spritesheet.py` to cut it into this folder-of-frames +convention: + +```bash +python scripts/slice_spritesheet.py path/to/idle_sheet.png assets/sprites/idle \ + --cols 6 --rows 1 +``` + +If your format is something else entirely (a single animated GIF/APNG, a +Spine/DragonBones skeletal export, an Aseprite `.json` atlas, etc.) — tell me +the format and I'll adapt `sprite.py`'s loader rather than making you convert +by hand. diff --git a/bolt_pet/assets/sprites/error/frame_00.png b/bolt_pet/assets/sprites/error/frame_00.png new file mode 100644 index 0000000000000000000000000000000000000000..b0583aa342011c0a3af8f7f51b18ebfb1e170799 GIT binary patch literal 5768 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 AmjD0& literal 0 HcmV?d00001 diff --git a/bolt_pet/assets/sprites/idle/frame_00.png b/bolt_pet/assets/sprites/idle/frame_00.png new file mode 100644 index 0000000000000000000000000000000000000000..297722b933b055da79e78b8fef371d8485308097 GIT binary patch literal 1386 zcmeAS@N?(olHy`uVBq!ia0vp^bAdR6gAGW&uKZNPz`*ju)5S5Q;?~--Z!Y*>Bjg2`o=`z0$kEeqC?i-9+ z`@AweW0q{QbDUab@^yD&;G0=D&YfFVzWo2hdBVrjRL-9_wtnBUVGUpOE}P5dF{hTl zDYDC+{;Ab@`~Kh*W%)lPuC*2GA3S~WclE0q=ZjzcSZ{vydA9ef?)2?KGdFIX>J+Q> z?#Ifm?EbJh`xf_4E00Lrs+hiOd0WmMy{D@l+=}bF^VhNV&x=`7vGJZd2hvtDmK?u! zcZykq{)SY?#pXuBv)azj7R}zApir}^g-KuJg^cBd~B<*P2u_n$3#*uM7Fm%N>o z31zS2o){i$DGGQz>Gyo!qt7O@9Vj)ges*4Rv-W)+|5xRu=_~)ueiDA{z~&>b=6p2M z-&z#6J5VihZ_e+f0yiBEUrg~Y$u~Hr)mQJA{ri0Jy_>79+?DuH;d1QIl1JP3ua^CN z_v}8MsRpNyY6q2=znv4AX1#ObqlJ^j*{ACp-7xGDdFX5X>a)41{>77hod^5EeQe{u zUDz8xD{+FOxMxb|*9#lBH0iB6akX}R&Zl>0Kl2#9Q0k0{`kH37PQ6j@RCbWut_AVu zUQbqMT6(vhza%Y3&4GQ_31+JcjeI2wm}Or$u#?PW0c+#{YZU~Wqr5;FY?eFNw7v$g z(TuV$cpS<(;DKa(5pzedFHMvgov3 zZ0K9F%Jq5$R=-$dnJ)I;(O%^@yGBjJA{MB=@x_y!D=(|f=j`$e-@m|HVb?4B`f{5e zpJpz2QSz007t5mCdE8(rppQ6CYbPonbYm)!Ud}kzOm?~OE}(%<-G|@K;c6+=TDUm= zg6a>6H6Pw2o^G7}{e>MH_bvSwLUj-3S3as@f7|ODH+g?+=>z#C@fQlWcCPkiDS>#` zqv-)Ctk*2F{>@&)OcjgWQ}iodu%#KZvPqRp zTy#71Opn2ZkEb4da#g;_JH1$0Zr9w+=|Matdx{RsG!l-@TvpF~ck1Q6K!Hhm&wE_V zpZI)_s9UYOF_g_(?xu15xiimq7X90}b?3*J4elNQ4 z_u2&`PrKBuSvs4qJavi>YdoH}_4R{4-@d)MSr^dqnmdKI;Vst E0PmrsMgRZ+ literal 0 HcmV?d00001 diff --git a/bolt_pet/assets/sprites/listening/frame_00.png b/bolt_pet/assets/sprites/listening/frame_00.png new file mode 100644 index 0000000000000000000000000000000000000000..b4602398abad8ca855da69f61c2904b27cd3a9e0 GIT binary patch literal 2372 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 E0PmrsMgRZ+ literal 0 HcmV?d00001 diff --git a/bolt_pet/assets/sprites/talking/frame_01.png b/bolt_pet/assets/sprites/talking/frame_01.png new file mode 100644 index 0000000000000000000000000000000000000000..356cfd0b007d6e148dd93d9e3f18e4e50d696d44 GIT binary patch literal 4060 zcmb_f_ct33*H1!XOVy@!)e0qMRclkT_NWzm?`RPt_9{i~z4umorL{wqiV|wIU!xRJ zMOD?~`=0YYf5G#^y`OXL`rLcY{o&pOJzaGwau#v`06?YjM8)6+TW%zp6mp|QElalm z0H~XWilR~A@T)#LVi9B$B_#@6H@hEgT ztJV4MFCpKbA52G~e}A{i_#VZ|TrGH$f7D&=Tlh33J1^XftTN<#xKdwq@yBT>U8(!5 z>$(C5?Bor&G+>OXbBYVU_CNl-GsM7mv@U3tGcXMSqrm)z%2wKq7>1UM zfrO|RA_lty{a<$Ois{1Rgd$ea769N*TVd zq)W?vbdaq@y?SY6~NIQnigS;Ru-Ee{R(IgL7<1>FHM=BRz5mD ze#OiLk)cy28uEos3#U=~ z8$hR<@`(Sh{(tzd;lDJ=H>?zIrMd>WrS3e04IcmDn)`*bKHlhjKNx%T_FNo8=>sbP zXPlm%e(ZEQiJkuNuZ6#J>x;YH*Kf|F%6%M+^@~mVk)Ngr&(?$YnTVzV`5lczfyy4i zSK!veS=~9pgseU0ok`;}-o3rOk#t_Ik#{i^qmf_=m9NLcl%hlp&V=aM7PB=S5QW8H zNRL@BvexX4i$28czBpKn$nq{=e{U(I({7Yp-@-!3&CM+&;DDUyy; zLp?Ksn8oX3MjGTMN_)5-wLU`BzuA~Q@;hrsWjMmq?&5a`b9euA)lIOh0XK(Aqs~>U7U7a9Nn*f_T)~Nk<7^k8$*7_H- z65s=B`LQy6)C ztk5utPz6I{=qnCW-Ye{8mhh!Aj9bVpf`;2AWT#>%~Ebv!%+H#zsaGW25saY!9V~4pO<4 zRj&VDg?esaBJBQ|NGo--H61UbY@A1Nl}8DP4qT|p&a1peZC(>3E#Aw?o*x`4y81hq z^uYV=$6nU5N_$zVXJE3jvy@a9xQ0XZgBjP_4bh7QbZ)Wv+?yZJr_4MAK zeZPL+^29~Y6p-0Y#AM)%?Qm>@ViZJ!Xx|ZD6u3Q|yDV$c^|jIV+@8{Bo#7J$ygxHD z^IMA}@Qu5xYb_1JpQ?xz8k_i1Z>|;2<{fzOIYl}*#HSUY9Vr6~Bg zja@I5!@R?zO>D>KU!c4t@fQEb-S=M4a%VakjOU34hPiPvo`qO8HLi73)&S2aa$4cs z59icnWvtt#RD!3nZWh=K%zp%?BG`TyuRT+xqrkAAh2OW-*H1HuF_9dgkJ)@tIoK64 zTZaXim+x-afxCNqdwWO_c+2||!U%q17oFXWnz3Z|u)mx{-|bI!K0NIsm`nz*)___? z14OY{v<#NIBhx|}jtSC!_84=*kZ3u(@YdI*b!3o+(g#i5)X?Bj39^iD<62BbRSm8y zF^LO0p?9Wp4OM5Z?7j&j16S)TEm6_NNKqnfVkUvMy8z0lluld@}-2B<0Cc3s;NDAtyALw>K#7KkvWjaM@=4w-s?$2{#6AO*9FV9kx&w ze*_KI*;|^+5HM_@LK-|=`!n|Pc^Fb8jo>AJEm)a4cQ2DfZG+bFSh1=U!g`VG)_gOk zS}!vZEbj!MLE1>`DyZQ*n-$e%SvDliaT1_yueAE^PJKu^HH&gi_BvJwom3yLD zqLuP{&^LVHLdGzeU|oIZU@(X)4Iuw~+XYdcagrPI=%=}fiHTc%Dh*xWrhCRB8Tnd2 zWx1kavsU5995H){z%Rd@uiDb@hTWTk%DhoqYiH-T$p~XSmmmvcabN*Tzgko;HWQcw zVh2ks__7(c`-P#@Fyc!&uTEH{vEW}Dg<1*3f$zso6@?qVSzlp{*kFdq{_NyG;=l>E zEC@3_FD9GnrpHV2RHFF2SN;Q9gM(j^!=*_B*~}<~6x(u6Lmx#Fo|0a%RZB8|_t*;b#2UVQ3oI5nl>w(MDC) zwg#=zCf{5xk8{_~I~YM-61(N$j&ZUyJ^8qy3^cX%Is*o7ucpWhNlaM)2d*r1jh6Oh z(~=xTupT8Y$Nt9L?mId6JR@G3OWMLg;VwRgorDdREYqRI=@}kFDrowoSh*!zLz#U5 za4kDbK5eb*c!E8c=+(Mvp*FRkP`xH;jz+WYK~hPlY1w`5h6vsJ_3Qnm{1wWG>!@|r zgCqqG#Z*&6unY$ho|_!9X4x_D(a1a(XcE;`ef{?6om3reOTtA;w=$T4SoDpkWaT}> zx-4e##2G zT%pHb6SXe2jw?5#OI702q@{uxwR2M=)JTt%_geqGACSd+D?&hCdIa{aYAf)&)Vutm z_%v4s16S*|7B>k|XROjKbZdA-cU!-*;cy~Oq*feW zvtzG^#Wt0SPv1wOq5OMSpURf~u%2^2FIV)O|9DK2NQmL(pwR}mxcAq;&nIYinL3eJ z0T>|7tZ{7N$eCF^=KSmj_=&MtIGj(qC&=4^53c`H-28gkt@&l&WI=HRLeqF08w`i7 zSG{sd#nbYvkBL;xjPUI3_|c=2J50y$Es=Ty-m_wsRk~7<*+l(yadLUV&2S5M@PTLP zX5qsO>IWJ4=N+jp>M|}At~4hVWlhF50yZvcW}0vEGtzsJ}Hf1tcmTNyAb-7x@9L6Hnk*#(Wlh5C?rECSdaPavtaxYvf7 ze#7xf5$57W{mtS#kE@2V-!ZU_|J3@j4wfqgl~>{X5?Cnmk5TP!Db1udM+wvm-EYc+ z0b?)AmH?~~WQSUXHF}cx1P53sfnA1S;aH>+AO7KRqg%L%V9*BRIt$Op+O>u|^Q}Av zj76t(fbZ+RLYC+Ru}AIA`$tC7)D^Xj7vSNUce;f4AE;@amt|#o{u8aUZB}k&x#K@#%4GQ|J@0gXj{Uv}! zwx)u!IRlbYcKit3I1`VxnrPvi2bJicoa@5Irt=Nvdm$sWH31%CQ=oiCxah=4NgGpa z7&Y5kd}ObboJ4#lbL(tGC&Mkf=5;*><-yITta(grn1xmtgTr!yy8p=P zTC0{J!<)Mg1~Lg~QF=PM;DDXcLlAoPJwIEtrAJ}_%%*qo8o_V=NmH+1X7DlR? zAq~BqkS}cg907JgB6c3$-VP2yBKCekK1gp7S5I4KN0FQN9WJ5jW$*3a=vM;?KfFl< NXsGI{G$|n>{{vKutOfu8 literal 0 HcmV?d00001 diff --git a/bolt_pet/assets/sprites/thinking/frame_00.png b/bolt_pet/assets/sprites/thinking/frame_00.png new file mode 100644 index 0000000000000000000000000000000000000000..bf6ac568681c78d669ba3c73a8fda891d548b4dc GIT binary patch literal 1783 zcmbtV`#;kQAOFr}%_78Xxh&fe&J*L5Tt>O>xWo!+?4Z~XlNEEB;f$FcNt{r~nsr{~ zmPvFRcY5-mT((CeIU0#(cocsK1-rt z#cSJROUYFE7I!CKI=50l&Hh?WBx#R_r{bDQY>?9rgpVo#tkKPzJ{Abb87L1hatPUy z>5XmFfZvm+uXw-7cOLEWHC09kCF|er#LonUs`ni002YJR{>pwMTyd}`lto3^e0p2+ zchKFPy^|hUl9N6$dy_w|kF1^vCar$&LV+8C$k+0~RrzDj^%;-SSe+f8l>rq`6GG&W z1AT}YL5FqOBQ8B zSMyO=2C)1*U!L_UGJX1yXHj<3e*=$!J+qfV91LW>90&<nLNYyNRE-MAAE3zhs$|<>r$j`Z@iguS^?TTY7n!>?zuQrZT-PMc-1D zpf%ZnTG;H1PVf?mq-)aJE`9eb;)no;#x}>KV)f4{(0q|aCwK>UW*zM9ksfhkbwQ1< zH-L45w*m&0iZtdS0SN1KK=OYy3i}|CeIUbqFcx5-v=6GW&*Cg+YAp^Omb1ZC4J|Z6 zx-MCLm+cO7@5!k*4pbzn*UskKP4onLmG>u*dIk^F4Q(;FD|;J+`;wnhF=p4Ymg`h$ zpYqw(Ok?#|!E2JPBXbP*RnKyxHMg8gyBjvf#7*7F9_49*iUfkmS{^X(xPM+2_sV5r za++NR;IXvH&Z`SaLP`Y4ge2zQ7*Vl3OF-5q^tKDWYG4~Q>4Sgr^x6sGq3{qbZQG#T zq*F8{Ep^+o)K#+t@r?)P&?)$TS9J_7J)~s0m{7$0FSKG-E2!WmxMAC%lUnh`JK;pe zT=%c1^|*W$DCbFf$;cuW$9qG`FoBLWPaMe5;EXADNH)k0I!t_TC{o?Qo(UN@ z?63ZigOP9vLLB_ZRmH!LM17EK#Oe26(n0$8z z9gkz1b{EN-es-E*&E!^U&L=xp=`i{Isr0ym3c^?$NDA{%yf#c9z4rC%6HS40W=6?E zR18RB$iI?ijJ1+lg)7nuH=7t23b&#WBvmkdWVIS0lTHH$AMLvnV*a7nW+LF2Mydiu zW*j0n;IM6Hxkaf!4R%r)p)0^ar*r)xRdeuxtkk8pJA(Z2c#<)#Kab#VNqa%GETNy` z)nN`HU0OD;JF0Tb9#QO$@$_oUHlxR5a^8Y(aVnsJgK7M`Vi6I(j{TUxyD`3UKUI%5 zn!f9KRQ0w%_i;{3r>%9JUK^U22YO8JI$~^D>W zyu%DDX{T>Qt)G3igDwZbvy7SAggyBVI$F_Bp!toe$$iP3h6|+mQzL1~Vet__I2{un xOQn*@bPOq;evL-OL|+YyionR7?SvIBjzlF##8-oV{jYow@WkQW_%4^T{s-q5HjDrO literal 0 HcmV?d00001 diff --git a/bolt_pet/assets/sprites/thinking/frame_01.png b/bolt_pet/assets/sprites/thinking/frame_01.png new file mode 100644 index 0000000000000000000000000000000000000000..806d10e9f7677ad3876f8e4b0ebc865984fd4a99 GIT binary patch literal 2036 zcmVv{5g000MqNkl3#L4kq=DweVmQNg!@Vp~Z}KPXhIbz89QqR84sN-6Cw zyHykcD@a>O)P6B0ej4`~7_vLeoHJ+M=gf1G=gaO)o9y}R|9#FD3W{r>In#Z(QSK== z%cWwYTzTb(uMO&vj{bDXc>Pwz)UjW_Gv4|2igEJ(HRFS|>&EGaH!`V_wdG2OHtM4v zZq$oI8@29ZcbB#lqFVj6>8=)Jqs7MTCgJj@S8rZ4-nx6)7+PP>EPzgG0azbdulIxo z;A7ZIgeF|U66yw@X%$;mFc5rjp#&s49r^Ja)AH#C-tb0!b7-T!O&K`2Qi2hWj^D0o z2cBj12wch&klcdj#ibrzDH{iC=Zyo`Cym~P38Uxx&!`ZD&or&^r(LklC+=M}-v513 z4qhwC5GD<7|9GG|YrM34&Uo?a7X&0C?YS~;exb9Nl>nI_AizQ zMNGnHo#m1c>XL4?K2KH-=AIzLFMQFn9-lfD!{2*Q+bT>RXx)+%!WbROL3Wq>R8rC2 zX&`XPYVZ8#+GOSJ;Gurk(Mz>1!nceYPZt}e#cOx9=+{0fldN1QTBQ(VP~3EuPr^UCyAhmphVxjnL%)I;)7QV$~qPi8zlP2|}k-?ohRAall6C zG=t76;`%wUJucV?ohHz!mOI)7K;wihROqO9p;Ia1y1|CTBGXeQRD94OUh{j>dD5^E zx{N~S=ji{mz((jY>|VW=K&<|#mP4FWywJ%Khak;(LPzM7e%2}Nuo1e9XRmhPvH%;Q z%Xpp6Iy^e6{wSd%bdIHytieX;IK)ZlC_+aOIzktvij7%9N9ck9qyo|L+jC|X_{Ptd z2pysG2b@*6>RX)j44kF3Sv)TrIDBW`sQu{U{6Wb8066;Iux_; ziIN1-{i3*#iA0VP2{&-uES8rVp2$_l37)76p}NB8PN37E3Y}ATLhFXV3_MY+*4|Q6 zNl4-<4twcFMA4P-(v6UHCA6P1IePP=5Ij+=CW1DOc9{%-%ELQS;BZaYZLr>tPDp{n zHMt&b5?L*g%ap~3QG@3NfU$dZv%zm)cn$AG)$s3|8Z{gIcG&P5-s3!-2k>AK{gh&& z>{@WNDF?MYH_IggHp+Gxq00$)2ba$&$9GYIN9Z)tzJ&?bSWN_wD87rLYeDEVtmTQo za{>sk1{tGD8oHo|S6U);0l{+`YZ3uupHPkxJVK|jRQs)Xh{x|36`><^QjpwADYE`< z7~m1QoYwN(k_uUT7sg|agf55R$)J(NcR{-rgf3^bJg*6XthXBkJVHmg3JHe{JU@>$ z5;`i-Q)zy>7KDxx3hgu5(5PFBjnGk{&+Z{1@l!Bn8;nwp?~shBS~o&R`Ht`0ipqrU zF?74<#_9wnUFuz!&=R;}qg*ke+ch^<;YJg>SfSfp9UJ6Er>sD|G^HhUhnus5CUiUJ z&h&7jQ&xhho!1k(#>}=y&|Ug?D>ph_8feaH30;3}x~pKVmW$owW~V0u)-s-JmRAey zi*X&}xyW7X%7tPFbkBYJNf$Sqt~E;aEsbv}*w?P=*eEwWSsl1OsjF)pEjTS$JH~d& zY0s7MmJ2FtSeu*O>_{N&i{+E#^m6?xP0J+Zo`P)A&9W{RxmzW8%vKWz&RQ?bo!v$# zqK)CS?>zWfNG^6&KP?fG@EiI%mPXLfAE=$*S_ot9sGjYH%Vl;OO#2s0M*odz9bIW@ zpgGfBh-y7wJ@d@Y%VR~1L~$%iZqZPCLuRT|IRq9B6C-e=jzwR|`XA{B=d|=zLx=zX z04;PySaefwW^{L9a%BKwc`jmXZ*OE|c`jped2n=ZE@^FHXJsx>PDe5{MQ&qnWMy)w S27m4V0000 int: + return self._loud_frames + + def reset(self) -> None: + self._loud_frames = 0 + + def check(self) -> bool: + """True once the user has been loud for long enough to count as an + interruption. Never raises: a mic hiccup mid-playback should not kill + the reply, it should just mean "no barge-in this frame".""" + try: + chunk, _ = self._stream.read(self._frame_len) + except Exception: + return False + frame = np.asarray(chunk) + if frame.ndim > 1: + frame = frame[:, 0] + if frame.size == 0: + return False + if rms(frame) >= self._threshold: + self._loud_frames += 1 + else: + self._loud_frames = 0 # a single thump/cough shouldn't count + return self._loud_frames >= self._required diff --git a/bolt_pet/audio/mic.py b/bolt_pet/audio/mic.py new file mode 100644 index 0000000..ba0e933 --- /dev/null +++ b/bolt_pet/audio/mic.py @@ -0,0 +1,111 @@ +"""Mic capture + simple energy-based VAD utterance recording. + +Ported from desk_client/bolt_desk.py's record_utterance() — same tuning +knobs, same behavior. Kept independent of any UI/threading model so it can +be unit tested by feeding it a fake "stream" object. +""" + +from __future__ import annotations + +import io +import wave +from typing import Optional, Protocol + +import numpy as np + +from .. import config + + +class AudioStream(Protocol): + """Minimal shape of the object record_utterance() needs — matches + sounddevice.InputStream's .read(frames) -> (data, overflowed).""" + + def read(self, frames: int): ... + + +def pcm_to_wav_bytes(pcm: np.ndarray, sample_rate: int = config.SAMPLE_RATE) -> bytes: + buf = io.BytesIO() + with wave.open(buf, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(sample_rate) + wf.writeframes(pcm.tobytes()) + return buf.getvalue() + + +def rms(frame: np.ndarray) -> float: + return float(np.sqrt(np.mean(frame.astype(np.float64) ** 2))) + + +def record_utterance( + stream: AudioStream, + should_continue=lambda: True, + rms_threshold: int = None, + silence_end_sec: float = None, + max_utterance_s: float = None, + min_utterance_s: float = None, + frame_len: int = config.FRAME_LEN, + sample_rate: int = config.SAMPLE_RATE, +) -> Optional[np.ndarray]: + """Capture one utterance from *stream*: wait for speech to start, stop + after trailing silence. Returns None if nothing usable was heard. + + *should_continue* is polled each frame so a caller can cancel recording + (e.g. the pet window was closed) without needing threading primitives + baked into this function. + """ + rms_threshold = config.RMS_THRESHOLD if rms_threshold is None else rms_threshold + silence_end_sec = config.SILENCE_END_SEC if silence_end_sec is None else silence_end_sec + max_utterance_s = config.MAX_UTTERANCE_S if max_utterance_s is None else max_utterance_s + min_utterance_s = config.MIN_UTTERANCE_S if min_utterance_s is None else min_utterance_s + + frames: list[np.ndarray] = [] + started = False + silence_frames = 0 + silence_limit = int(silence_end_sec * sample_rate / frame_len) + max_frames = int(max_utterance_s * sample_rate / frame_len) + grace_frames = int(4.0 * sample_rate / frame_len) # wait up to 4s for speech to begin + waited = 0 + + while should_continue(): + chunk, _ = stream.read(frame_len) + frame = np.asarray(chunk)[:, 0].copy() + frame_rms = rms(frame) + if not started: + waited += 1 + if frame_rms >= rms_threshold: + started = True + frames.append(frame) + elif waited > grace_frames: + return None # woke it up but said nothing + continue + frames.append(frame) + if frame_rms < rms_threshold: + silence_frames += 1 + if silence_frames >= silence_limit: + break + else: + silence_frames = 0 + if len(frames) >= max_frames: + break + + if not frames: + return None + pcm = np.concatenate(frames) + if len(pcm) < min_utterance_s * sample_rate: + return None + return pcm + + +def open_input_stream(): + """Real sounddevice input stream, imported lazily so pure-logic tests + (record_utterance with a fake stream) don't need PortAudio installed.""" + import sounddevice as sd + + return sd.InputStream( + samplerate=config.SAMPLE_RATE, + channels=1, + dtype="int16", + blocksize=config.FRAME_LEN, + device=config.MIC_DEVICE, + ) diff --git a/bolt_pet/audio/stt.py b/bolt_pet/audio/stt.py new file mode 100644 index 0000000..e3782c6 --- /dev/null +++ b/bolt_pet/audio/stt.py @@ -0,0 +1,42 @@ +"""Speech-to-text for the actual query, after the wake word fires. + +Deepgram, same as desk_client/bolt_desk.py. +""" + +from __future__ import annotations + +import requests + +from .. import config +from .mic import pcm_to_wav_bytes + + +class SttError(Exception): + pass + + +def transcribe(pcm) -> str: + if not config.DEEPGRAM_API_KEY: + raise SttError("DEEPGRAM_API_KEY is not set") + try: + response = requests.post( + "https://api.deepgram.com/v1/listen", + params={"model": config.DEEPGRAM_MODEL, "language": "en", "smart_format": "true"}, + headers={ + "Authorization": f"Token {config.DEEPGRAM_API_KEY}", + "Content-Type": "audio/wav", + }, + data=pcm_to_wav_bytes(pcm), + timeout=30, + ) + response.raise_for_status() + except Exception as exc: + raise SttError(f"transcription request failed: {exc}") from exc + try: + return ( + response.json() + .get("results", {}).get("channels", [{}])[0] + .get("alternatives", [{}])[0].get("transcript", "") + ).strip() + except Exception as exc: + raise SttError(f"couldn't parse transcription response: {exc}") from exc diff --git a/bolt_pet/audio/tts.py b/bolt_pet/audio/tts.py new file mode 100644 index 0000000..c0b2917 --- /dev/null +++ b/bolt_pet/audio/tts.py @@ -0,0 +1,163 @@ +"""Text-to-speech: ElevenLabs, requested as raw PCM so playback is just +sounddevice — no external player binary (mpv/ffplay), unlike +desk_client/bolt_desk.py which shells out because it only targets Linux. + +Falls back to pyttsx3 (offline, cross-platform: SAPI5 on Windows, NSSpeech +on macOS, espeak on Linux) if ElevenLabs isn't configured or the request +fails, so the pet can still talk with zero cloud config. +""" + +from __future__ import annotations + +from typing import Iterable, Iterator + +import numpy as np +import requests + +from .. import config, speech_text + + +class TtsError(Exception): + pass + + +def synthesize_pcm(text: str) -> tuple[np.ndarray, int]: + """Returns (pcm_int16_mono, sample_rate). Raises TtsError on failure — + callers should fall back to speak_offline() rather than treating this + as fatal.""" + if not (config.ELEVENLABS_API_KEY and config.ELEVENLABS_VOICE_ID): + raise TtsError("ELEVENLABS_API_KEY / ELEVENLABS_VOICE_ID not set") + try: + response = requests.post( + f"https://api.elevenlabs.io/v1/text-to-speech/{config.ELEVENLABS_VOICE_ID}", + headers={"xi-api-key": config.ELEVENLABS_API_KEY}, + params={"output_format": f"pcm_{config.TTS_SAMPLE_RATE}"}, + json={"text": text, "model_id": config.ELEVENLABS_MODEL_ID}, + timeout=60, + ) + response.raise_for_status() + except Exception as exc: + raise TtsError(f"ElevenLabs request failed: {exc}") from exc + pcm = np.frombuffer(response.content, dtype=np.int16) + if pcm.size == 0: + raise TtsError("ElevenLabs returned no audio") + return pcm, config.TTS_SAMPLE_RATE + + +def stream_pcm(text: str, chunk_bytes: int = 4096) -> Iterator[np.ndarray]: + """Same audio as synthesize_pcm(), but yielded as it arrives from + ElevenLabs' /stream endpoint so playback can start on the first chunk + (~300ms) instead of after the whole clip is synthesized. Raises TtsError + before yielding anything if the request itself fails, so callers can fall + back cleanly; a mid-stream failure just ends the generator.""" + if not (config.ELEVENLABS_API_KEY and config.ELEVENLABS_VOICE_ID): + raise TtsError("ELEVENLABS_API_KEY / ELEVENLABS_VOICE_ID not set") + try: + response = requests.post( + f"https://api.elevenlabs.io/v1/text-to-speech/{config.ELEVENLABS_VOICE_ID}/stream", + headers={"xi-api-key": config.ELEVENLABS_API_KEY}, + params={"output_format": f"pcm_{config.TTS_SAMPLE_RATE}"}, + json={"text": text, "model_id": config.ELEVENLABS_MODEL_ID}, + timeout=60, + stream=True, + ) + response.raise_for_status() + except Exception as exc: + raise TtsError(f"ElevenLabs stream request failed: {exc}") from exc + return chunks_to_int16(response.iter_content(chunk_size=chunk_bytes)) + + +def chunks_to_int16(byte_chunks: Iterable[bytes]) -> Iterator[np.ndarray]: + """Reassemble a byte stream into int16 frames. HTTP chunk boundaries fall + wherever they like, including *inside* a 16-bit sample, so a trailing odd + byte has to be carried into the next chunk — otherwise every chunk after + the first is shifted by one byte and plays as static.""" + carry = b"" + for chunk in byte_chunks: + if not chunk: + continue + data = carry + chunk + usable = len(data) - (len(data) % 2) + carry = data[usable:] + if usable: + yield np.frombuffer(data[:usable], dtype=np.int16) + + +def play_pcm(pcm: np.ndarray, sample_rate: int, blocking: bool = True, should_stop=None) -> bool: + """Play a whole clip. Returns True if it finished, False if *should_stop* + (barge-in) cut it short. *should_stop* is polled while audio plays — each + poll consumes one mic frame, which is what paces this loop.""" + import sounddevice as sd + + sd.play(pcm, samplerate=sample_rate, device=config.SPEAKER_DEVICE) + if not blocking: + return True + if should_stop is None: + sd.wait() + return True + while True: + try: + if not sd.get_stream().active: + break + except Exception: + break # stream already torn down — playback is over + if should_stop(): + sd.stop() + return False + return True + + +def play_stream(chunks: Iterable[np.ndarray], sample_rate: int, should_stop=None) -> bool: + """Play int16 chunks as they arrive. Returns False if interrupted.""" + import sounddevice as sd + + with sd.OutputStream( + samplerate=sample_rate, channels=1, dtype="int16", device=config.SPEAKER_DEVICE + ) as out: + for chunk in chunks: + if should_stop is not None and should_stop(): + # abort() rather than draining: barge-in should stop the voice + # now, not at the end of the buffered chunk. + out.abort() + return False + out.write(chunk) + return True + + +def speak_offline(text: str) -> None: + try: + import pyttsx3 + except ImportError: + return # no TTS available at all — caller already logs the text + engine = pyttsx3.init() + engine.say(text) + engine.runAndWait() + + +def speak(text: str, on_error=None, should_stop=None) -> bool: + """Speak *text*, preferring streaming ElevenLabs, then whole-clip + ElevenLabs, then offline TTS. *on_error*, if given, is called with the + exception when ElevenLabs fails (useful for logging) — a fallback still + runs either way. Returns False if barge-in interrupted playback. + + The text is sanitized first (speech_text.for_speech): server replies are + written for a chat window, and a voice reads markdown/emoji literally + ("asterisk asterisk"). Sanitizing here rather than at the call sites means + every path to the speakers — reply, heartbeat announcement — is covered.""" + text = speech_text.for_speech(text) + if not text: + return True + if config.TTS_STREAMING: + try: + return play_stream(stream_pcm(text), config.TTS_SAMPLE_RATE, should_stop=should_stop) + except TtsError as exc: + if on_error is not None: + on_error(exc) + try: + pcm, sample_rate = synthesize_pcm(text) + return play_pcm(pcm, sample_rate, should_stop=should_stop) + except TtsError as exc: + if on_error is not None: + on_error(exc) + speak_offline(text) + return True diff --git a/bolt_pet/audio/wake_word.py b/bolt_pet/audio/wake_word.py new file mode 100644 index 0000000..7fb82d0 --- /dev/null +++ b/bolt_pet/audio/wake_word.py @@ -0,0 +1,185 @@ +"""Wake-word detection via a custom-trained openWakeWord model. + +Uses `thunderbolt.onnx` — trained specifically for "thunderbolt", the same +way the main repo's `desk_client/bolt_desk.py` uses `bolt.onnx` for "hey +bolt". Same runtime (openWakeWord, ONNX inference), same per-frame +predict()/reset() pattern; the only difference is the model file +(WAKE_MODEL_FILE) and threshold (WAKE_WORD_THRESHOLD), both configurable via +.env if a differently-trained model is swapped in later. +""" + +from __future__ import annotations + +from collections import deque +from typing import Callable, Optional, Protocol, Union + +import numpy as np + +from .. import config + + +class WakeModel(Protocol): + def predict(self, frame: np.ndarray) -> dict: ... + def reset(self) -> None: ... + + +class NearMissLog: + """Rolling record of frames that *almost* fired the wake word. + + WAKE_WORD_THRESHOLD is otherwise tuned by guessing at a number in .env + and seeing whether the pet ignores you. Keeping the near misses (scores + within WAKE_NEAR_MISS_MARGIN below the threshold) turns that into + evidence: the tray's wake-word tuner shows what your actual "thunderbolt" + scores, so you can set the threshold just under it. + + Pure bookkeeping — the caller supplies timestamps, so it's testable. + """ + + def __init__(self, limit: int = None, margin: float = None): + self._entries: deque[tuple[float, float, float]] = deque( # (timestamp, score, threshold) + maxlen=max(1, config.WAKE_NEAR_MISS_LIMIT if limit is None else limit) + ) + self._margin = config.WAKE_NEAR_MISS_MARGIN if margin is None else margin + self._peak = 0.0 + + @property + def peak(self) -> float: + """Highest score seen since the last reset — the "how close did I + get?" readout while you test the wake phrase.""" + return self._peak + + def observe(self, score: float, threshold: float, timestamp: float) -> bool: + """Record *score*; returns True if it counted as a near miss.""" + self._peak = max(self._peak, score) + if score >= threshold or score < threshold - self._margin: + return False + self._entries.append((timestamp, score, threshold)) + return True + + def entries(self) -> list[tuple[float, float, float]]: + return list(self._entries) + + def clear(self) -> None: + self._entries.clear() + self._peak = 0.0 + + +def _construct_model(model_cls, model_path: str): + """openwakeword's Model() constructor keyword has drifted across + releases (wakeword_models -> wakeword_model_paths) and some builds + reject inference_framework entirely — the main repo's ai/wake_word.py + hit the same drift and works around it the same way: try each known + calling convention in turn.""" + attempts = [ + lambda: model_cls(wakeword_model_paths=[model_path], inference_framework="onnx"), + lambda: model_cls(wakeword_model_paths=[model_path]), + lambda: model_cls(wakeword_models=[model_path], inference_framework="onnx"), + lambda: model_cls(wakeword_models=[model_path]), + lambda: model_cls([model_path]), + ] + last_exc: Optional[TypeError] = None + for attempt in attempts: + try: + return attempt() + except TypeError as exc: + last_exc = exc + raise RuntimeError( + f"Could not construct openwakeword.Model with any known calling convention " + f"(last error: {last_exc})" + ) + + +class _OpenWakeWordModel: + """Lazily loads the ONNX model on first use so importing this module + (and unit-testing listen_for_wake_word with a fake model) never requires + onnxruntime/openwakeword or the model file to be present.""" + + def __init__(self): + self._model = None + + def _ensure_model(self): + if self._model is None: + from openwakeword.model import Model + # from openwakeword.utils import download_models + + # # The pip package doesn't bundle its melspectrogram/embedding + # # feature-extraction sub-models — fetch them once on first use + # # (no-op if already cached in openwakeword's own resources dir). + # # A non-empty, non-matching model_names list keeps this from + # # also pulling every official pretrained wakeword model. + # download_models(model_names=["thunderbolt"]) + + self._model = _construct_model(Model, config.WAKE_MODEL_PATH) + return self._model + + def predict(self, frame: np.ndarray) -> dict: + return self._ensure_model().predict(frame) + + def reset(self) -> None: + if self._model is not None: + self._model.reset() + + +_default_model = _OpenWakeWordModel() + + +def listen_for_wake_word( + stream, + should_continue=lambda: True, + model: Optional[WakeModel] = None, + threshold: Union[float, Callable[[], float], None] = None, + on_tick=None, + on_score: Optional[Callable[[float, float], None]] = None, +) -> bool: + """Block until the wake word fires (returns True) or *should_continue* + goes false (returns False). + + Feeds every frame to *model* (the thunderbolt openWakeWord model by + default) and treats any class score >= *threshold* as a detection, + resetting the model's internal state afterward so the next call starts + clean — same pattern as desk_client/bolt_desk.py's main loop. + + *threshold* may be a number or a zero-argument callable. The callable + form exists because this function blocks for minutes at a time: the + tray's wake-word tuner slider has to be able to change sensitivity + *during* a listen, not only at the start of the next one. + + *on_tick*, if given, is called once per ``WAKE_CHECK_INTERVAL_SECONDS`` + (not every frame — prediction is cheap enough to run on every frame, but + this is the only point control returns to the caller while otherwise + blocked here for a possibly long time, so it's how a caller drives + periodic work, e.g. the heartbeat/announcement poll in controller.py, + during quiet stretches with no wake word). + + *on_score*, if given, gets ``(best_score, threshold)`` every frame — used + to log near misses for threshold tuning. + """ + model = model or _default_model + if threshold is None: + threshold = config.WAKE_WORD_THRESHOLD + resolve_threshold = threshold if callable(threshold) else (lambda: threshold) + + frame_len = config.FRAME_LEN + check_every_frames = max(1, int(config.WAKE_CHECK_INTERVAL_SECONDS * config.SAMPLE_RATE / frame_len)) + frames_since_tick = 0 + + while should_continue(): + chunk, _ = stream.read(frame_len) + frame = np.asarray(chunk)[:, 0] + scores = model.predict(frame) + current_threshold = resolve_threshold() + best = max(scores.values()) if scores else 0.0 + + frames_since_tick += 1 + if frames_since_tick >= check_every_frames: + frames_since_tick = 0 + if on_tick is not None: + on_tick() + + if on_score is not None: + on_score(best, current_threshold) + + if scores and best >= current_threshold: + model.reset() + return True + return False diff --git a/bolt_pet/config.py b/bolt_pet/config.py new file mode 100644 index 0000000..2090e94 --- /dev/null +++ b/bolt_pet/config.py @@ -0,0 +1,192 @@ +"""Environment configuration for the Bolt desktop pet. + +Same lightweight ".env next to the script" pattern as desk_client/bolt_desk.py +in the main tmn-api repo, so this project can be copied anywhere (it does not +import anything from that repo) and configured the same way. +""" + +from __future__ import annotations + +import os +import platform +from pathlib import Path + +HERE = Path(__file__).resolve().parent.parent # project root (one above bolt_pet/) + + +def _load_env() -> None: + env_path = HERE / ".env" + if not env_path.exists(): + return + for line in env_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + os.environ.setdefault(key.strip(), value.strip()) + + +_load_env() + + +def _node_name() -> str: + try: + return platform.node() or "desktop" + except Exception: + return "desktop" + + +# ── server / identity ─────────────────────────────────────────────────────── + +SERVER_URL = os.environ.get("BOLT_SERVER_URL", "").rstrip("/") +API_KEY = os.environ.get("DESK_API_KEY", "") +SESSION_ID = os.environ.get("DESK_SESSION_ID", "pet-" + _node_name()) + +# ── wake word ──────────────────────────────────────────────────────────────── +# openWakeWord model trained specifically for "thunderbolt" — same pattern as +# desk_client/bolt_desk.py's WAKE_MODEL_FILE (bolt.onnx / "hey bolt") in the +# main repo, resolved relative to the project root so it sits next to .env. + +WAKE_MODEL_PATH = str(HERE / os.environ.get("WAKE_MODEL_FILE", "thunderbolt.onnx")) +WAKE_WORD_THRESHOLD = float(os.environ.get("WAKE_WORD_THRESHOLD", "0.5")) +# How often (independent of prediction, which runs every frame) the wake +# listener yields control back to its caller via on_tick — e.g. the +# heartbeat poll in controller.py during quiet stretches with no wake word. +WAKE_CHECK_INTERVAL_SECONDS = float(os.environ.get("WAKE_CHECK_INTERVAL_SECONDS", "1.2")) + +# ── STT (Deepgram, same as bolt_desk.py) ──────────────────────────────────── + +DEEPGRAM_API_KEY = os.environ.get("DEEPGRAM_API_KEY", "") +DEEPGRAM_MODEL = os.environ.get("DEEPGRAM_MODEL", "nova-3") + +# ── TTS (ElevenLabs, requested as raw PCM so playback needs no external +# player binary — cross-platform via sounddevice instead of shelling out to +# mpv/ffplay like bolt_desk.py does on Linux) ─────────────────────────────── + +ELEVENLABS_API_KEY = os.environ.get("ELEVENLABS_API_KEY", "") +ELEVENLABS_VOICE_ID = os.environ.get("ELEVENLABS_VOICE_ID", "") +ELEVENLABS_MODEL_ID = os.environ.get("ELEVENLABS_MODEL_ID", "eleven_flash_v2") +# ElevenLabs PCM output formats are named pcm_. +TTS_SAMPLE_RATE = int(os.environ.get("TTS_SAMPLE_RATE", "24000")) + +# ── mic / VAD (same tuning knobs as bolt_desk.py) ─────────────────────────── + +MIC_DEVICE = os.environ.get("MIC_DEVICE", "") or None # sounddevice name/index +SPEAKER_DEVICE = os.environ.get("SPEAKER_DEVICE", "") or None + +RMS_THRESHOLD = int(os.environ.get("VAD_RMS_THRESHOLD", "300")) +SILENCE_END_SEC = float(os.environ.get("VAD_SILENCE_END_SEC", "1.2")) +MAX_UTTERANCE_S = float(os.environ.get("VAD_MAX_UTTERANCE_SECONDS", "15")) +MIN_UTTERANCE_S = float(os.environ.get("VAD_MIN_UTTERANCE_SECONDS", "0.4")) + +COMMAND_TIMEOUT_SECONDS = int(os.environ.get("COMMAND_TIMEOUT_SECONDS", "30")) +HEARTBEAT_INTERVAL_SECONDS = float(os.environ.get("HEARTBEAT_INTERVAL_SECONDS", "60")) + +# ── barge-in (interrupt playback by talking over it) ──────────────────────── +# The mic stays live while the pet talks; sustained loud frames cut playback +# short. The threshold is deliberately well above VAD_RMS_THRESHOLD because +# the mic also hears the pet's own voice through the speakers — raise it +# further (or set BARGE_IN=false) if playback keeps interrupting itself. + +BARGE_IN = os.environ.get("BARGE_IN", "true").lower() in ("1", "true", "yes", "on") +BARGE_IN_RMS_THRESHOLD = int(os.environ.get("BARGE_IN_RMS_THRESHOLD", str(RMS_THRESHOLD * 4))) +BARGE_IN_FRAMES = int(os.environ.get("BARGE_IN_FRAMES", "4")) # consecutive loud frames (80ms each) + +# ── streaming TTS ─────────────────────────────────────────────────────────── +# ElevenLabs' /stream endpoint + chunked playback: the pet starts talking +# after the first PCM chunk instead of after the whole clip is synthesized. + +TTS_STREAMING = os.environ.get("TTS_STREAMING", "true").lower() in ("1", "true", "yes", "on") + +# ── screen context ────────────────────────────────────────────────────────── +# Appends the active window's title to what you say, so "what's this error?" +# has a referent. The desk API takes text only, so this is a text annotation +# (no screenshot upload). + +SCREEN_CONTEXT = os.environ.get("SCREEN_CONTEXT", "true").lower() in ("1", "true", "yes", "on") + +# ── quiet hours / do-not-disturb ──────────────────────────────────────────── +# Comma-separated HH:MM-HH:MM ranges (wrapping midnight is fine). While +# napping the pet dims, stops wandering, and makes no proactive noise — +# wake word and click-to-talk still work. + +QUIET_HOURS = os.environ.get("QUIET_HOURS", "") +DND_ON_FULLSCREEN = os.environ.get("DND_ON_FULLSCREEN", "true").lower() in ("1", "true", "yes", "on") + +# ── desktop notification bridge ───────────────────────────────────────────── +# Mirrors desktop notifications to the server so Bolt can react to them. +# Off by default: every forwarded notification costs a converse() round trip. + +NOTIFICATION_BRIDGE = os.environ.get("NOTIFICATION_BRIDGE", "false").lower() in ("1", "true", "yes", "on") +# Regex matched against ": "; empty means "everything". +NOTIFICATION_FILTER = os.environ.get("NOTIFICATION_FILTER", "") +NOTIFICATION_MIN_INTERVAL_SECONDS = float(os.environ.get("NOTIFICATION_MIN_INTERVAL_SECONDS", "60")) + +# ── conversation history ──────────────────────────────────────────────────── + +HISTORY_LIMIT = int(os.environ.get("HISTORY_LIMIT", "100")) + +# ── push-to-talk ──────────────────────────────────────────────────────────── +# Global hotkey (needs `pynput`; unavailable on most Wayland sessions, in +# which case it logs once and the wake word / tray still work). Empty to +# disable. + +PUSH_TO_TALK_HOTKEY = os.environ.get("PUSH_TO_TALK_HOTKEY", "ctrl+alt+space") + +# ── wake-word tuning ──────────────────────────────────────────────────────── +# Scores this far below the threshold are recorded as "near misses" and shown +# in the tray's wake-word tuner, so the threshold can be set from evidence. + +WAKE_NEAR_MISS_MARGIN = float(os.environ.get("WAKE_NEAR_MISS_MARGIN", "0.2")) +WAKE_NEAR_MISS_LIMIT = int(os.environ.get("WAKE_NEAR_MISS_LIMIT", "40")) + +SAMPLE_RATE = 16000 # mic capture / STT rate +FRAME_LEN = 1280 # 80ms @ 16kHz — matches bolt_desk.py's chunking + +# ── pet window ─────────────────────────────────────────────────────────────── + +PET_SIZE = int(os.environ.get("PET_SIZE", "160")) # on-screen pixel size (square) +PET_START_X = os.environ.get("PET_START_X", "") # blank = bottom-right of primary screen +PET_START_Y = os.environ.get("PET_START_Y", "") +PET_ALWAYS_ON_TOP = os.environ.get("PET_ALWAYS_ON_TOP", "true").lower() in ("1", "true", "yes", "on") +IDLE_ANIMATION_FPS = float(os.environ.get("IDLE_ANIMATION_FPS", "6")) + +# ── wandering ──────────────────────────────────────────────────────────────── +# The pet strolls to a random spot on its own while idle. Only ever moves when +# it's IDLE and not speaking/being dragged, so it never walks out from under a +# speech bubble mid-sentence. + +PET_WANDER = os.environ.get("PET_WANDER", "true").lower() in ("1", "true", "yes", "on") +# Average seconds of standing still between strolls (each wait is randomized +# to 0.5x–1.5x this, so the pet doesn't move on an obvious metronome). +PET_WANDER_INTERVAL_SECONDS = float(os.environ.get("PET_WANDER_INTERVAL_SECONDS", "45")) +PET_WANDER_SPEED = float(os.environ.get("PET_WANDER_SPEED", "90")) # pixels/second +# Cap on how far one stroll can be, so it doesn't teleport across a 4K screen. +PET_WANDER_MAX_DISTANCE = float(os.environ.get("PET_WANDER_MAX_DISTANCE", "600")) +PET_WANDER_MARGIN = int(os.environ.get("PET_WANDER_MARGIN", "20")) # keep off screen edges + +# ── mouse behaviour ───────────────────────────────────────────────────────── +# The sprite is drawn in a square translucent window, so its transparent +# corners would otherwise swallow clicks meant for whatever is underneath. +# PET_SHAPED_INPUT masks the window's input region to the sprite's own opaque +# pixels; PET_CLICK_THROUGH goes further and makes the whole pet ignore the +# mouse (tray-only control until you turn it back off). + +PET_SHAPED_INPUT = os.environ.get("PET_SHAPED_INPUT", "true").lower() in ("1", "true", "yes", "on") +PET_CLICK_THROUGH = os.environ.get("PET_CLICK_THROUGH", "false").lower() in ("1", "true", "yes", "on") +# Snap flush to a screen edge when dropped/parked within this many pixels of it. +PET_EDGE_SNAP = os.environ.get("PET_EDGE_SNAP", "true").lower() in ("1", "true", "yes", "on") +PET_SNAP_MARGIN = int(os.environ.get("PET_SNAP_MARGIN", "48")) + + +def is_configured() -> bool: + return bool(SERVER_URL and API_KEY) + + +def missing_config() -> list[str]: + missing = [] + if not SERVER_URL: + missing.append("BOLT_SERVER_URL") + if not API_KEY: + missing.append("DESK_API_KEY") + return missing diff --git a/bolt_pet/controller.py b/bolt_pet/controller.py new file mode 100644 index 0000000..75dc008 --- /dev/null +++ b/bolt_pet/controller.py @@ -0,0 +1,348 @@ +"""Orchestrates the pet's mic -> wake word -> STT -> server -> TTS pipeline. + +Runs on a background QThread (see ui/app.py) so the Qt event loop / window +painting is never blocked by audio I/O or network calls. Talks to the UI +only through Qt signals (state_changed / said / log / action / napping), +which Qt marshals safely across threads — this class never touches a QWidget +directly. + +Beyond the core loop it owns the side channels that let the pet act on its +own: the heartbeat (proactive announcements), the desktop notification +bridge, quiet hours, barge-in, and the live wake-word threshold. +""" + +from __future__ import annotations + +import threading +import time +from typing import Optional + +from PySide6.QtCore import QObject, Signal + +from . import config, history as history_mod, notifications, pet_actions, quiet, screen_context, server_client, speech_text +from .audio import barge_in, mic, stt, tts, wake_word +from .state import PetState, PetStateMachine + +# How often to re-check whether the pet should be napping. The fullscreen +# probe shells out to xprop, so this deliberately isn't every heartbeat tick. +_NAP_CHECK_INTERVAL_SECONDS = 10.0 + + +class PetController(QObject): + state_changed = Signal(str) # PetState.value + said = Signal(str) # text now showing in the speech bubble + log = Signal(str) + action = Signal(dict) # parsed petctl action for the UI to perform + napping = Signal(bool) # quiet hours / fullscreen do-not-disturb + finished = Signal() + + def __init__(self): + super().__init__() + self._running = True + self._muted = False + self._talk_now = threading.Event() + self._stream = None + self._last_heartbeat = 0.0 + self._state = PetStateMachine(on_change=self._handle_state_change) + + # Conversation scrollback, shared read-only with the UI's History + # window. Append-only from this thread; the UI only ever snapshots it. + self.history = history_mod.ConversationHistory(limit=config.HISTORY_LIMIT) + + # Wake-word sensitivity is live-tunable (tray tuner), so it's read + # through a callable on every frame rather than captured per listen. + self._wake_threshold = config.WAKE_WORD_THRESHOLD + self._near_misses = wake_word.NearMissLog() + + self._barge_in: Optional[barge_in.BargeInDetector] = None + self._napping = False + self._nap_forced: Optional[bool] = None # petctl nap on/off overrides the schedule + self._last_nap_check = 0.0 + + self._notification_watcher: Optional[notifications.NotificationWatcher] = None + self._notification_gate = notifications.NotificationGate( + config.NOTIFICATION_FILTER, config.NOTIFICATION_MIN_INTERVAL_SECONDS + ) + self._pending_notifications: list[notifications.Notification] = [] + self._notification_lock = threading.Lock() + + # ── external controls (safe to call from the Qt/UI thread) ───────── + + def request_talk_now(self) -> None: + self._talk_now.set() + + def toggle_mute(self) -> bool: + self._muted = not self._muted + self.log.emit("Muted." if self._muted else "Unmuted.") + return self._muted + + def set_napping(self, napping: Optional[bool]) -> None: + """Force the nap state on/off, or pass None to hand control back to + the quiet-hours schedule.""" + self._nap_forced = napping + if napping is not None: + self._apply_nap_state(napping) + + def wake_threshold(self) -> float: + return self._wake_threshold + + def set_wake_threshold(self, value: float) -> None: + self._wake_threshold = min(max(float(value), 0.01), 0.99) + + def wake_stats(self) -> dict: + return {"peak": self._near_misses.peak, "near_misses": self._near_misses.entries()} + + def reset_wake_stats(self) -> None: + self._near_misses.clear() + + def stop(self) -> None: + self._running = False + self._talk_now.set() # wake up anything blocked waiting on it + if self._notification_watcher is not None: + self._notification_watcher.stop() + + # ── internal ───────────────────────────────────────────────────────── + + def _handle_state_change(self, _old: PetState, new: PetState) -> None: + self.state_changed.emit(new.value) + + def _should_continue(self) -> bool: + return self._running + + def run(self) -> None: + """Thread entry point (connected to QThread.started).""" + missing = config.missing_config() + if missing: + self.log.emit(f"Missing config: {', '.join(missing)} — set them in .env and restart.") + self.finished.emit() + return + try: + self._stream = mic.open_input_stream() + except Exception as exc: + self.log.emit(f"Could not open microphone: {exc}") + self.finished.emit() + return + + if config.BARGE_IN: + self._barge_in = barge_in.BargeInDetector(self._stream) + + with self._stream: + try: + health = server_client.check_health() + self.log.emit(f"Connected to server: {health}") + except Exception as exc: + self.log.emit(f"Server not reachable yet ({exc}) — will keep trying per-request.") + self._start_notification_bridge() + self._loop() + if self._notification_watcher is not None: + self._notification_watcher.stop() + self.finished.emit() + + def _loop(self) -> None: + while self._running: + if self._muted: + triggered = self._talk_now.wait(timeout=0.5) + if not self._running: + return + if not triggered: + continue + self._talk_now.clear() + else: + if not self._wait_for_wake_or_click(): + if not self._running: + return + continue + self._handle_conversation_turn() + + def _wait_for_wake_or_click(self) -> bool: + """True once either the wake phrase was heard or a click-to-talk + request came in; False on a spurious wakeup (loop again).""" + def should_continue() -> bool: + return self._running and not self._talk_now.is_set() + + detected = wake_word.listen_for_wake_word( + self._stream, + should_continue=should_continue, + threshold=self.wake_threshold, # callable: the tuner slider is live + on_tick=self._maybe_heartbeat, + on_score=self._observe_wake_score, + ) + if not self._running: + return False + if detected: + self._talk_now.clear() # in case both fired around the same time + return True + if self._talk_now.is_set(): + self._talk_now.clear() + return True + return False + + def _observe_wake_score(self, score: float, threshold: float) -> None: + self._near_misses.observe(score, threshold, time.time()) + + def _handle_conversation_turn(self) -> None: + self._state.transition(PetState.LISTENING) + pcm = mic.record_utterance(self._stream, should_continue=self._should_continue) + if pcm is None: + self._state.transition(PetState.IDLE) + return + + self._state.transition(PetState.THINKING) + try: + text = stt.transcribe(pcm) + except stt.SttError as exc: + self.log.emit(f"STT failed: {exc}") + self._state.transition(PetState.ERROR) + self._state.transition(PetState.IDLE) + return + if not text: + self._state.transition(PetState.IDLE) + return + self.log.emit(f"You: {text}") + self.history.add(history_mod.USER, text, time.time()) + + try: + # What's focused right now rides along, so "what's this error?" + # has a referent without you having to describe the window. + reply = server_client.converse( + screen_context.context_for(text), on_command=self._handle_command + ) + except server_client.ServerError as exc: + self.log.emit(f"Server error: {exc}") + self._state.transition(PetState.ERROR) + self._state.transition(PetState.IDLE) + return + + self._speak(reply) + self._state.transition(PetState.IDLE) + + def _handle_command(self, command: str) -> str: + """Server-relayed command. `petctl ...` drives the pet's body and + never reaches a shell; everything else is a real command, exactly as + before (see the security notes in the README).""" + try: + action = pet_actions.parse(command) + except pet_actions.ActionError as exc: + self.log.emit(f"petctl: {exc}") + return f"[pet] {exc}" + if action is None: + return server_client.run_local_command(command) + self.log.emit(f"Pet action: {action}") + if action["action"] == "nap": + self.set_napping(bool(action["enabled"])) + self.action.emit(action) + return pet_actions.describe(action) + + def _speak(self, text: str) -> None: + self._state.transition(PetState.TALKING) + # Bubble gets the markdown stripped but emoji kept (it can't render + # **bold** but draws emoji fine); tts.speak() does its own, stricter + # sanitizing for the voice. + self.said.emit(speech_text.for_display(text)) + self.log.emit(f"Bolt: {text}") + self.history.add(history_mod.PET, text, time.time()) + + should_stop = None + if self._barge_in is not None: + self._barge_in.reset() + should_stop = self._barge_in.check + completed = tts.speak( + text, + on_error=lambda exc: self.log.emit(f"TTS failed: {exc}"), + should_stop=should_stop, + ) + if not completed: + # You talked over it — take that as the start of the next turn + # rather than making you say the wake word again. + self.log.emit("Interrupted — listening.") + self._talk_now.set() + + # ── quiet hours / do-not-disturb ───────────────────────────────────── + + def _apply_nap_state(self, napping: bool) -> None: + if napping == self._napping: + return + self._napping = napping + self.log.emit("Napping — no proactive noise." if napping else "Awake.") + self.napping.emit(napping) + + def _refresh_nap_state(self) -> None: + if self._nap_forced is not None: + self._apply_nap_state(self._nap_forced) + return + now = time.monotonic() + if now - self._last_nap_check < _NAP_CHECK_INTERVAL_SECONDS: + return + self._last_nap_check = now + napping = quiet.is_quiet( + config.QUIET_HOURS, + on_error=lambda exc: self.log.emit(f"QUIET_HOURS is malformed ({exc}) — ignoring it."), + ) + if not napping and config.DND_ON_FULLSCREEN: + napping = screen_context.is_fullscreen_active() + self._apply_nap_state(napping) + + # ── desktop notification bridge ────────────────────────────────────── + + def _start_notification_bridge(self) -> None: + if not config.NOTIFICATION_BRIDGE: + return + watcher = notifications.NotificationWatcher(self._queue_notification) + problem = watcher.start() + if problem: + self.log.emit(problem) + return + self._notification_watcher = watcher + self.log.emit("Notification bridge on.") + + def _queue_notification(self, notification: notifications.Notification) -> None: + """Called on the watcher thread — just queue it; forwarding happens on + the pipeline thread where it can't collide with a live conversation.""" + if not self._notification_gate.should_forward(notification, time.monotonic()): + return + with self._notification_lock: + self._pending_notifications.append(notification) + + def _drain_notifications(self) -> None: + with self._notification_lock: + pending, self._pending_notifications = self._pending_notifications, [] + for notification in pending: + if not self._running or self._napping: + return + self.log.emit(f"Notification: {notification.as_text()}") + self.history.add(history_mod.SYSTEM, notification.as_text(), time.time()) + try: + reply = server_client.converse( + f"[desktop notification] {notification.as_text()}", + on_command=self._handle_command, + ) + except server_client.ServerError as exc: + self.log.emit(f"Couldn't forward notification: {exc}") + return + if reply.strip(): + self._speak(reply) + self._state.transition(PetState.IDLE) + + # ── heartbeat ──────────────────────────────────────────────────────── + + def _maybe_heartbeat(self) -> None: + self._refresh_nap_state() + now = time.monotonic() + if now - self._last_heartbeat < config.HEARTBEAT_INTERVAL_SECONDS: + return + self._last_heartbeat = now + if self._state.state != PetState.IDLE: + return + if self._napping: + return # quiet hours: still answers when spoken to, just doesn't start + self._drain_notifications() + if self._state.state != PetState.IDLE: + return + try: + announcement = server_client.report_status() + except server_client.ServerError as exc: + self.log.emit(f"Heartbeat failed: {exc}") + return + if announcement: + self._speak(announcement) + self._state.transition(PetState.IDLE) diff --git a/bolt_pet/history.py b/bolt_pet/history.py new file mode 100644 index 0000000..30177b1 --- /dev/null +++ b/bolt_pet/history.py @@ -0,0 +1,62 @@ +"""Rolling transcript of the conversation. + +The speech bubble hides itself after a few seconds, which is fine for chat +but bad for anything you actually needed to read (a command's output, a +number, a URL). This keeps the last HISTORY_LIMIT turns so the tray's +History window — and click-to-copy on the bubble — have something to show. + +Pure logic, no Qt: the UI half is ui/history_window.py. +""" + +from __future__ import annotations + +from collections import deque +from dataclasses import dataclass +from typing import Iterable, Optional + +USER = "you" +PET = "bolt" +SYSTEM = "system" + + +@dataclass(frozen=True) +class Entry: + role: str + text: str + timestamp: Optional[float] = None # time.time(); None when not recorded + + def formatted(self, clock=None) -> str: + label = {USER: "You", PET: "Bolt", SYSTEM: "—"}.get(self.role, self.role) + stamp = clock(self.timestamp) if (clock and self.timestamp) else None + return f"[{stamp}] {label}: {self.text}" if stamp else f"{label}: {self.text}" + + +class ConversationHistory: + def __init__(self, limit: int = 100): + self._entries: deque[Entry] = deque(maxlen=max(1, limit)) + + def add(self, role: str, text: str, timestamp: Optional[float] = None) -> Optional[Entry]: + text = (text or "").strip() + if not text: + return None + entry = Entry(role=role, text=text, timestamp=timestamp) + self._entries.append(entry) + return entry + + def entries(self) -> list[Entry]: + return list(self._entries) + + def last(self, role: Optional[str] = None) -> Optional[Entry]: + for entry in reversed(self._entries): + if role is None or entry.role == role: + return entry + return None + + def clear(self) -> None: + self._entries.clear() + + def as_text(self, clock=None, entries: Optional[Iterable[Entry]] = None) -> str: + return "\n".join(e.formatted(clock) for e in (entries if entries is not None else self._entries)) + + def __len__(self) -> int: + return len(self._entries) diff --git a/bolt_pet/hotkey.py b/bolt_pet/hotkey.py new file mode 100644 index 0000000..9c45f36 --- /dev/null +++ b/bolt_pet/hotkey.py @@ -0,0 +1,105 @@ +"""Global push-to-talk hotkey. + +The wake word is the primary trigger, but it misfires in a noisy room and +won't fire at all if you're on a call — so there's a keyboard fallback that +works even when the pet has no focus (it's a frameless Qt.Tool window with no +taskbar entry, so an ordinary QShortcut would never see the key). + +Needs `pynput`, which needs an X11/Win32/macOS input hook: on most Wayland +sessions it can't grab global keys, and on macOS it needs Accessibility +permission. All of that is a soft failure — start() reports the reason and +the wake word / tray keep working. +""" + +from __future__ import annotations + +from typing import Callable, Optional + +# Aliases for the names people actually type in a .env file. +_ALIASES = { + "control": "ctrl", + "ctl": "ctrl", + "option": "alt", + "opt": "alt", + "win": "cmd", + "windows": "cmd", + "super": "cmd", + "meta": "cmd", + "command": "cmd", + "return": "enter", + "escape": "esc", + "del": "delete", + "ins": "insert", + "pgup": "page_up", + "pgdn": "page_down", +} + + +class HotkeyError(Exception): + pass + + +def to_pynput_spec(spec: str) -> str: + """"ctrl+alt+space" -> "++" (pynput's GlobalHotKeys + syntax: named keys in angle brackets, literal characters bare).""" + tokens = [t.strip().lower() for t in (spec or "").split("+")] + tokens = [t for t in tokens if t] + if not tokens: + raise HotkeyError("empty hotkey") + parts = [] + for token in tokens: + token = _ALIASES.get(token, token) + parts.append(token if len(token) == 1 else f"<{token}>") + return "+".join(parts) + + +class GlobalHotkey: + """Fires *callback* whenever the hotkey is pressed, anywhere. Safe to + construct unconditionally — nothing happens until start(), and start() + reports failure instead of raising into the UI thread.""" + + def __init__(self, spec: str, callback: Callable[[], None]): + self.spec = spec + self._callback = callback + self._listener = None + + @property + def running(self) -> bool: + return self._listener is not None + + def start(self) -> Optional[str]: + """None on success, otherwise a human-readable reason it's off.""" + if not (self.spec or "").strip(): + return None # explicitly disabled — not an error worth reporting + try: + pynput_spec = to_pynput_spec(self.spec) + except HotkeyError as exc: + return f"push-to-talk hotkey {self.spec!r} is invalid: {exc}" + try: + from pynput import keyboard + except Exception as exc: # ImportError, or a backend that won't load + return f"push-to-talk needs pynput ({exc}) — wake word still works" + try: + listener = keyboard.GlobalHotKeys({pynput_spec: self._safe_callback}) + listener.daemon = True + listener.start() + except Exception as exc: + return f"push-to-talk unavailable on this session ({exc}) — wake word still works" + self._listener = listener + return None + + def _safe_callback(self) -> None: + # This runs on pynput's listener thread; an exception there would + # silently kill the listener for the rest of the session. + try: + self._callback() + except Exception: + pass + + def stop(self) -> None: + if self._listener is not None: + try: + self._listener.stop() + except Exception: + pass + self._listener = None diff --git a/bolt_pet/notifications.py b/bolt_pet/notifications.py new file mode 100644 index 0000000..6ad4a6d --- /dev/null +++ b/bolt_pet/notifications.py @@ -0,0 +1,166 @@ +"""Desktop notification bridge (Linux/D-Bus). + +Lets Bolt react to things that happen without you: a build finishing, a +calendar alert, a message arriving. Notifications are tailed from +`dbus-monitor`, filtered, rate-limited, and handed to the controller, which +forwards them through the normal converse() path — so the pet can say +"your deploy just went green" instead of only ever answering questions. + +Off by default (NOTIFICATION_BRIDGE): every forwarded notification is a +round trip to the server, and an unfiltered desktop can be very chatty. +NOTIFICATION_FILTER (a regex) is the main knob for keeping it useful. + +The dbus-monitor *parsing* is pure and unit tested; only the subprocess +plumbing needs a real session bus. +""" + +from __future__ import annotations + +import platform +import re +import shutil +import subprocess +import threading +from dataclasses import dataclass +from typing import Callable, Iterable, Iterator, Optional + +_STRING_LINE = re.compile(r'^\s*string\s+"(.*)"\s*$') +_BLOCK_START = re.compile(r"^(method call|signal|method return|error)\b") + + +@dataclass(frozen=True) +class Notification: + app: str + summary: str + body: str + + def as_text(self) -> str: + parts = [p for p in (self.summary, self.body) if p] + joined = " — ".join(parts) + return f"{self.app}: {joined}" if self.app else joined + + +def iter_notifications(lines: Iterable[str]) -> Iterator[Notification]: + """Pull Notification records out of a `dbus-monitor` line stream. + + A Notify call prints its arguments one per line after the header; the + string arguments arrive in the order app_name, app_icon, summary, body + (replaces_id is a uint32, so it isn't in the string list). Anything + that doesn't look like that is skipped rather than guessed at. + """ + collecting = False + strings: list[str] = [] + + def _emit() -> Optional[Notification]: + if len(strings) < 3: + return None + return Notification(app=strings[0].strip(), summary=strings[2].strip(), + body=(strings[3].strip() if len(strings) > 3 else "")) + + for line in lines: + if _BLOCK_START.match(line): + if collecting: + notification = _emit() + if notification is not None: + yield notification + collecting = "member=Notify" in line + strings = [] + continue + if not collecting: + continue + match = _STRING_LINE.match(line) + if match: + strings.append(match.group(1)) + if collecting: + notification = _emit() + if notification is not None: + yield notification + + +class NotificationGate: + """Filter + rate limit. Clock is passed in (monotonic seconds) so the + rate limiting is testable without sleeping.""" + + def __init__(self, pattern: str = "", min_interval: float = 60.0): + self._min_interval = max(0.0, min_interval) + self._last_forwarded = None + self._pattern = None + if (pattern or "").strip(): + try: + self._pattern = re.compile(pattern, re.IGNORECASE) + except re.error: + self._pattern = None # a broken regex shouldn't mute everything + + def matches(self, notification: Notification) -> bool: + if self._pattern is None: + return True + return bool(self._pattern.search(notification.as_text())) + + def should_forward(self, notification: Notification, now: float) -> bool: + if not notification.as_text().strip(): + return False + if not self.matches(notification): + return False + if self._last_forwarded is not None and now - self._last_forwarded < self._min_interval: + return False + self._last_forwarded = now + return True + + +def available() -> bool: + return platform.system() == "Linux" and shutil.which("dbus-monitor") is not None + + +class NotificationWatcher: + """Tails dbus-monitor on a daemon thread, calling *callback* per + notification. Best-effort: if the session bus isn't reachable it reports + why via start() and stays off.""" + + _ARGS = [ + "dbus-monitor", "--session", + "interface='org.freedesktop.Notifications',member='Notify'", + ] + + def __init__(self, callback: Callable[[Notification], None]): + self._callback = callback + self._process: Optional[subprocess.Popen] = None + self._thread: Optional[threading.Thread] = None + self._running = False + + def start(self) -> Optional[str]: + """None on success, otherwise the reason the bridge is off.""" + if not available(): + return "notification bridge needs Linux + dbus-monitor — skipping" + try: + self._process = subprocess.Popen( + self._ARGS, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + text=True, bufsize=1, + ) + except Exception as exc: + return f"couldn't start dbus-monitor ({exc}) — notification bridge off" + self._running = True + self._thread = threading.Thread(target=self._pump, name="notification-bridge", daemon=True) + self._thread.start() + return None + + def _pump(self) -> None: + assert self._process is not None and self._process.stdout is not None + try: + for notification in iter_notifications(self._process.stdout): + if not self._running: + return + try: + self._callback(notification) + except Exception: + pass # one bad notification shouldn't end the bridge + except Exception: + pass + + def stop(self) -> None: + self._running = False + if self._process is not None: + try: + self._process.terminate() + except Exception: + pass + self._process = None diff --git a/bolt_pet/pet_actions.py b/bolt_pet/pet_actions.py new file mode 100644 index 0000000..5e31da4 --- /dev/null +++ b/bolt_pet/pet_actions.py @@ -0,0 +1,137 @@ +"""Commands that drive the pet's *body* instead of the shell. + +The server relays shell commands to this machine (see server_client. +run_local_command). Rather than inventing a new payload type the desk API +doesn't speak — this client can't change the server — a small `petctl` +pseudo-command is intercepted before it ever reaches `subprocess`: if Bolt +emits `petctl move top-left` or `petctl emote wave`, the pet does it and +returns a normal-looking command output string, so from the server's side +it's just another tool call that worked. + +Pure parsing logic — no Qt, no subprocess — so it's cheap to unit test. The +UI half lives in ui/pet_window.py (apply_action). +""" + +from __future__ import annotations + +import shlex +from typing import Optional + +# What Bolt is allowed to type. Anything else falls through to a real shell. +_PREFIXES = ("petctl", "bolt-pet", "pet") + +ANCHORS = ( + "top-left", "top", "top-right", + "left", "center", "right", + "bottom-left", "bottom", "bottom-right", + "cursor", "random", +) + +EMOTES = ("wave", "hop", "spin", "nod", "shake", "bounce", "wiggle") + +HELP = ( + "petctl move | <" + "|".join(ANCHORS) + ">\n" + "petctl emote <" + "|".join(EMOTES) + ">\n" + "petctl say \n" + "petctl wander on|off\n" + "petctl nap on|off" +) + + +class ActionError(Exception): + """Bad petctl syntax — reported back to the server as command output.""" + + +def is_pet_command(command: str) -> bool: + parts = (command or "").strip().split() + return bool(parts) and parts[0].lower() in _PREFIXES + + +def _bool_arg(value: str) -> bool: + value = value.lower() + if value in ("on", "true", "yes", "1", "start", "enable"): + return True + if value in ("off", "false", "no", "0", "stop", "disable"): + return False + raise ActionError(f"expected on/off, got {value!r}") + + +def parse(command: str) -> Optional[dict]: + """Parse a `petctl ...` string into an action dict, or None if this isn't + a pet command at all (caller should run it as a real shell command). + Raises ActionError on a pet command that doesn't make sense.""" + if not is_pet_command(command): + return None + try: + parts = shlex.split(command.strip()) + except ValueError as exc: # unbalanced quotes + raise ActionError(f"couldn't parse arguments: {exc}") from exc + verb = (parts[1].lower() if len(parts) > 1 else "help") + args = parts[2:] + + if verb in ("help", "-h", "--help"): + return {"action": "help"} + + if verb in ("move", "goto", "walk"): + if not args: + raise ActionError("move needs a target: " + ", ".join(ANCHORS) + ", or x y") + if len(args) >= 2 and _looks_numeric(args[0]) and _looks_numeric(args[1]): + return {"action": "move", "x": int(float(args[0])), "y": int(float(args[1]))} + anchor = args[0].lower().replace("_", "-") + if anchor not in ANCHORS: + raise ActionError(f"unknown position {args[0]!r}; try one of: " + ", ".join(ANCHORS)) + return {"action": "move", "anchor": anchor} + + if verb in ("emote", "do"): + if not args: + raise ActionError("emote needs a name: " + ", ".join(EMOTES)) + emote = args[0].lower() + if emote not in EMOTES: + raise ActionError(f"unknown emote {args[0]!r}; try one of: " + ", ".join(EMOTES)) + return {"action": "emote", "emote": emote} + + if verb == "say": + text = " ".join(args).strip() + if not text: + raise ActionError("say needs something to say") + return {"action": "say", "text": text} + + if verb == "wander": + if not args: + raise ActionError("wander needs on or off") + return {"action": "wander", "enabled": _bool_arg(args[0])} + + if verb in ("nap", "sleep", "dnd"): + if not args: + raise ActionError("nap needs on or off") + return {"action": "nap", "enabled": _bool_arg(args[0])} + + raise ActionError(f"unknown petctl verb {verb!r}\n{HELP}") + + +def _looks_numeric(value: str) -> bool: + try: + float(value) + return True + except ValueError: + return False + + +def describe(action: dict) -> str: + """The text handed back to the server as this "command"'s output. Phrased + as a completed fact so the model doesn't narrate the mechanics of it.""" + kind = action.get("action") + if kind == "move": + where = action.get("anchor") or f"({action.get('x')}, {action.get('y')})" + return f"[pet] walking to {where}" + if kind == "emote": + return f"[pet] {action['emote']}" + if kind == "say": + return "[pet] showing that in the speech bubble" + if kind == "wander": + return "[pet] wandering " + ("enabled" if action["enabled"] else "disabled") + if kind == "nap": + return "[pet] " + ("napping" if action["enabled"] else "awake") + if kind == "help": + return HELP + return "[pet] ok" diff --git a/bolt_pet/quiet.py b/bolt_pet/quiet.py new file mode 100644 index 0000000..2e54f21 --- /dev/null +++ b/bolt_pet/quiet.py @@ -0,0 +1,78 @@ +"""Quiet hours — when the pet is allowed to make noise on its own. + +Napping only ever suppresses *proactive* noise (heartbeat announcements, +forwarded notifications) and wandering. The wake word, click-to-talk and +push-to-talk still work: telling it to be quiet shouldn't mean it stops +answering when spoken to. + +Pure logic (parsing + a time comparison) so it's testable without waiting +for 11pm. +""" + +from __future__ import annotations + +from datetime import time as dtime +from typing import Iterable, Optional + + +class QuietHoursError(ValueError): + pass + + +def _parse_clock(value: str) -> dtime: + parts = value.strip().split(":") + if len(parts) != 2: + raise QuietHoursError(f"expected HH:MM, got {value!r}") + try: + hour, minute = int(parts[0]), int(parts[1]) + except ValueError as exc: + raise QuietHoursError(f"expected HH:MM, got {value!r}") from exc + if not (0 <= hour <= 23 and 0 <= minute <= 59): + raise QuietHoursError(f"{value!r} is not a real time of day") + return dtime(hour, minute) + + +def parse_ranges(spec: str) -> list[tuple[dtime, dtime]]: + """"23:00-08:00, 13:00-14:00" -> [(23:00, 08:00), (13:00, 14:00)]. + Empty/blank spec means "no quiet hours".""" + ranges: list[tuple[dtime, dtime]] = [] + for chunk in (spec or "").split(","): + chunk = chunk.strip() + if not chunk: + continue + start, sep, end = chunk.partition("-") + if not sep: + raise QuietHoursError(f"expected HH:MM-HH:MM, got {chunk!r}") + ranges.append((_parse_clock(start), _parse_clock(end))) + return ranges + + +def in_ranges(now: dtime, ranges: Iterable[tuple[dtime, dtime]]) -> bool: + for start, end in ranges: + if start == end: + continue # a zero-length range is a typo, not "all day" + if start < end: + if start <= now < end: + return True + elif now >= start or now < end: # wraps past midnight + return True + return False + + +def is_quiet(spec: str, now: Optional[dtime] = None, on_error=None) -> bool: + """True if *now* (defaults to the local wall clock) falls inside *spec*. + A malformed spec is reported via *on_error* and treated as "not quiet" — + a config typo shouldn't silently mute the pet forever.""" + if not (spec or "").strip(): + return False + try: + ranges = parse_ranges(spec) + except QuietHoursError as exc: + if on_error is not None: + on_error(exc) + return False + if now is None: + from datetime import datetime + + now = datetime.now().time() + return in_ranges(now, ranges) diff --git a/bolt_pet/screen_context.py b/bolt_pet/screen_context.py new file mode 100644 index 0000000..7946912 --- /dev/null +++ b/bolt_pet/screen_context.py @@ -0,0 +1,184 @@ +"""What's on screen right now — the active window's title, and whether +something is running fullscreen. + +Two consumers: + * annotate() tacks the focused window's title onto what you said, so + "what's this error?" has a referent without you describing it (the desk + API takes text only, so this is a text annotation — no screenshot upload). + * is_fullscreen_active() feeds do-not-disturb: the pet shouldn't announce + anything over a call or a fullscreen game. + +Everything here is best-effort and must never raise: on a locked-down Wayland +session none of it is available, and the correct behaviour is simply "no +context", not a crashed pipeline. The subprocess *parsing* is split into pure +functions so it can be tested without a display server. +""" + +from __future__ import annotations + +import platform +import re +import shutil +import subprocess +from typing import Optional + +_TIMEOUT = 2.0 +_MAX_TITLE_CHARS = 160 + +# Titles that are just the desktop itself — annotating with these is noise. +_BORING_TITLES = {"", "desktop", "@!0,0;bdib", "plasmashell", "gnome-shell", "xfdesktop"} + + +def _run(args: list[str]) -> Optional[str]: + try: + completed = subprocess.run(args, capture_output=True, text=True, timeout=_TIMEOUT) + except Exception: + return None + if completed.returncode != 0: + return None + return completed.stdout + + +# ── pure parsing helpers (unit tested; no display server needed) ───────────── + +def parse_xprop_window_id(output: str) -> Optional[str]: + """`xprop -root _NET_ACTIVE_WINDOW` -> '_NET_ACTIVE_WINDOW(WINDOW): window id # 0x3c00007'""" + match = re.search(r"(0x[0-9a-fA-F]+)", output or "") + if not match or int(match.group(1), 16) == 0: + return None + return match.group(1) + + +def parse_xprop_window_name(output: str) -> Optional[str]: + """`xprop -id _NET_WM_NAME` -> '_NET_WM_NAME(UTF8_STRING) = "Firefox"'""" + match = re.search(r'=\s*"(.*)"\s*$', (output or "").strip(), re.DOTALL) + if not match: + return None + return match.group(1).strip() + + +def parse_xprop_fullscreen(output: str) -> bool: + return "_NET_WM_STATE_FULLSCREEN" in (output or "") + + +def clean_title(title: Optional[str]) -> Optional[str]: + title = (title or "").strip().replace("\n", " ") + if title.lower() in _BORING_TITLES: + return None + if len(title) > _MAX_TITLE_CHARS: + title = title[: _MAX_TITLE_CHARS - 1].rstrip() + "…" + return title or None + + +def annotate(text: str, title: Optional[str]) -> str: + """Attach the window title as an explicit aside rather than splicing it + into the sentence, so the model can ignore it when it's irrelevant.""" + text = (text or "").strip() + title = clean_title(title) + if not text or not title: + return text + return f"{text}\n\n[on screen right now: {title}]" + + +# ── platform probes ────────────────────────────────────────────────────────── + +def _linux_active_window_id() -> Optional[str]: + if not shutil.which("xprop"): + return None + output = _run(["xprop", "-root", "_NET_ACTIVE_WINDOW"]) + return parse_xprop_window_id(output or "") + + +def _linux_title() -> Optional[str]: + if shutil.which("xdotool"): + output = _run(["xdotool", "getactivewindow", "getwindowname"]) + if output and output.strip(): + return output.strip() + window_id = _linux_active_window_id() + if window_id is None: + return None + for prop in ("_NET_WM_NAME", "WM_NAME"): + output = _run(["xprop", "-id", window_id, prop]) + title = parse_xprop_window_name(output or "") + if title: + return title + return None + + +def _windows_title() -> Optional[str]: + try: + import ctypes + + user32 = ctypes.windll.user32 + handle = user32.GetForegroundWindow() + if not handle: + return None + length = user32.GetWindowTextLengthW(handle) + buffer = ctypes.create_unicode_buffer(length + 1) + user32.GetWindowTextW(handle, buffer, length + 1) + return buffer.value or None + except Exception: + return None + + +def _macos_title() -> Optional[str]: + script = ( + 'tell application "System Events" to get name of first application process ' + "whose frontmost is true" + ) + output = _run(["osascript", "-e", script]) + return (output or "").strip() or None + + +def active_window_title() -> Optional[str]: + """Focused window's title, or None if the platform won't tell us.""" + try: + system = platform.system() + if system == "Linux": + return clean_title(_linux_title()) + if system == "Windows": + return clean_title(_windows_title()) + if system == "Darwin": + return clean_title(_macos_title()) + except Exception: + pass + return None + + +def is_fullscreen_active() -> bool: + """True when the focused window is fullscreen (call, game, presentation). + False whenever we can't tell — do-not-disturb should be something you opt + into, not something a failed probe turns on.""" + try: + system = platform.system() + if system == "Linux": + window_id = _linux_active_window_id() + if window_id is None: + return False + return parse_xprop_fullscreen(_run(["xprop", "-id", window_id, "_NET_WM_STATE"]) or "") + if system == "Windows": + import ctypes + from ctypes import wintypes + + user32 = ctypes.windll.user32 + handle = user32.GetForegroundWindow() + if not handle: + return False + rect = wintypes.RECT() + user32.GetWindowRect(handle, ctypes.byref(rect)) + screen_w = user32.GetSystemMetrics(0) + screen_h = user32.GetSystemMetrics(1) + return (rect.right - rect.left) >= screen_w and (rect.bottom - rect.top) >= screen_h + except Exception: + pass + return False + + +def context_for(text: str) -> str: + """What the controller sends: *text* plus the active window title, when + SCREEN_CONTEXT is on and there's a title worth mentioning.""" + from . import config + + if not config.SCREEN_CONTEXT: + return text + return annotate(text, active_window_title()) diff --git a/bolt_pet/server_client.py b/bolt_pet/server_client.py new file mode 100644 index 0000000..524cc47 --- /dev/null +++ b/bolt_pet/server_client.py @@ -0,0 +1,125 @@ +"""HTTP client for Bolt's desk API (ai/desk_api.py on the server). + +Protocol is identical to desk_client/bolt_desk.py in the main tmn-api repo — +this pet is just another desk client, so it gets the exact same brain, +memory, tools, and persona as Discord chat and the Linux voice client: + + text -> POST /desk/converse + [server may relay a shell command back to run on THIS machine] + ... -> POST /desk/tool_result (repeat until the server sends a reply) + reply <- returned to caller + +Kept dependency-free beyond `requests` so it's easy to unit test with mocks. +""" + +from __future__ import annotations + +import subprocess +from pathlib import Path +from typing import Callable, Optional + +import requests + +from . import config + +_MAX_RELAY_HOPS = 16 + + +class ServerError(Exception): + """Raised when the server responds with an error payload or unreachable.""" + + +def _headers() -> dict: + return {"X-Desk-Api-Key": config.API_KEY} + + +def check_health(timeout: float = 10.0) -> dict: + response = requests.get(f"{config.SERVER_URL}/desk/health", headers=_headers(), timeout=timeout) + response.raise_for_status() + return response.json() + + +def run_local_command(command: str, timeout: int = None) -> str: + """Execute a command relayed by the server, exactly as bolt_desk.py does — + "full desktop control" for things like "open firefox" or "how full is my + disk". Runs as the current desktop user. See README security notes.""" + timeout = timeout or config.COMMAND_TIMEOUT_SECONDS + try: + completed = subprocess.run( + command, shell=True, capture_output=True, text=True, + timeout=timeout, cwd=str(Path.home()), + ) + output = (completed.stdout or "") + (completed.stderr or "") + return f"[exit {completed.returncode}]\n{output}"[:6000] + except subprocess.TimeoutExpired: + return f"[command timed out after {timeout}s]" + except Exception as exc: + return f"[command failed: {exc}]" + + +def converse( + text: str, + on_command: Callable[[str], str] = run_local_command, + timeout: float = 120.0, +) -> str: + """Send one turn of conversation to the desk API, relaying any commands + the server sends back until it produces a final reply. + + *on_command* is injectable for tests; defaults to actually running the + command locally (matching bolt_desk.py's behavior). + """ + headers = _headers() + try: + response = requests.post( + f"{config.SERVER_URL}/desk/converse", + json={"session_id": config.SESSION_ID, "text": text}, + headers=headers, timeout=timeout, + ) + payload = response.json() + except Exception as exc: + raise ServerError(f"couldn't reach the server: {exc}") from exc + + for _ in range(_MAX_RELAY_HOPS): + if payload.get("type") != "command": + break + output = on_command(str(payload.get("command") or "")) + try: + response = requests.post( + f"{config.SERVER_URL}/desk/tool_result", + json={ + "session_id": config.SESSION_ID, + "token": payload.get("token"), + "output": output, + }, + headers=headers, timeout=180, + ) + payload = response.json() + except Exception as exc: + raise ServerError(f"couldn't reach the server during tool relay: {exc}") from exc + + if payload.get("type") == "reply": + return str(payload.get("text") or "") + raise ServerError(str(payload.get("error") or "unknown server response")) + + +def report_status(timeout: float = 15.0) -> Optional[str]: + """Heartbeat — lets the desk API attach a pending spoken announcement + (proactive nudges, reminders fired since the last heartbeat) that the pet + can speak unprompted, exactly like the phone/desk clients. A pet has no + battery/GPS to report, so the device-status fields + (battery/is_charging/latitude/longitude/address, all optional server-side) + are simply omitted. + + Returns the announcement text to speak, or None if there's nothing pending. + """ + try: + response = requests.post( + f"{config.SERVER_URL}/desk/report_status", + json={"session_id": config.SESSION_ID}, headers=_headers(), timeout=timeout, + ) + response.raise_for_status() + data = response.json() + except Exception as exc: + raise ServerError(f"heartbeat failed: {exc}") from exc + reply = data.get("reply") + return str(reply) if reply else None diff --git a/bolt_pet/speech_text.py b/bolt_pet/speech_text.py new file mode 100644 index 0000000..e022e70 --- /dev/null +++ b/bolt_pet/speech_text.py @@ -0,0 +1,130 @@ +"""Turn a server reply into something worth *hearing*. + +The server's persona writes for a chat window: markdown emphasis, bullet +lists, emoji, bare URLs. A TTS voice reads those literally ("asterisk +asterisk OS colon", "https colon slash slash..."), so everything spoken goes +through for_speech() first. Pure string logic, no Qt/audio imports — cheap to +unit test (see tests/test_speech_text.py). + +for_display() is the lighter sibling used for the speech bubble: it drops the +markdown *syntax* but keeps emoji and punctuation, since those render fine. +""" + +from __future__ import annotations + +import re + +# Pictographs, symbols, flags, dingbats, arrows, box drawing, variation +# selectors, ZWJ — anything a voice would either skip or read as a name +# ("black right-pointing triangle"). +_EMOJI = re.compile( + "[" + "\U0001F000-\U0001FAFF" # emoji / pictographs / symbols blocks + "\U00002190-\U000021FF" # arrows + "\U00002300-\U000023FF" # misc technical (⌘ ⏱ …) + "\U000025A0-\U000027BF" # geometric shapes, misc symbols, dingbats + "\U00002B00-\U00002BFF" # extra arrows / shapes + "\U0000FE00-\U0000FE0F" # variation selectors + "\U0001F1E6-\U0001F1FF" # regional indicators (flags) + "\U0000200D" # zero-width joiner + "]+", + flags=re.UNICODE, +) + +# Characters that are markup or decoration rather than speech. Kept out of +# the spoken text entirely; ordinary punctuation (. , ! ? ; : ' " ( ) -) is +# preserved because it shapes prosody. +_UNSPEAKABLE = re.compile(r"[*_#`~^|<>\\{}\[\]/=+@©®™•·–—]+") + +_FENCED_CODE = re.compile(r"```.*?```", re.DOTALL) +_INLINE_CODE = re.compile(r"`([^`]*)`") +_MD_IMAGE = re.compile(r"!\[([^\]]*)\]\([^)]*\)") +_MD_LINK = re.compile(r"\[([^\]]+)\]\([^)]*\)") +_URL = re.compile(r"\b(?:https?://|www\.)\S+") +_HEADING = re.compile(r"^\s{0,3}#{1,6}\s*", re.MULTILINE) +_BLOCKQUOTE = re.compile(r"^\s{0,3}>\s?", re.MULTILINE) +_RULE = re.compile(r"^\s*([-*_=])(?:\s*\1){2,}\s*$", re.MULTILINE) +_BULLET = re.compile(r"^\s*(?:[-*+•·]|\d+[.)])\s+", re.MULTILINE) +_EMPHASIS = re.compile(r"(\*{1,3}|_{1,3})(\S(?:.*?\S)?)\1", re.DOTALL) +_TABLE_PIPE = re.compile(r"[ \t]*\|[ \t]*") + +# Symbols worth saying out loud rather than dropping — a bare "&" read as +# nothing turns "R&D" into "RD". Split in two because the non-ASCII ones sit +# inside the arrow/symbol blocks _EMOJI strips, so they have to be worded +# before that pass; the ASCII ones must wait until *after* markdown parsing +# (an "=" turned into " equals " would stop a "====" rule matching _RULE). +_PRE_SPOKEN_SYMBOLS = { + "→": " to ", + "×": " by ", + "°": " degrees ", + "≈": " about ", +} +_SPOKEN_SYMBOLS = { + "&": " and ", + "%": " percent ", + "@": " at ", + "+": " plus ", + "=": " equals ", +} + +_MULTI_SPACE = re.compile(r"[ \t]+") +_MULTI_PUNCT = re.compile(r"(?:\s*\.){2,}") + + +def _strip_markdown(text: str, *, keep_emoji: bool) -> str: + text = _FENCED_CODE.sub(" (code) ", text) + text = _INLINE_CODE.sub(r"\1", text) + text = _MD_IMAGE.sub(r"\1", text) + text = _MD_LINK.sub(r"\1", text) + text = _RULE.sub("", text) + text = _HEADING.sub("", text) + text = _BLOCKQUOTE.sub("", text) + text = _EMPHASIS.sub(r"\2", text) + if not keep_emoji: + text = _EMOJI.sub(" ", text) + return text + + +def _bullets_to_sentences(text: str) -> str: + """A read-aloud list needs pauses where the eye would see line breaks, + otherwise "OS Linux Uptime 1 day CPU load moderate" runs together.""" + lines = [_BULLET.sub("", line).strip() for line in text.splitlines()] + lines = [line for line in lines if line] + if len(lines) < 2: + return lines[0] if lines else "" + # A bullet like "OS: Linux" ends mid-thought — give the voice a full stop + # so consecutive items don't slur into one run-on sentence. + return " ".join(line if line[-1] in ".!?:,;" else line + "." for line in lines) + + +def for_speech(text: str) -> str: + """Plain prose for the TTS engine: no markdown, no emoji, no bare URLs, + no stray symbols that would be read out character by character.""" + text = (text or "").strip() + if not text: + return "" + for symbol, spoken in _PRE_SPOKEN_SYMBOLS.items(): + text = text.replace(symbol, spoken) + text = _strip_markdown(text, keep_emoji=False) + text = _URL.sub(" link ", text) + text = _TABLE_PIPE.sub(", ", text) + text = _bullets_to_sentences(text) + for symbol, spoken in _SPOKEN_SYMBOLS.items(): + text = text.replace(symbol, spoken) + text = _UNSPEAKABLE.sub(" ", text) + text = _MULTI_PUNCT.sub(".", text) + text = _MULTI_SPACE.sub(" ", text) + text = re.sub(r"\s+([.,!?;:])", r"\1", text) + return text.strip() + + +def for_display(text: str) -> str: + """What the speech bubble shows: markdown syntax removed (the bubble + can't render it) but emoji and layout-ish punctuation left alone.""" + text = (text or "").strip() + if not text: + return "" + text = _strip_markdown(text, keep_emoji=True) + lines = [_BULLET.sub("• ", line).strip() for line in text.splitlines()] + text = " ".join(line for line in lines if line) + return _MULTI_SPACE.sub(" ", text).strip() diff --git a/bolt_pet/state.py b/bolt_pet/state.py new file mode 100644 index 0000000..35ba9b4 --- /dev/null +++ b/bolt_pet/state.py @@ -0,0 +1,71 @@ +"""Pet state machine — pure logic, no Qt/audio dependencies, so it's cheap +to unit test. The UI layer (ui/pet_window.py) reacts to state changes by +swapping the active sprite animation; the worker thread (ui/app.py) drives +transitions as the mic/wake/converse/tts pipeline progresses. +""" + +from __future__ import annotations + +from enum import Enum +from typing import Callable, Optional + + +class PetState(str, Enum): + IDLE = "idle" # waiting for the wake phrase (or a click) + LISTENING = "listening" # actively recording an utterance + THINKING = "thinking" # waiting on the server (STT done, converse in flight) + TALKING = "talking" # playing back the TTS reply + ERROR = "error" # brief flash state on failure, then back to idle + + +# States it's valid to move to from each state. Keeps ad-hoc bugs (e.g. +# firing TALKING before a reply exists) from silently passing through. +# +# IDLE -> TALKING is legal (not just IDLE -> LISTENING) because of proactive +# announcements: the heartbeat poll (controller.py's _maybe_heartbeat) can +# make the pet speak unprompted — a reminder firing, a nudge from the server +# — without the user having said anything first, so there's no preceding +# LISTENING/THINKING leg for that turn. +_TRANSITIONS: dict[PetState, set[PetState]] = { + PetState.IDLE: {PetState.LISTENING, PetState.TALKING, PetState.ERROR}, + PetState.LISTENING: {PetState.THINKING, PetState.IDLE, PetState.ERROR}, + PetState.THINKING: {PetState.TALKING, PetState.IDLE, PetState.ERROR}, + PetState.TALKING: {PetState.IDLE, PetState.ERROR}, + PetState.ERROR: {PetState.IDLE}, +} + + +class InvalidTransition(Exception): + pass + + +class PetStateMachine: + def __init__(self, on_change: Optional[Callable[[PetState, PetState], None]] = None): + self._state = PetState.IDLE + self._on_change = on_change + + @property + def state(self) -> PetState: + return self._state + + def transition(self, new_state: PetState) -> None: + if new_state == self._state: + return + allowed = _TRANSITIONS.get(self._state, set()) + if new_state not in allowed: + raise InvalidTransition(f"{self._state} -> {new_state} is not allowed") + old_state = self._state + self._state = new_state + if self._on_change is not None: + self._on_change(old_state, new_state) + + def force(self, new_state: PetState) -> None: + """Bypass the transition table — used only for recovering to IDLE + from an unexpected/edge-case state (e.g. after an exception mid + pipeline). Prefer transition() everywhere else.""" + old_state = self._state + if new_state == old_state: + return + self._state = new_state + if self._on_change is not None: + self._on_change(old_state, new_state) diff --git a/bolt_pet/ui/__init__.py b/bolt_pet/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bolt_pet/ui/app.py b/bolt_pet/ui/app.py new file mode 100644 index 0000000..51f53c5 --- /dev/null +++ b/bolt_pet/ui/app.py @@ -0,0 +1,103 @@ +"""Wires everything together: QApplication, the pet window, the tray icon, +the history / wake-tuner windows, the global push-to-talk hotkey, and the +background PetController thread that owns the mic/wake/server/TTS pipeline. + +Everything the controller wants the UI to do arrives as a Qt signal, so the +worker thread never touches a widget. +""" + +from __future__ import annotations + +import sys + +from PySide6.QtCore import QThread +from PySide6.QtWidgets import QApplication + +from .. import config +from ..controller import PetController +from ..hotkey import GlobalHotkey +from ..state import PetState +from .history_window import HistoryWindow +from .pet_window import PetWindow +from .tray import PetTray +from .wake_tuner import WakeTunerWindow + + +def _log(message: str) -> None: + print(message, flush=True) + + +def run() -> int: + app = QApplication(sys.argv) + app.setQuitOnLastWindowClosed(False) # tray-driven app; closing the pet isn't "quit" + + window = PetWindow() + window.show() + + controller = PetController() + thread = QThread() + controller.moveToThread(thread) + + thread.started.connect(controller.run) + controller.state_changed.connect(lambda value: window.set_state(PetState(value))) + controller.said.connect(window.say) + controller.log.connect(_log) + controller.action.connect(window.apply_action) # petctl move/emote/say/... + controller.finished.connect(thread.quit) + + window.talk_requested.connect(controller.request_talk_now) + window.copied.connect(lambda text: _log(f"Copied to clipboard: {text[:60]}")) + + history_window = HistoryWindow(controller.history) + tuner_window = WakeTunerWindow( + get_threshold=controller.wake_threshold, + set_threshold=controller.set_wake_threshold, + get_stats=controller.wake_stats, + on_reset=controller.reset_wake_stats, + ) + + def _set_nap(napping: bool) -> None: + # Clicking the tray item pins the state; the schedule takes over again + # only after a restart or a `petctl nap off`. + controller.set_napping(napping) + window.set_napping(napping) + + tray = PetTray( + on_talk_now=controller.request_talk_now, + on_toggle_mute=controller.toggle_mute, + on_quit=app.quit, + on_set_wander=window.set_wander_enabled, + wander_enabled=config.PET_WANDER, + on_set_click_through=window.set_click_through, + click_through_enabled=config.PET_CLICK_THROUGH, + on_set_nap=_set_nap, + on_show_history=history_window.show_refreshed, + on_show_wake_tuner=tuner_window.show_refreshed, + ) + + def _handle_napping(napping: bool) -> None: + window.set_napping(napping) + tray.set_napping(napping) + + controller.napping.connect(_handle_napping) + + # Push-to-talk: a global hook, because the pet window never has focus. + # request_talk_now() only sets a threading.Event, so it's safe to call + # from pynput's listener thread. + hotkey = GlobalHotkey(config.PUSH_TO_TALK_HOTKEY, controller.request_talk_now) + problem = hotkey.start() + if problem: + _log(problem) + elif hotkey.running: + _log(f"Push-to-talk: {config.PUSH_TO_TALK_HOTKEY}") + + def _shutdown() -> None: + hotkey.stop() + controller.stop() + thread.quit() + thread.wait(5000) + + app.aboutToQuit.connect(_shutdown) + + thread.start() + return app.exec() diff --git a/bolt_pet/ui/history_window.py b/bolt_pet/ui/history_window.py new file mode 100644 index 0000000..00795cc --- /dev/null +++ b/bolt_pet/ui/history_window.py @@ -0,0 +1,83 @@ +"""Scrollback for the speech bubble. + +The bubble is transient by design, so anything Bolt said more than a few +seconds ago is gone. This is the "wait, what was that path again?" window: +the last HISTORY_LIMIT turns, copyable. Opened from the tray. + +Reads a bolt_pet.history.ConversationHistory (pure logic, tested separately); +this file is only presentation. +""" + +from __future__ import annotations + +import time +from typing import Callable, Optional + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( + QApplication, QDialog, QHBoxLayout, QPlainTextEdit, QPushButton, QVBoxLayout, +) + +from ..history import ConversationHistory + + +def _clock(timestamp: float) -> str: + return time.strftime("%H:%M:%S", time.localtime(timestamp)) + + +class HistoryWindow(QDialog): + def __init__(self, history: ConversationHistory, on_clear: Optional[Callable[[], None]] = None): + super().__init__() + self._history = history + self._on_clear = on_clear + self.setWindowTitle("Bolt — conversation history") + self.resize(620, 420) + + self._view = QPlainTextEdit() + self._view.setReadOnly(True) + self._view.setLineWrapMode(QPlainTextEdit.WidgetWidth) + + copy_button = QPushButton("Copy all") + copy_button.clicked.connect(self._copy_all) + clear_button = QPushButton("Clear") + clear_button.clicked.connect(self._clear) + close_button = QPushButton("Close") + close_button.clicked.connect(self.close) + close_button.setDefault(True) + + buttons = QHBoxLayout() + buttons.addWidget(copy_button) + buttons.addWidget(clear_button) + buttons.addStretch(1) + buttons.addWidget(close_button) + + layout = QVBoxLayout(self) + layout.addWidget(self._view) + layout.addLayout(buttons) + + def refresh(self) -> None: + self._view.setPlainText(self._history.as_text(clock=_clock)) + # Jump to the newest line — that's what you opened this for. + scrollbar = self._view.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def show_refreshed(self) -> None: + self.refresh() + self.show() + self.raise_() + self.activateWindow() + + def _copy_all(self) -> None: + QApplication.clipboard().setText(self._history.as_text(clock=_clock)) + + def _clear(self) -> None: + self._history.clear() + if self._on_clear is not None: + self._on_clear() + self.refresh() + + def keyPressEvent(self, event) -> None: + if event.key() == Qt.Key_Escape: + self.close() + return + super().keyPressEvent(event) diff --git a/bolt_pet/ui/pet_window.py b/bolt_pet/ui/pet_window.py new file mode 100644 index 0000000..830e8c6 --- /dev/null +++ b/bolt_pet/ui/pet_window.py @@ -0,0 +1,582 @@ +"""The pet itself: a frameless, translucent, always-on-top window that +renders the current sprite animation, wanders the desktop on its own while +idle, walks/emotes on command from the server (see pet_actions.py), can be +dragged around, dims when napping, and turns a plain (non-drag) click into a +"talk now" request. +""" + +from __future__ import annotations + +import math +import random +import time +from pathlib import Path +from typing import Optional + +from PySide6.QtCore import QPoint, Qt, QTimer, Signal +from PySide6.QtGui import ( + QColor, QCursor, QFont, QFontMetrics, QPainter, QPainterPath, QPixmap, QRegion, QTransform, +) +from PySide6.QtWidgets import QApplication, QWidget + +from .. import config +from ..state import PetState +from .sprite import SpriteSet + +_DRAG_THRESHOLD_PX = 4 +# Movement runs on its own ~30fps timer, independent of the (slower) sprite +# animation timer, so a stroll looks smooth even at IDLE_ANIMATION_FPS=6. +_WANDER_TICK_MS = 33 +_EMOTE_TICKS = 36 # ~1.2s per emote at the tick rate above +_NAP_OPACITY = 0.35 + + +def emote_transform(emote: str, progress: float) -> tuple[float, float, float, float]: + """(dx, dy, rotation_degrees, scale) for an emote at *progress* 0..1. + + Pure maths, deliberately separate from paintEvent so the motion curves can + be unit tested (and so adding an emote doesn't mean touching painting + code). Every emote must return to (0, 0, 0, 1) at progress 1.0, otherwise + the pet ends up permanently askew. + """ + progress = min(max(progress, 0.0), 1.0) + fade = math.sin(math.pi * progress) # 0 -> 1 -> 0, so it always lands home + tau = 2 * math.pi + if emote == "wave": + return 0.0, 0.0, 14.0 * fade * math.sin(tau * 2 * progress), 1.0 + if emote in ("hop", "bounce"): + hops = 2 if emote == "hop" else 3 + return 0.0, -22.0 * fade * abs(math.sin(math.pi * hops * progress)), 0.0, 1.0 + if emote == "spin": + return 0.0, 0.0, 360.0 * progress % 360.0, 1.0 + if emote == "nod": + return 0.0, 10.0 * fade * math.sin(tau * 2 * progress), 0.0, 1.0 - 0.05 * fade + if emote in ("shake", "wiggle"): + return 14.0 * fade * math.sin(tau * 3 * progress), 0.0, 0.0, 1.0 + return 0.0, 0.0, 0.0, 1.0 + + +class SpeechBubble(QWidget): + """Small translucent word-bubble shown above the pet while it talks.""" + + _MAX_WIDTH = 260 + _PADDING = 10 + + copied = Signal(str) + + def __init__(self, parent: Optional[QWidget] = None): + super().__init__(parent, Qt.FramelessWindowHint | Qt.Tool) + self.setAttribute(Qt.WA_TranslucentBackground) + self.setAttribute(Qt.WA_ShowWithoutActivating) + self._text = "" + self._font = QFont() + self._font.setPointSize(10) + self._flash = "" # transient overlay ("Copied") drawn over the text + self._hide_timer = QTimer(self) + self._hide_timer.setSingleShot(True) + self._hide_timer.timeout.connect(self.hide) + self._flash_timer = QTimer(self) + self._flash_timer.setSingleShot(True) + self._flash_timer.timeout.connect(self._clear_flash) + self.setToolTip("Click to copy") + self.setCursor(Qt.PointingHandCursor) + self.hide() + + @property + def text(self) -> str: + return self._text + + def show_text(self, text: str, duration_ms: int = 6000) -> None: + text = (text or "").strip() + if not text: + self.hide() + return + self._text = text + self._relayout() + self.show() + self.raise_() + self._hide_timer.start(duration_ms) + + # ── click to copy ──────────────────────────────────────────────────── + # The bubble hides itself after a few seconds, which is fine for chat and + # awful for anything you needed to keep (a path, a number, a command's + # output). One click puts it on the clipboard; the tray's History window + # has the rest. + + def mousePressEvent(self, event) -> None: + if event.button() != Qt.LeftButton or not self._text: + return + QApplication.clipboard().setText(self._text) + self.copied.emit(self._text) + self._flash = "Copied to clipboard" + self.update() + self._flash_timer.start(900) + self._hide_timer.start(2500) # linger a moment so the flash is visible + + def _clear_flash(self) -> None: + self._flash = "" + self.update() + + def _wrapped_lines(self) -> list[str]: + metrics = QFontMetrics(self._font) + words = self._text.split() + lines: list[str] = [] + current = "" + max_text_width = self._MAX_WIDTH - 2 * self._PADDING + for word in words: + candidate = f"{current} {word}".strip() + if metrics.horizontalAdvance(candidate) <= max_text_width or not current: + current = candidate + else: + lines.append(current) + current = word + if current: + lines.append(current) + return lines[:6] # don't let a huge reply turn into a wall of bubble + + def _relayout(self) -> None: + metrics = QFontMetrics(self._font) + lines = self._wrapped_lines() + text_width = max((metrics.horizontalAdvance(line) for line in lines), default=0) + width = min(self._MAX_WIDTH, text_width + 2 * self._PADDING) + height = metrics.height() * len(lines) + 2 * self._PADDING + self.resize(width, height) + + def paintEvent(self, _event) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + path = QPainterPath() + path.addRoundedRect(0, 0, self.width(), self.height(), 10, 10) + painter.fillPath(path, QColor(30, 30, 35, 230)) + painter.setFont(self._font) + metrics = QFontMetrics(self._font) + if self._flash: + painter.setPen(QColor(150, 230, 170)) + painter.drawText(self.rect(), Qt.AlignCenter, self._flash) + return + painter.setPen(QColor(240, 240, 245)) + y = self._PADDING + metrics.ascent() + for line in self._wrapped_lines(): + painter.drawText(self._PADDING, y, line) + y += metrics.height() + + +class PetWindow(QWidget): + talk_requested = Signal() + copied = Signal(str) # bubble text the user just put on the clipboard + + def __init__(self, sprite_dir: Optional[Path] = None, size: Optional[int] = None): + super().__init__() + flags = Qt.FramelessWindowHint | Qt.Tool + if config.PET_ALWAYS_ON_TOP: + flags |= Qt.WindowStaysOnTopHint + self.setWindowFlags(flags) + self.setAttribute(Qt.WA_TranslucentBackground) + + self.sprites = SpriteSet(sprite_dir or (Path(__file__).resolve().parent.parent / "assets" / "sprites"), + size or config.PET_SIZE) + self.resize(self.sprites.size, self.sprites.size) + self._current_state = PetState.IDLE + + self._drag_offset: Optional[QPoint] = None + self._press_pos: Optional[QPoint] = None + self._dragged = False + + self._bubble = SpeechBubble() + self._bubble.copied.connect(self.copied) + + self._napping = False + self._emote: Optional[str] = None + self._emote_tick = 0 + self._mask_key = None + + self._anim_timer = QTimer(self) + self._anim_timer.timeout.connect(self._advance_frame) + fps = max(1.0, config.IDLE_ANIMATION_FPS) + self._anim_timer.start(int(1000 / fps)) + + self._wander_enabled = config.PET_WANDER + self._wander_target: Optional[QPoint] = None + self._commanded_move = False # a petctl move — happens even mid-conversation + self._next_wander_at = 0.0 + self._bob_offset = 0 + self._bob_phase = 0.0 + self._schedule_next_wander() + self._wander_timer = QTimer(self) + self._wander_timer.timeout.connect(self._movement_tick) + self._wander_timer.start(_WANDER_TICK_MS) + + self._click_through = False + self.set_click_through(config.PET_CLICK_THROUGH) + self._place_start_position() + + # ── placement ──────────────────────────────────────────────────────── + + def _place_start_position(self) -> None: + screen = QApplication.primaryScreen() + geo = screen.availableGeometry() if screen else None + try: + x = int(config.PET_START_X) if config.PET_START_X else None + y = int(config.PET_START_Y) if config.PET_START_Y else None + except ValueError: + x = y = None + if geo is not None: + x = geo.right() - self.width() - 40 if x is None else x + y = geo.bottom() - self.height() - 60 if y is None else y + self.move(x or 0, y or 0) + self._reposition_bubble() + + def _reposition_bubble(self) -> None: + top_left = self.geometry().topLeft() + self._bubble.move( + top_left.x() + self.width() // 2 - self._bubble.width() // 2, + top_left.y() - self._bubble.height() - 8, + ) + + # ── server-driven actions (petctl) ─────────────────────────────────── + + def apply_action(self, action: dict) -> None: + """Perform one parsed petctl action (see pet_actions.py). Called on + the UI thread via a queued signal from the controller.""" + kind = action.get("action") + if kind == "move": + target = self._resolve_move_target(action) + if target is not None: + self._wander_target = target + self._commanded_move = True # overrides the idle-only rule + elif kind == "emote": + self.start_emote(action["emote"]) + elif kind == "say": + self.say(action["text"]) + elif kind == "wander": + self.set_wander_enabled(bool(action["enabled"])) + elif kind == "nap": + self.set_napping(bool(action["enabled"])) + + def _resolve_move_target(self, action: dict) -> Optional[QPoint]: + geo = self._screen_geometry() + if "x" in action and "y" in action: + point = QPoint(int(action["x"]), int(action["y"])) + return self._clamp_to_screen(point, geo) + anchor = action.get("anchor") + if anchor == "cursor": + cursor = QCursor.pos() + return self._clamp_to_screen( + QPoint(cursor.x() - self.width() // 2, cursor.y() - self.height() // 2), geo + ) + if geo is None: + return None + if anchor == "random": + return self._pick_wander_target() + margin = config.PET_WANDER_MARGIN + left, right = geo.left() + margin, geo.right() - self.width() - margin + top, bottom = geo.top() + margin, geo.bottom() - self.height() - margin + middle_x = geo.left() + (geo.width() - self.width()) // 2 + middle_y = geo.top() + (geo.height() - self.height()) // 2 + positions = { + "top-left": (left, top), "top": (middle_x, top), "top-right": (right, top), + "left": (left, middle_y), "center": (middle_x, middle_y), "right": (right, middle_y), + "bottom-left": (left, bottom), "bottom": (middle_x, bottom), "bottom-right": (right, bottom), + } + if anchor not in positions: + return None + return QPoint(*positions[anchor]) + + def _clamp_to_screen(self, point: QPoint, geo) -> QPoint: + if geo is None: + return point + x = min(max(point.x(), geo.left()), max(geo.left(), geo.right() - self.width())) + y = min(max(point.y(), geo.top()), max(geo.top(), geo.bottom() - self.height())) + return QPoint(x, y) + + # ── emotes ─────────────────────────────────────────────────────────── + + def start_emote(self, emote: str) -> None: + self._emote = emote + self._emote_tick = 0 + self.update() + + def _advance_emote(self) -> None: + if self._emote is None: + return + self._emote_tick += 1 + if self._emote_tick > _EMOTE_TICKS: + self._emote = None + self._emote_tick = 0 + self.update() + + def _emote_transform(self) -> tuple[float, float, float, float]: + if self._emote is None: + return 0.0, 0.0, 0.0, 1.0 + return emote_transform(self._emote, self._emote_tick / _EMOTE_TICKS) + + # ── napping (quiet hours / do-not-disturb) ─────────────────────────── + + def set_napping(self, napping: bool) -> None: + """Dim and stand still. Purely cosmetic here — the controller is what + actually suppresses proactive speech.""" + if napping == self._napping: + return + self._napping = napping + self.setWindowOpacity(_NAP_OPACITY if napping else 1.0) + if napping: + self._stop_walking() + self.update() + + @property + def napping(self) -> bool: + return self._napping + + # ── mouse transparency ─────────────────────────────────────────────── + + def set_click_through(self, enabled: bool) -> None: + """When on, the pet ignores the mouse entirely (tray-only control) — + for when it's parked over something you need to click a lot.""" + self._click_through = enabled + self.setAttribute(Qt.WA_TransparentForMouseEvents, enabled) + if enabled: + self.clearMask() + self._mask_key = None + else: + self._mask_key = None # force the shaped mask to be rebuilt + + @property + def click_through(self) -> bool: + return self._click_through + + def _apply_input_mask(self, pixmap: Optional[QPixmap], x: int, y: int) -> None: + """Restrict the window to the sprite's opaque pixels, so the square + window's transparent corners stop swallowing clicks meant for what's + underneath. Rebuilt only when the frame actually changes — the mask + is derived from the pixmap's alpha, which isn't free.""" + if self._click_through or not config.PET_SHAPED_INPUT: + return + if pixmap is None: + if self._mask_key is not None: + self.clearMask() + self._mask_key = None + return + key = (pixmap.cacheKey(), x, y) + if key == self._mask_key: + return + self._mask_key = key + try: + region = QRegion(pixmap.mask()) + region.translate(x, y) + self.setMask(region) + except Exception: + self.clearMask() # a sprite without an alpha channel — never mind + + # ── wandering ──────────────────────────────────────────────────────── + + def set_wander_enabled(self, enabled: bool) -> None: + self._wander_enabled = enabled + if not enabled: + self._stop_walking() + + def wander_now(self) -> None: + """Stroll immediately (tray menu / anything that wants a nudge).""" + self._next_wander_at = 0.0 + + def _screen_geometry(self): + # screenAt() so a multi-monitor setup keeps the pet on the screen + # it's currently standing on rather than yanking it to the primary. + screen = QApplication.screenAt(self.frameGeometry().center()) or QApplication.primaryScreen() + return screen.availableGeometry() if screen else None + + def _schedule_next_wander(self) -> None: + base = max(1.0, config.PET_WANDER_INTERVAL_SECONDS) + self._next_wander_at = time.monotonic() + random.uniform(0.5 * base, 1.5 * base) + + def _stop_walking(self) -> None: + if self._wander_target is None and not self._bob_offset: + return + self._wander_target = None + self._commanded_move = False + self._bob_phase = 0.0 + self._bob_offset = 0 + self.update() + + def snap_to_edge(self) -> bool: + """If the pet has come to rest near a screen edge, tuck it flush + against it — a desktop pet parked 11px off the taskbar looks like a + bug. Returns True if it moved.""" + geo = self._screen_geometry() + if geo is None or not config.PET_EDGE_SNAP: + return False + margin = config.PET_SNAP_MARGIN + here = self.pos() + x, y = here.x(), here.y() + if abs(x - geo.left()) <= margin: + x = geo.left() + elif abs(geo.right() - (x + self.width())) <= margin: + x = geo.right() - self.width() + 1 + if abs(y - geo.top()) <= margin: + y = geo.top() + elif abs(geo.bottom() - (y + self.height())) <= margin: + y = geo.bottom() - self.height() + 1 + if (x, y) == (here.x(), here.y()): + return False + self.move(x, y) + self._reposition_bubble() + return True + + def _pick_wander_target(self) -> Optional[QPoint]: + geo = self._screen_geometry() + if geo is None: + return None + margin = config.PET_WANDER_MARGIN + min_x, max_x = geo.left() + margin, geo.right() - self.width() - margin + min_y, max_y = geo.top() + margin, geo.bottom() - self.height() - margin + if max_x <= min_x or max_y <= min_y: # pet bigger than the screen + return None + here = self.pos() + target = QPoint(random.randint(min_x, max_x), random.randint(min_y, max_y)) + dx, dy = target.x() - here.x(), target.y() - here.y() + distance = math.hypot(dx, dy) + limit = max(1.0, config.PET_WANDER_MAX_DISTANCE) + if distance > limit: # shorten the trip rather than sprinting the diagonal + scale = limit / distance + target = QPoint(round(here.x() + dx * scale), round(here.y() + dy * scale)) + elif distance < 8: # already there — not worth a stroll + return None + return target + + def _movement_tick(self) -> None: + """One timer, two jobs — emotes play whatever the pet is doing, while + wandering only happens when it's otherwise unoccupied.""" + self._advance_emote() + self._wander_tick() + + def _wander_tick(self) -> None: + # Only stroll while genuinely idle: not mid-drag, not napping, not + # talking/listening, and not while a speech bubble is up (it would walk + # out from under it). A commanded `petctl move` ignores all of that + # except the drag — if Bolt says go, it goes. + busy = ( + not self._wander_enabled + or self._napping + or self._current_state != PetState.IDLE + or self._bubble.isVisible() + ) + if self._drag_offset is not None or (busy and not self._commanded_move): + self._stop_walking() + self._schedule_next_wander() # settle first, then wander + return + + if self._wander_target is None: + if time.monotonic() < self._next_wander_at: + return + self._wander_target = self._pick_wander_target() + if self._wander_target is None: + self._schedule_next_wander() + return + + here = self.pos() + dx = self._wander_target.x() - here.x() + dy = self._wander_target.y() - here.y() + distance = math.hypot(dx, dy) + step = max(1.0, config.PET_WANDER_SPEED * _WANDER_TICK_MS / 1000.0) + if distance <= step: + self.move(self._wander_target) + self._stop_walking() + self._schedule_next_wander() + self.snap_to_edge() + else: + self.move(round(here.x() + dx / distance * step), round(here.y() + dy / distance * step)) + self._bob_phase += 0.45 # little walk-cycle hop + self._bob_offset = int(round(-2.5 * abs(math.sin(self._bob_phase)))) + self.update() + self._reposition_bubble() + + # ── state / speech ────────────────────────────────────────────────── + + def set_state(self, state: PetState) -> None: + if state == self._current_state: + return + self._current_state = state + self.sprites.get(state).reset() + if state != PetState.IDLE and not self._commanded_move: + # Stand still while listening/thinking/talking — but not if Bolt + # just told it to walk somewhere: that command arrives mid-turn, + # and the reply (-> TALKING) lands a moment later. + self._stop_walking() + self.update() + + def say(self, text: str, duration_ms: int = 6000) -> None: + self._reposition_bubble() + self._bubble.show_text(text, duration_ms) + + def closeEvent(self, event) -> None: + self._bubble.close() + super().closeEvent(event) + + # ── animation ──────────────────────────────────────────────────────── + + def _advance_frame(self) -> None: + self.sprites.get(self._current_state).advance() + self.update() + + def paintEvent(self, _event) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + painter.setRenderHint(QPainter.SmoothPixmapTransform) + pixmap: Optional[QPixmap] = self.sprites.get(self._current_state).current() + if pixmap is None: + self._apply_input_mask(None, 0, 0) + return + # Non-square source art (e.g. the Kenney robot sprites) keeps its + # aspect ratio when scaled in SpriteSet, so it may be narrower or + # shorter than the (square) window — center it either way. + x = (self.width() - pixmap.width()) // 2 + y = (self.height() - pixmap.height()) // 2 + self._bob_offset + # The input mask tracks the resting position, not the emote/bob + # offset: rebuilding it every frame of a spin would be both expensive + # and visibly janky, and the offsets are only a few pixels. + self._apply_input_mask(pixmap, x, (self.height() - pixmap.height()) // 2) + + dx, dy, angle, scale = self._emote_transform() + if (dx, dy, angle, scale) == (0.0, 0.0, 0.0, 1.0): + painter.drawPixmap(x, y, pixmap) + return + # Rotate/scale about the sprite's own center so a spin doesn't orbit + # the window's corner. + center_x = x + pixmap.width() / 2 + center_y = y + pixmap.height() / 2 + transform = QTransform() + transform.translate(center_x + dx, center_y + dy) + transform.rotate(angle) + transform.scale(scale, scale) + transform.translate(-pixmap.width() / 2, -pixmap.height() / 2) + painter.setTransform(transform) + painter.drawPixmap(0, 0, pixmap) + + # ── drag / click-to-talk ───────────────────────────────────────────── + + def mousePressEvent(self, event) -> None: + if event.button() == Qt.LeftButton: + global_pos = event.globalPosition().toPoint() + self._drag_offset = global_pos - self.frameGeometry().topLeft() + self._press_pos = global_pos + self._dragged = False + self._stop_walking() # grabbing it interrupts a stroll at once + + def mouseMoveEvent(self, event) -> None: + if self._drag_offset is None: + return + global_pos = event.globalPosition().toPoint() + self.move(global_pos - self._drag_offset) + self._reposition_bubble() + if (global_pos - self._press_pos).manhattanLength() > _DRAG_THRESHOLD_PX: + self._dragged = True + + def mouseReleaseEvent(self, event) -> None: + if event.button() != Qt.LeftButton: + return + was_click = not self._dragged + self._drag_offset = None + self._press_pos = None + if was_click: + self.talk_requested.emit() + else: + self.snap_to_edge() # dropped near an edge -> tuck it flush diff --git a/bolt_pet/ui/sprite.py b/bolt_pet/ui/sprite.py new file mode 100644 index 0000000..7e2fc12 --- /dev/null +++ b/bolt_pet/ui/sprite.py @@ -0,0 +1,111 @@ +"""Sprite loading + frame animation. + +Convention: assets/sprites//*.png, frames played in filename-sorted +order (e.g. frame_00.png, frame_01.png, ...), looping. matches +bolt_pet.state.PetState values: idle, listening, thinking, talking. + +If a state's directory has no frames (real art not dropped in yet), falls +back to a small procedurally-drawn placeholder blob so the app still runs +end-to-end. Swap in real sprite sheets by pointing SPRITE_DIR at your own +folder (see assets/sprites/README.md) — no code changes needed as long as +the same per-state-subfolder-of-PNGs convention is followed. If your sheets +use a different layout (single grid image, etc.), tell me the format and +this loader can be adapted. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Optional + +from PySide6.QtCore import QSize, Qt +from PySide6.QtGui import QColor, QPainter, QPixmap + +from ..state import PetState + +DEFAULT_SPRITE_DIR = Path(__file__).resolve().parent.parent / "assets" / "sprites" + +# Placeholder palette per state, used only when no frames are found. +_PLACEHOLDER_COLORS = { + PetState.IDLE: QColor(120, 170, 240), + PetState.LISTENING: QColor(120, 220, 160), + PetState.THINKING: QColor(230, 190, 90), + PetState.TALKING: QColor(240, 130, 150), + PetState.ERROR: QColor(220, 90, 90), +} + + +def _placeholder_frames(state: PetState, size: int) -> list[QPixmap]: + """A tiny 2-frame "breathing" blob so idle/listening/etc. are visually + distinguishable even before real art exists.""" + color = _PLACEHOLDER_COLORS.get(state, QColor(150, 150, 150)) + frames = [] + for scale in (1.0, 0.92): + pixmap = QPixmap(size, size) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.Antialiasing) + painter.setBrush(color) + painter.setPen(Qt.NoPen) + margin = size * (1 - scale) / 2 + painter.drawEllipse(int(margin), int(margin), int(size * scale), int(size * scale)) + # simple eyes so it reads as a face, not just a circle + eye_r = max(2, size // 16) + eye_y = int(size * 0.42) + painter.setBrush(QColor(30, 30, 40)) + painter.drawEllipse(int(size * 0.36) - eye_r, eye_y - eye_r, eye_r * 2, eye_r * 2) + painter.drawEllipse(int(size * 0.64) - eye_r, eye_y - eye_r, eye_r * 2, eye_r * 2) + painter.end() + frames.append(pixmap) + return frames + + +class SpriteAnimation: + """One state's frame sequence + current playback position.""" + + def __init__(self, frames: list[QPixmap]): + self.frames = frames or [] + self._index = 0 + + def advance(self) -> None: + if self.frames: + self._index = (self._index + 1) % len(self.frames) + + def current(self) -> Optional[QPixmap]: + if not self.frames: + return None + return self.frames[self._index] + + def reset(self) -> None: + self._index = 0 + + +def _load_frames_from_dir(directory: Path, size: int) -> list[QPixmap]: + if not directory.is_dir(): + return [] + paths = sorted(directory.glob("*.png")) + sorted(directory.glob("*.PNG")) + frames = [] + for path in paths: + pixmap = QPixmap(str(path)) + if pixmap.isNull(): + continue + if pixmap.size() != QSize(size, size): + pixmap = pixmap.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + frames.append(pixmap) + return frames + + +class SpriteSet: + """All animations for every PetState, loaded from *sprite_dir*.""" + + def __init__(self, sprite_dir: Path = DEFAULT_SPRITE_DIR, size: int = 160): + self.size = size + self._animations: dict[PetState, SpriteAnimation] = {} + for state in PetState: + frames = _load_frames_from_dir(sprite_dir / state.value, size) + if not frames: + frames = _placeholder_frames(state, size) + self._animations[state] = SpriteAnimation(frames) + + def get(self, state: PetState) -> SpriteAnimation: + return self._animations[state] diff --git a/bolt_pet/ui/tray.py b/bolt_pet/ui/tray.py new file mode 100644 index 0000000..9bfcd13 --- /dev/null +++ b/bolt_pet/ui/tray.py @@ -0,0 +1,133 @@ +"""System tray icon — the pet window is frameless with no taskbar entry, so +this menu is the only always-available way to control or exit it: talk now, +mute, wander, click-through, nap, history, wake-word tuning, quit. + +Every entry is a plain callback passed in by ui/app.py; this file knows +nothing about the controller or the pet window. +""" + +from __future__ import annotations + +from typing import Callable, Optional + +from PySide6.QtCore import Qt +from PySide6.QtGui import QAction, QColor, QIcon, QPainter, QPixmap +from PySide6.QtWidgets import QMenu, QSystemTrayIcon + + +def _make_icon(muted: bool, napping: bool = False) -> QIcon: + pixmap = QPixmap(32, 32) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.Antialiasing) + if muted: + color = QColor(200, 60, 60) + elif napping: + color = QColor(120, 120, 140) + else: + color = QColor(120, 170, 240) + painter.setBrush(color) + painter.setPen(Qt.NoPen) + painter.drawEllipse(2, 2, 28, 28) + painter.end() + return QIcon(pixmap) + + +class PetTray(QSystemTrayIcon): + def __init__( + self, + on_talk_now: Callable[[], None], + on_toggle_mute: Callable[[], bool], + on_quit: Callable[[], None], + on_set_wander: Optional[Callable[[bool], None]] = None, + wander_enabled: bool = True, + on_set_click_through: Optional[Callable[[bool], None]] = None, + click_through_enabled: bool = False, + on_set_nap: Optional[Callable[[bool], None]] = None, + on_show_history: Optional[Callable[[], None]] = None, + on_show_wake_tuner: Optional[Callable[[], None]] = None, + parent=None, + ): + super().__init__(_make_icon(muted=False), parent) + self._on_toggle_mute = on_toggle_mute + self._muted = False + self._napping = False + self.setToolTip("Bolt") + + menu = QMenu() + self._talk_action = QAction("Talk now", menu) + self._talk_action.triggered.connect(on_talk_now) + menu.addAction(self._talk_action) + + self._mute_action = QAction("Mute mic", menu) + self._mute_action.setCheckable(True) + self._mute_action.triggered.connect(self._handle_toggle_mute) + menu.addAction(self._mute_action) + + self._nap_action = None + if on_set_nap is not None: + self._nap_action = QAction("Nap (no proactive noise)", menu) + self._nap_action.setCheckable(True) + self._nap_action.triggered.connect(lambda checked: on_set_nap(checked)) + menu.addAction(self._nap_action) + + menu.addSeparator() + + if on_set_wander is not None: + self._wander_action = QAction("Wander around", menu) + self._wander_action.setCheckable(True) + self._wander_action.setChecked(wander_enabled) + self._wander_action.triggered.connect(lambda checked: on_set_wander(checked)) + menu.addAction(self._wander_action) + + if on_set_click_through is not None: + self._click_through_action = QAction("Click through the pet", menu) + self._click_through_action.setCheckable(True) + self._click_through_action.setChecked(click_through_enabled) + self._click_through_action.setToolTip( + "Ignore the mouse entirely — control it from this menu instead." + ) + self._click_through_action.triggered.connect(lambda checked: on_set_click_through(checked)) + menu.addAction(self._click_through_action) + + menu.addSeparator() + + if on_show_history is not None: + history_action = QAction("History…", menu) + history_action.triggered.connect(on_show_history) + menu.addAction(history_action) + + if on_show_wake_tuner is not None: + tuner_action = QAction("Wake word tuning…", menu) + tuner_action.triggered.connect(on_show_wake_tuner) + menu.addAction(tuner_action) + + menu.addSeparator() + quit_action = QAction("Quit", menu) + quit_action.triggered.connect(on_quit) + menu.addAction(quit_action) + + self.setContextMenu(menu) + self.show() + + def _handle_toggle_mute(self) -> None: + self._muted = self._on_toggle_mute() + self._mute_action.setChecked(self._muted) + self._refresh_icon() + + def set_napping(self, napping: bool) -> None: + """Reflect a nap the *controller* decided on (quiet hours, fullscreen, + or a petctl command) — not just ones clicked here.""" + self._napping = napping + if self._nap_action is not None: + self._nap_action.setChecked(napping) + self._refresh_icon() + + def _refresh_icon(self) -> None: + self.setIcon(_make_icon(self._muted, self._napping)) + if self._muted: + self.setToolTip("Bolt (muted)") + elif self._napping: + self.setToolTip("Bolt (napping)") + else: + self.setToolTip("Bolt") diff --git a/bolt_pet/ui/wake_tuner.py b/bolt_pet/ui/wake_tuner.py new file mode 100644 index 0000000..a463b3b --- /dev/null +++ b/bolt_pet/ui/wake_tuner.py @@ -0,0 +1,118 @@ +"""Wake-word sensitivity tuner. + +WAKE_WORD_THRESHOLD is otherwise a number you guess at in .env, restart, and +then test by saying "thunderbolt" at your computer repeatedly. This window +makes it evidence-based: a live peak-score readout while you talk, a rolling +list of near misses (frames that scored just under the threshold — i.e. the +times it *nearly* heard you), and a slider that takes effect immediately, +mid-listen, without a restart. + +The controller owns the threshold; this window is a view over it. +""" + +from __future__ import annotations + +import time +from typing import Callable + +from PySide6.QtCore import Qt, QTimer +from PySide6.QtWidgets import ( + QDialog, QHBoxLayout, QLabel, QListWidget, QPushButton, QSlider, QVBoxLayout, +) + +_SLIDER_SCALE = 100 # QSlider is integer-only; threshold is 0.00-1.00 + + +class WakeTunerWindow(QDialog): + def __init__( + self, + get_threshold: Callable[[], float], + set_threshold: Callable[[float], None], + get_stats: Callable[[], dict], + on_reset: Callable[[], None], + ): + super().__init__() + self._get_threshold = get_threshold + self._set_threshold = set_threshold + self._get_stats = get_stats + self._on_reset = on_reset + + self.setWindowTitle("Bolt — wake word tuning") + self.resize(460, 380) + + self._threshold_label = QLabel() + self._slider = QSlider(Qt.Horizontal) + self._slider.setRange(5, 99) + self._slider.setValue(int(round(get_threshold() * _SLIDER_SCALE))) + self._slider.valueChanged.connect(self._threshold_changed) + + self._peak_label = QLabel("Peak score since reset: —") + self._peak_label.setToolTip( + 'Say "thunderbolt" a few times and watch this. Set the threshold ' + "just below the peak you can hit reliably." + ) + + self._misses = QListWidget() + + reset_button = QPushButton("Reset stats") + reset_button.clicked.connect(self._reset) + close_button = QPushButton("Close") + close_button.clicked.connect(self.close) + close_button.setDefault(True) + + buttons = QHBoxLayout() + buttons.addWidget(reset_button) + buttons.addStretch(1) + buttons.addWidget(close_button) + + layout = QVBoxLayout(self) + layout.addWidget(self._threshold_label) + layout.addWidget(self._slider) + layout.addWidget(self._peak_label) + layout.addWidget(QLabel("Near misses (heard something, didn't quite fire):")) + layout.addWidget(self._misses) + layout.addLayout(buttons) + + # Polled rather than signal-driven: scores arrive ~12x/second on the + # audio thread, and a queued signal per frame to repaint a label is + # more traffic than this is worth. + self._timer = QTimer(self) + self._timer.timeout.connect(self.refresh) + self._update_threshold_label() + + def _threshold_changed(self, value: int) -> None: + self._set_threshold(value / _SLIDER_SCALE) + self._update_threshold_label() + + def _update_threshold_label(self) -> None: + threshold = self._slider.value() / _SLIDER_SCALE + self._threshold_label.setText( + f"Threshold: {threshold:.2f} (lower = more sensitive, more false triggers)" + ) + + def _reset(self) -> None: + self._on_reset() + self.refresh() + + def refresh(self) -> None: + stats = self._get_stats() or {} + peak = stats.get("peak", 0.0) + self._peak_label.setText(f"Peak score since reset: {peak:.3f}") + self._misses.clear() + for timestamp, score, threshold in reversed(stats.get("near_misses", [])): + when = time.strftime("%H:%M:%S", time.localtime(timestamp)) + self._misses.addItem(f"{when} scored {score:.3f} (threshold {threshold:.2f})") + if self._misses.count() == 0: + self._misses.addItem("Nothing yet — say the wake phrase a few times.") + + def show_refreshed(self) -> None: + self._slider.setValue(int(round(self._get_threshold() * _SLIDER_SCALE))) + self.refresh() + self.show() + self.raise_() + self.activateWindow() + self._timer.start(500) + + def closeEvent(self, event) -> None: + self._timer.stop() + super().closeEvent(event) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7c7cc56 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,37 @@ +# Desktop UI +PySide6>=6.6 + +# Audio I/O (mic capture + speaker playback) — cross-platform via PortAudio. +# On Linux you may also need the system package: sudo apt install libportaudio2 +sounddevice>=0.4.6 +numpy>=1.24 + +# HTTP client to the Bolt desk API +requests>=2.31 + +# Wake-word detection (local, offline after first run) — runs the +# custom-trained thunderbolt.onnx model shipped in this repo, same runtime +# as the main repo's desk_client/bolt_desk.py (bolt.onnx). First use +# downloads openwakeword's feature-extraction sub-models (~few MB, cached +# under the package's own resources/ dir afterward) — needs internet once. +openwakeword + +# Offline TTS fallback if ElevenLabs isn't configured or a request fails. +# Uses SAPI5 on Windows, NSSpeechSynthesizer on macOS, espeak on Linux +# (Linux also needs: sudo apt install espeak-ng). +pyttsx3>=2.90 + +# Global push-to-talk hotkey (PUSH_TO_TALK_HOTKEY). Optional: the pet +# degrades to wake-word + tray + click if it's missing or if the session +# won't allow a global key hook (most Wayland setups; macOS needs +# Accessibility permission). +pynput>=1.7 + +# Optional — only needed for scripts/slice_spritesheet.py (converting a +# grid sprite sheet into the per-frame-PNG convention sprite.py expects). +# Not imported by the app itself. +Pillow>=10.0 + +# Test runner (tests/ — pure logic, no audio hardware or display needed; +# run with QT_QPA_PLATFORM=offscreen). +pytest>=8.0 diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..599df02 --- /dev/null +++ b/run.bat @@ -0,0 +1,11 @@ +@echo off +REM Convenience launcher for Windows. +cd /d "%~dp0" + +if not exist .venv ( + python -m venv .venv + .venv\Scripts\pip install --upgrade pip + .venv\Scripts\pip install -r requirements.txt +) + +.venv\Scripts\python -m bolt_pet diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..3fe538e --- /dev/null +++ b/run.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Convenience launcher for macOS/Linux. +set -euo pipefail +cd "$(dirname "$0")" + +if [ ! -d .venv ]; then + python3 -m venv .venv + ./.venv/bin/pip install --upgrade pip + ./.venv/bin/pip install -r requirements.txt +fi + +exec ./.venv/bin/python -m bolt_pet diff --git a/scripts/slice_spritesheet.py b/scripts/slice_spritesheet.py new file mode 100755 index 0000000..ff67744 --- /dev/null +++ b/scripts/slice_spritesheet.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Cut a grid-based sprite sheet (rows x cols of equal-size frames in one +PNG) into the assets/sprites//frame_NN.png convention this project's +sprite loader expects. + +Usage: + python scripts/slice_spritesheet.py idle_sheet.png assets/sprites/idle --cols 6 --rows 1 +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from PIL import Image + + +def slice_sheet(sheet_path: Path, out_dir: Path, cols: int, rows: int) -> int: + sheet = Image.open(sheet_path).convert("RGBA") + frame_w = sheet.width // cols + frame_h = sheet.height // rows + if frame_w == 0 or frame_h == 0: + raise ValueError(f"sheet is {sheet.width}x{sheet.height}, too small for {cols}x{rows} frames") + + out_dir.mkdir(parents=True, exist_ok=True) + count = 0 + for row in range(rows): + for col in range(cols): + box = (col * frame_w, row * frame_h, (col + 1) * frame_w, (row + 1) * frame_h) + frame = sheet.crop(box) + frame.save(out_dir / f"frame_{count:02d}.png") + count += 1 + return count + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("sheet", type=Path, help="path to the grid sprite sheet PNG") + parser.add_argument("out_dir", type=Path, help="e.g. assets/sprites/idle") + parser.add_argument("--cols", type=int, required=True) + parser.add_argument("--rows", type=int, default=1) + args = parser.parse_args() + + count = slice_sheet(args.sheet, args.out_dir, args.cols, args.rows) + print(f"Wrote {count} frames to {args.out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_barge_in.py b/tests/test_barge_in.py new file mode 100644 index 0000000..4e76962 --- /dev/null +++ b/tests/test_barge_in.py @@ -0,0 +1,65 @@ +"""Barge-in detection, driven by a fake mic stream (no audio hardware).""" + +import sys +from pathlib import Path + +import numpy as np + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from bolt_pet.audio.barge_in import BargeInDetector + + +class FakeStream: + """Yields frames of a given amplitude, mimicking sounddevice's + (data, overflowed) 2-D int16 return shape.""" + + def __init__(self, amplitudes): + self._amplitudes = list(amplitudes) + + def read(self, frames): + amplitude = self._amplitudes.pop(0) if self._amplitudes else 0 + data = np.full((frames, 1), amplitude, dtype=np.int16) + return data, False + + +def test_silence_never_interrupts(): + detector = BargeInDetector(FakeStream([0] * 20), threshold=1000, required_frames=3) + assert not any(detector.check() for _ in range(20)) + + +def test_sustained_speech_interrupts_after_the_required_frames(): + detector = BargeInDetector(FakeStream([2000] * 5), threshold=1000, required_frames=3) + assert detector.check() is False + assert detector.check() is False + assert detector.check() is True + + +def test_a_single_thump_does_not_interrupt(): + # loud, quiet, loud, quiet ... never three in a row + detector = BargeInDetector(FakeStream([2000, 0, 2000, 0, 2000, 0]), threshold=1000, required_frames=3) + assert not any(detector.check() for _ in range(6)) + + +def test_counter_resets_after_a_quiet_frame(): + detector = BargeInDetector(FakeStream([2000, 2000, 0, 2000, 2000, 2000]), threshold=1000, required_frames=3) + results = [detector.check() for _ in range(6)] + assert results == [False, False, False, False, False, True] + + +def test_reset_clears_progress(): + detector = BargeInDetector(FakeStream([2000] * 6), threshold=1000, required_frames=3) + detector.check() + detector.check() + detector.reset() + assert detector.check() is False + assert detector.loud_frames == 1 + + +def test_a_mic_error_mid_playback_is_not_fatal(): + class BrokenStream: + def read(self, frames): + raise OSError("device disappeared") + + detector = BargeInDetector(BrokenStream(), threshold=1000, required_frames=1) + assert detector.check() is False diff --git a/tests/test_controller.py b/tests/test_controller.py new file mode 100644 index 0000000..db5e782 --- /dev/null +++ b/tests/test_controller.py @@ -0,0 +1,195 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import numpy as np +import pytest +from PySide6.QtWidgets import QApplication + +from bolt_pet import controller as controller_mod +from bolt_pet.state import PetState + +# A QApplication is required before any QObject with signals can be built. +_app = QApplication.instance() or QApplication(["test"]) + + +@pytest.fixture(autouse=True) +def no_screen_probes(monkeypatch): + # The real ones shell out to xprop/osascript — irrelevant here, and slow + # (or hung) on a headless box. + monkeypatch.setattr(controller_mod.screen_context, "context_for", lambda text: text) + monkeypatch.setattr(controller_mod.screen_context, "is_fullscreen_active", lambda: False) + + +@pytest.fixture +def ctrl(): + return controller_mod.PetController() + + +def _capture(signal): + events = [] + signal.connect(lambda *a: events.append(a[0] if len(a) == 1 else a)) + return events + + +# ── conversation turn ──────────────────────────────────────────────────────── + +def test_full_turn_happy_path(monkeypatch, ctrl): + states = _capture(ctrl.state_changed) + said = _capture(ctrl.said) + + monkeypatch.setattr(controller_mod.mic, "record_utterance", lambda *a, **k: np.zeros(10, dtype=np.int16)) + monkeypatch.setattr(controller_mod.stt, "transcribe", lambda pcm: "what's the weather") + monkeypatch.setattr(controller_mod.server_client, "converse", lambda text, on_command=None: "sunny and 72") + spoken = [] + monkeypatch.setattr(controller_mod.tts, "speak", + lambda text, on_error=None, should_stop=None: (spoken.append(text), True)[1]) + + ctrl._handle_conversation_turn() + + assert states == ["listening", "thinking", "talking", "idle"] + assert said == ["sunny and 72"] + assert spoken == ["sunny and 72"] + assert ctrl._state.state == PetState.IDLE + + +def test_turn_with_nothing_heard_returns_to_idle_without_calling_server(monkeypatch, ctrl): + states = _capture(ctrl.state_changed) + monkeypatch.setattr(controller_mod.mic, "record_utterance", lambda *a, **k: None) + called = {"n": 0} + monkeypatch.setattr(controller_mod.server_client, "converse", + lambda text, on_command=None: called.__setitem__("n", called["n"] + 1)) + + ctrl._handle_conversation_turn() + + assert states == ["listening", "idle"] + assert called["n"] == 0 + + +def test_turn_with_empty_transcript_returns_to_idle(monkeypatch, ctrl): + states = _capture(ctrl.state_changed) + monkeypatch.setattr(controller_mod.mic, "record_utterance", lambda *a, **k: np.zeros(10, dtype=np.int16)) + monkeypatch.setattr(controller_mod.stt, "transcribe", lambda pcm: "") + + ctrl._handle_conversation_turn() + + assert states == ["listening", "thinking", "idle"] + + +def test_turn_with_stt_error_flashes_error_then_idle(monkeypatch, ctrl): + states = _capture(ctrl.state_changed) + logs = _capture(ctrl.log) + monkeypatch.setattr(controller_mod.mic, "record_utterance", lambda *a, **k: np.zeros(10, dtype=np.int16)) + + def boom(pcm): + raise controller_mod.stt.SttError("deepgram is down") + monkeypatch.setattr(controller_mod.stt, "transcribe", boom) + + ctrl._handle_conversation_turn() + + assert states == ["listening", "thinking", "error", "idle"] + assert any("deepgram is down" in msg for msg in logs) + + +def test_turn_with_server_error_flashes_error_then_idle(monkeypatch, ctrl): + states = _capture(ctrl.state_changed) + monkeypatch.setattr(controller_mod.mic, "record_utterance", lambda *a, **k: np.zeros(10, dtype=np.int16)) + monkeypatch.setattr(controller_mod.stt, "transcribe", lambda pcm: "hello") + + def boom(text, on_command=None): + raise controller_mod.server_client.ServerError("server is down") + monkeypatch.setattr(controller_mod.server_client, "converse", boom) + + ctrl._handle_conversation_turn() + + assert states == ["listening", "thinking", "error", "idle"] + + +# ── mute / talk-now / wake-or-click disambiguation ────────────────────────── + +def test_toggle_mute_flips_and_returns_new_state(ctrl): + assert ctrl.toggle_mute() is True + assert ctrl.toggle_mute() is False + + +def test_wait_for_wake_or_click_true_on_phrase_detection(monkeypatch, ctrl): + monkeypatch.setattr(controller_mod.wake_word, "listen_for_wake_word", lambda *a, **k: True) + ctrl._stream = object() + assert ctrl._wait_for_wake_or_click() is True + + +def test_wait_for_wake_or_click_true_on_manual_trigger(monkeypatch, ctrl): + # listen_for_wake_word returns False because should_continue() went + # false (the talk_now flag got set) — controller must still recognize + # this as "proceed", not "spurious wakeup". + def fake_listen(stream, should_continue, on_tick=None, **kwargs): + ctrl.request_talk_now() + should_continue() # simulate the loop noticing the flag + return False + + monkeypatch.setattr(controller_mod.wake_word, "listen_for_wake_word", fake_listen) + ctrl._stream = object() + assert ctrl._wait_for_wake_or_click() is True + assert not ctrl._talk_now.is_set() # cleared after being consumed + + +def test_wait_for_wake_or_click_false_on_shutdown(monkeypatch, ctrl): + def fake_listen(stream, should_continue, on_tick=None, **kwargs): + ctrl.stop() + return False + monkeypatch.setattr(controller_mod.wake_word, "listen_for_wake_word", fake_listen) + ctrl._stream = object() + assert ctrl._wait_for_wake_or_click() is False + + +# ── heartbeat / proactive announcements ───────────────────────────────────── + +def test_heartbeat_speaks_a_pending_announcement_when_idle(monkeypatch, ctrl): + monkeypatch.setattr(controller_mod.config, "HEARTBEAT_INTERVAL_SECONDS", 0) + monkeypatch.setattr(controller_mod.server_client, "report_status", lambda: "don't forget your 3pm") + spoken = [] + monkeypatch.setattr(controller_mod.tts, "speak", + lambda text, on_error=None, should_stop=None: (spoken.append(text), True)[1]) + said = _capture(ctrl.said) + + ctrl._maybe_heartbeat() + + assert spoken == ["don't forget your 3pm"] + assert said == ["don't forget your 3pm"] + assert ctrl._state.state == PetState.IDLE + + +def test_heartbeat_does_nothing_when_not_idle(monkeypatch, ctrl): + monkeypatch.setattr(controller_mod.config, "HEARTBEAT_INTERVAL_SECONDS", 0) + ctrl._state.transition(PetState.LISTENING) + called = {"n": 0} + monkeypatch.setattr(controller_mod.server_client, "report_status", + lambda: called.__setitem__("n", called["n"] + 1)) + + ctrl._maybe_heartbeat() + + assert called["n"] == 0 + + +def test_heartbeat_respects_the_interval(monkeypatch, ctrl): + # Deterministic fake clock — time.monotonic()'s absolute value is + # arbitrary (often system uptime), so asserting behavior relative to it + # without controlling it would be flaky. + fake_now = {"t": 1000.0} + monkeypatch.setattr(controller_mod.time, "monotonic", lambda: fake_now["t"]) + monkeypatch.setattr(controller_mod.config, "HEARTBEAT_INTERVAL_SECONDS", 60) + called = {"n": 0} + monkeypatch.setattr(controller_mod.server_client, "report_status", + lambda: called.__setitem__("n", called["n"] + 1)) + + ctrl._maybe_heartbeat() # last_heartbeat starts at 0.0 -> elapsed is huge -> runs + assert called["n"] == 1 + + fake_now["t"] += 10 # only 10s later — inside the 60s interval + ctrl._maybe_heartbeat() + assert called["n"] == 1 # skipped + + fake_now["t"] += 60 # now well past the interval + ctrl._maybe_heartbeat() + assert called["n"] == 2 diff --git a/tests/test_controller_features.py b/tests/test_controller_features.py new file mode 100644 index 0000000..c710acb --- /dev/null +++ b/tests/test_controller_features.py @@ -0,0 +1,243 @@ +"""Controller-level wiring for the newer behaviours: petctl routing, +barge-in follow-up, quiet hours, screen context, notification forwarding and +the live wake threshold. + +Needs a QApplication (signals), so run with QT_QPA_PLATFORM=offscreen. +""" + +import sys +from pathlib import Path + +import numpy as np +import pytest +from PySide6.QtWidgets import QApplication + +from bolt_pet import controller as controller_mod +from bolt_pet.notifications import Notification +from bolt_pet.state import PetState + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +_app = QApplication.instance() or QApplication(["test"]) + + +@pytest.fixture(autouse=True) +def no_screen_probes(monkeypatch): + monkeypatch.setattr(controller_mod.screen_context, "is_fullscreen_active", lambda: False) + monkeypatch.setattr(controller_mod.screen_context, "context_for", lambda text: text) + + +@pytest.fixture +def ctrl(): + return controller_mod.PetController() + + +def _capture(signal): + events = [] + signal.connect(lambda *a: events.append(a[0] if len(a) == 1 else a)) + return events + + +# ── petctl routing ────────────────────────────────────────────────────────── + +def test_petctl_commands_never_reach_the_shell(monkeypatch, ctrl): + ran = [] + monkeypatch.setattr(controller_mod.server_client, "run_local_command", lambda cmd: ran.append(cmd)) + actions = _capture(ctrl.action) + + output = ctrl._handle_command("petctl move top-left") + + assert ran == [] + assert actions == [{"action": "move", "anchor": "top-left"}] + assert "top-left" in output + + +def test_ordinary_commands_still_run_locally(monkeypatch, ctrl): + ran = [] + monkeypatch.setattr(controller_mod.server_client, "run_local_command", + lambda cmd: ran.append(cmd) or "[exit 0]\n") + actions = _capture(ctrl.action) + + ctrl._handle_command("df -h /") + + assert ran == ["df -h /"] + assert actions == [] + + +def test_bad_petctl_syntax_is_reported_back_not_executed(monkeypatch, ctrl): + ran = [] + monkeypatch.setattr(controller_mod.server_client, "run_local_command", lambda cmd: ran.append(cmd)) + output = ctrl._handle_command("petctl move sideways") + assert ran == [] + assert "[pet]" in output + + +def test_petctl_nap_also_flips_the_controller_state(ctrl): + napping = _capture(ctrl.napping) + ctrl._handle_command("petctl nap on") + assert napping == [True] + assert ctrl._napping is True + + +# ── barge-in ──────────────────────────────────────────────────────────────── + +def test_interrupted_playback_queues_an_immediate_next_turn(monkeypatch, ctrl): + logs = _capture(ctrl.log) + monkeypatch.setattr(controller_mod.tts, "speak", + lambda text, on_error=None, should_stop=None: False) # interrupted + + ctrl._speak("a very long explanation") + + assert ctrl._talk_now.is_set() # loop picks the next turn up without a wake word + assert any("Interrupted" in message for message in logs) + + +def test_uninterrupted_playback_does_not_queue_a_turn(monkeypatch, ctrl): + monkeypatch.setattr(controller_mod.tts, "speak", + lambda text, on_error=None, should_stop=None: True) + ctrl._speak("short answer") + assert not ctrl._talk_now.is_set() + + +def test_speech_is_recorded_in_the_history(monkeypatch, ctrl): + monkeypatch.setattr(controller_mod.tts, "speak", + lambda text, on_error=None, should_stop=None: True) + ctrl._speak("**bold** reply") + assert ctrl.history.last().text == "**bold** reply" # raw, for copy/paste + + +# ── screen context ────────────────────────────────────────────────────────── + +def test_the_active_window_rides_along_with_the_utterance(monkeypatch, ctrl): + monkeypatch.setattr(controller_mod.mic, "record_utterance", + lambda *a, **k: np.zeros(10, dtype=np.int16)) + monkeypatch.setattr(controller_mod.stt, "transcribe", lambda pcm: "what's this error?") + monkeypatch.setattr(controller_mod.screen_context, "context_for", + lambda text: f"{text}\n\n[on screen right now: app.py]") + monkeypatch.setattr(controller_mod.tts, "speak", + lambda text, on_error=None, should_stop=None: True) + sent = [] + monkeypatch.setattr(controller_mod.server_client, "converse", + lambda text, on_command=None: sent.append(text) or "that's a KeyError") + + ctrl._handle_conversation_turn() + + assert "[on screen right now: app.py]" in sent[0] + # ...but the *history* keeps what you actually said, not the annotation. + assert ctrl.history.entries()[0].text == "what's this error?" + + +# ── quiet hours / do-not-disturb ──────────────────────────────────────────── + +def test_quiet_hours_suppress_the_heartbeat(monkeypatch, ctrl): + monkeypatch.setattr(controller_mod.config, "HEARTBEAT_INTERVAL_SECONDS", 0) + monkeypatch.setattr(controller_mod.config, "QUIET_HOURS", "00:00-23:59") + called = {"n": 0} + monkeypatch.setattr(controller_mod.server_client, "report_status", + lambda: called.__setitem__("n", called["n"] + 1)) + + ctrl._maybe_heartbeat() + + assert called["n"] == 0 + assert ctrl._napping is True + + +def test_fullscreen_triggers_do_not_disturb(monkeypatch, ctrl): + monkeypatch.setattr(controller_mod.config, "QUIET_HOURS", "") + monkeypatch.setattr(controller_mod.config, "DND_ON_FULLSCREEN", True) + monkeypatch.setattr(controller_mod.screen_context, "is_fullscreen_active", lambda: True) + napping = _capture(ctrl.napping) + + ctrl._refresh_nap_state() + + assert napping == [True] + + +def test_a_manual_nap_overrides_the_schedule(monkeypatch, ctrl): + monkeypatch.setattr(controller_mod.config, "QUIET_HOURS", "") + ctrl.set_napping(True) + ctrl._last_nap_check = 0.0 + ctrl._refresh_nap_state() + assert ctrl._napping is True # the schedule doesn't wake it back up + + ctrl.set_napping(None) # back on schedule + ctrl._last_nap_check = 0.0 + ctrl._refresh_nap_state() + assert ctrl._napping is False + + +def test_napping_still_answers_when_spoken_to(monkeypatch, ctrl): + monkeypatch.setattr(controller_mod.mic, "record_utterance", + lambda *a, **k: np.zeros(10, dtype=np.int16)) + monkeypatch.setattr(controller_mod.stt, "transcribe", lambda pcm: "you awake?") + monkeypatch.setattr(controller_mod.server_client, "converse", + lambda text, on_command=None: "always") + spoken = [] + monkeypatch.setattr(controller_mod.tts, "speak", + lambda text, on_error=None, should_stop=None: spoken.append(text) or True) + ctrl.set_napping(True) + + ctrl._handle_conversation_turn() + + assert spoken == ["always"] + + +# ── notification bridge ───────────────────────────────────────────────────── + +def test_notifications_are_forwarded_and_spoken(monkeypatch, ctrl): + monkeypatch.setattr(controller_mod.config, "NOTIFICATION_MIN_INTERVAL_SECONDS", 0) + ctrl._notification_gate = controller_mod.notifications.NotificationGate("", 0) + sent, spoken = [], [] + monkeypatch.setattr(controller_mod.server_client, "converse", + lambda text, on_command=None: sent.append(text) or "your build is green") + monkeypatch.setattr(controller_mod.tts, "speak", + lambda text, on_error=None, should_stop=None: spoken.append(text) or True) + + ctrl._queue_notification(Notification(app="CI", summary="Build finished", body="")) + ctrl._drain_notifications() + + assert "CI: Build finished" in sent[0] + assert spoken == ["your build is green"] + assert ctrl._state.state == PetState.IDLE + + +def test_filtered_out_notifications_are_never_queued(ctrl): + ctrl._notification_gate = controller_mod.notifications.NotificationGate("deploy", 0) + ctrl._queue_notification(Notification(app="Chat", summary="lunch?", body="")) + assert ctrl._pending_notifications == [] + + +def test_notifications_are_not_forwarded_while_napping(monkeypatch, ctrl): + ctrl._notification_gate = controller_mod.notifications.NotificationGate("", 0) + called = {"n": 0} + monkeypatch.setattr(controller_mod.server_client, "converse", + lambda text, on_command=None: called.__setitem__("n", called["n"] + 1)) + ctrl.set_napping(True) + + ctrl._queue_notification(Notification(app="CI", summary="Build finished", body="")) + ctrl._drain_notifications() + + assert called["n"] == 0 + + +# ── wake threshold ────────────────────────────────────────────────────────── + +def test_threshold_is_live_and_clamped(ctrl): + ctrl.set_wake_threshold(0.72) + assert ctrl.wake_threshold() == pytest.approx(0.72) + ctrl.set_wake_threshold(5) + assert ctrl.wake_threshold() == pytest.approx(0.99) + ctrl.set_wake_threshold(-1) + assert ctrl.wake_threshold() == pytest.approx(0.01) + + +def test_near_misses_are_recorded_for_the_tuner(ctrl): + ctrl.set_wake_threshold(0.5) + ctrl._observe_wake_score(0.42, 0.5) # near miss + ctrl._observe_wake_score(0.01, 0.5) # background noise, not interesting + stats = ctrl.wake_stats() + assert len(stats["near_misses"]) == 1 + assert stats["peak"] == pytest.approx(0.42) + + ctrl.reset_wake_stats() + assert ctrl.wake_stats()["near_misses"] == [] diff --git a/tests/test_history_and_hotkey.py b/tests/test_history_and_hotkey.py new file mode 100644 index 0000000..895acaa --- /dev/null +++ b/tests/test_history_and_hotkey.py @@ -0,0 +1,92 @@ +"""Conversation scrollback + push-to-talk hotkey parsing.""" + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from bolt_pet import history as history_mod +from bolt_pet.hotkey import GlobalHotkey, HotkeyError, to_pynput_spec + + +# ── history ───────────────────────────────────────────────────────────────── + +def test_keeps_entries_in_order(): + log = history_mod.ConversationHistory(limit=10) + log.add(history_mod.USER, "what's the weather") + log.add(history_mod.PET, "sunny and 72") + assert [e.text for e in log.entries()] == ["what's the weather", "sunny and 72"] + + +def test_drops_the_oldest_past_the_limit(): + log = history_mod.ConversationHistory(limit=2) + for i in range(5): + log.add(history_mod.PET, f"line {i}") + assert [e.text for e in log.entries()] == ["line 3", "line 4"] + + +def test_blank_entries_are_ignored(): + log = history_mod.ConversationHistory() + assert log.add(history_mod.PET, " ") is None + assert len(log) == 0 + + +def test_last_can_filter_by_role(): + log = history_mod.ConversationHistory() + log.add(history_mod.USER, "hello") + log.add(history_mod.PET, "hi there") + log.add(history_mod.USER, "still there?") + assert log.last().text == "still there?" + assert log.last(history_mod.PET).text == "hi there" + + +def test_as_text_is_copyable_transcript(): + log = history_mod.ConversationHistory() + log.add(history_mod.USER, "ping") + log.add(history_mod.PET, "pong") + assert log.as_text() == "You: ping\nBolt: pong" + + +def test_timestamps_are_rendered_when_present(): + log = history_mod.ConversationHistory() + log.add(history_mod.PET, "pong", timestamp=1710000000.0) + assert log.as_text(clock=lambda t: "12:00:00") == "[12:00:00] Bolt: pong" + + +def test_clear_empties_the_log(): + log = history_mod.ConversationHistory() + log.add(history_mod.PET, "pong") + log.clear() + assert len(log) == 0 + + +# ── hotkey ────────────────────────────────────────────────────────────────── + +def test_translates_a_readable_spec_to_pynput_syntax(): + assert to_pynput_spec("ctrl+alt+space") == "++" + assert to_pynput_spec("ctrl+shift+b") == "++b" + + +def test_accepts_the_names_people_actually_type(): + assert to_pynput_spec("Control+Option+Space") == "++" + assert to_pynput_spec("super+k") == "+k" + + +def test_empty_spec_is_an_error_at_parse_time(): + with pytest.raises(HotkeyError): + to_pynput_spec("") + + +def test_disabled_hotkey_starts_cleanly_and_reports_nothing(): + hotkey = GlobalHotkey("", lambda: None) + assert hotkey.start() is None + assert hotkey.running is False + + +def test_invalid_hotkey_reports_instead_of_raising(): + hotkey = GlobalHotkey("+++", lambda: None) + problem = hotkey.start() + assert problem and "invalid" in problem + assert hotkey.running is False diff --git a/tests/test_mic.py b/tests/test_mic.py new file mode 100644 index 0000000..f20a9de --- /dev/null +++ b/tests/test_mic.py @@ -0,0 +1,114 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import numpy as np +import pytest + +from bolt_pet.audio import mic + +FRAME_LEN = 320 # small for fast tests +SAMPLE_RATE = 8000 + + +class _ScriptedStream: + """Replays a fixed list of frames, then quiet forever.""" + + def __init__(self, frames): + self._frames = list(frames) + + def read(self, frames): + if self._frames: + frame = self._frames.pop(0) + else: + frame = np.zeros(FRAME_LEN, dtype=np.int16) + return frame.reshape(-1, 1), False + + +def _loud(n=1): + return [np.full(FRAME_LEN, 5000, dtype=np.int16) for _ in range(n)] + + +def _quiet(n=1): + return [np.zeros(FRAME_LEN, dtype=np.int16) for _ in range(n)] + + +def test_returns_none_when_nothing_ever_gets_loud(): + stream = _ScriptedStream(_quiet(50)) + calls = {"i": 0} + + def should_continue(): + calls["i"] += 1 + return calls["i"] <= 50 + + result = mic.record_utterance( + stream, should_continue=should_continue, + rms_threshold=300, silence_end_sec=0.5, max_utterance_s=5, min_utterance_s=0.1, + frame_len=FRAME_LEN, sample_rate=SAMPLE_RATE, + ) + assert result is None + + +def test_captures_speech_and_stops_after_trailing_silence(): + # speech, then enough silence to cross the silence_end_sec threshold + silence_end_sec = 0.5 + silence_limit_frames = int(silence_end_sec * SAMPLE_RATE / FRAME_LEN) + frames = _loud(5) + _quiet(silence_limit_frames + 2) + stream = _ScriptedStream(frames) + + result = mic.record_utterance( + stream, rms_threshold=300, silence_end_sec=silence_end_sec, + max_utterance_s=5, min_utterance_s=0.05, + frame_len=FRAME_LEN, sample_rate=SAMPLE_RATE, + ) + assert result is not None + # captured the loud frames plus the silence up to (and including) the + # frame that crossed the silence-end threshold, but not endless silence + assert len(result) < len(frames) * FRAME_LEN + + +def test_returns_none_if_utterance_shorter_than_minimum(): + frames = _loud(1) + _quiet(2) # crosses silence limit almost immediately + stream = _ScriptedStream(frames) + result = mic.record_utterance( + stream, rms_threshold=300, silence_end_sec=0.05, + max_utterance_s=5, min_utterance_s=5.0, # impossible to satisfy + frame_len=FRAME_LEN, sample_rate=SAMPLE_RATE, + ) + assert result is None + + +def test_stops_at_max_utterance_even_without_silence(): + max_utterance_s = 0.5 + max_frames = int(max_utterance_s * SAMPLE_RATE / FRAME_LEN) + stream = _ScriptedStream(_loud(max_frames + 20)) # never goes quiet + + result = mic.record_utterance( + stream, rms_threshold=300, silence_end_sec=10.0, # would never trigger + max_utterance_s=max_utterance_s, min_utterance_s=0.01, + frame_len=FRAME_LEN, sample_rate=SAMPLE_RATE, + ) + assert result is not None + assert len(result) == max_frames * FRAME_LEN + + +def test_returns_none_when_should_continue_stops_before_speech(): + stream = _ScriptedStream(_quiet(100)) + result = mic.record_utterance(stream, should_continue=lambda: False, + frame_len=FRAME_LEN, sample_rate=SAMPLE_RATE) + assert result is None + + +def test_pcm_to_wav_bytes_round_trips_via_wave_module(): + import wave + import io + + pcm = np.array([0, 100, -100, 32767, -32768], dtype=np.int16) + wav_bytes = mic.pcm_to_wav_bytes(pcm, sample_rate=16000) + with wave.open(io.BytesIO(wav_bytes), "rb") as wf: + assert wf.getnchannels() == 1 + assert wf.getsampwidth() == 2 + assert wf.getframerate() == 16000 + frames = wf.readframes(wf.getnframes()) + assert np.frombuffer(frames, dtype=np.int16).tolist() == pcm.tolist() diff --git a/tests/test_notifications.py b/tests/test_notifications.py new file mode 100644 index 0000000..569e55d --- /dev/null +++ b/tests/test_notifications.py @@ -0,0 +1,90 @@ +"""dbus-monitor parsing + the forward/rate-limit gate. No session bus +needed — the parser is fed canned dbus-monitor output.""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from bolt_pet.notifications import Notification, NotificationGate, iter_notifications + +SAMPLE = '''signal time=1710000000.1 sender=org.freedesktop.DBus -> destination=:1.7 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired + string ":1.7" +method call time=1710000001.2 sender=:1.72 -> destination=org.freedesktop.Notifications serial=88 path=/org/freedesktop/Notifications; interface=org.freedesktop.Notifications; member=Notify + string "Firefox" + uint32 0 + string "" + string "Build finished" + string "All 42 tests passed" + array [ + ] + int32 -1 +method call time=1710000002.3 sender=:1.80 -> destination=org.freedesktop.Notifications serial=91 path=/org/freedesktop/Notifications; interface=org.freedesktop.Notifications; member=Notify + string "Calendar" + uint32 0 + string "calendar-icon" + string "Standup in 5 minutes" + string "" + array [ + ] +''' + + +def _parse(text=SAMPLE): + return list(iter_notifications(text.splitlines())) + + +def test_parses_each_notify_call(): + parsed = _parse() + assert parsed == [ + Notification(app="Firefox", summary="Build finished", body="All 42 tests passed"), + Notification(app="Calendar", summary="Standup in 5 minutes", body=""), + ] + + +def test_unrelated_dbus_traffic_is_ignored(): + noise = SAMPLE.split("method call")[0] + assert _parse(noise) == [] + + +def test_trailing_notification_without_a_following_block_is_still_emitted(): + assert _parse()[-1].summary == "Standup in 5 minutes" + + +def test_as_text_is_what_gets_sent_to_the_server(): + assert _parse()[0].as_text() == "Firefox: Build finished — All 42 tests passed" + assert _parse()[1].as_text() == "Calendar: Standup in 5 minutes" + + +# ── gate ──────────────────────────────────────────────────────────────────── + +def _notification(summary="Build finished", app="Firefox"): + return Notification(app=app, summary=summary, body="") + + +def test_empty_filter_forwards_everything(): + gate = NotificationGate("", min_interval=0) + assert gate.should_forward(_notification(), now=0) is True + + +def test_filter_regex_selects_what_is_worth_a_round_trip(): + gate = NotificationGate(r"build|deploy", min_interval=0) + assert gate.should_forward(_notification("Build finished"), now=0) is True + assert gate.should_forward(_notification("New message from Dave"), now=1) is False + + +def test_rate_limit_drops_a_burst(): + gate = NotificationGate("", min_interval=60) + assert gate.should_forward(_notification(), now=100) is True + assert gate.should_forward(_notification(), now=120) is False + assert gate.should_forward(_notification(), now=161) is True + + +def test_a_broken_regex_does_not_silence_the_bridge(): + gate = NotificationGate("(unclosed", min_interval=0) + assert gate.should_forward(_notification(), now=0) is True + + +def test_empty_notifications_are_dropped(): + gate = NotificationGate("", min_interval=0) + assert gate.should_forward(Notification(app="", summary="", body=""), now=0) is False diff --git a/tests/test_pet_actions.py b/tests/test_pet_actions.py new file mode 100644 index 0000000..9d90e2b --- /dev/null +++ b/tests/test_pet_actions.py @@ -0,0 +1,71 @@ +"""petctl parsing — the pseudo-commands the server can relay to drive the +pet's body instead of a shell.""" + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from bolt_pet import pet_actions + + +def test_non_pet_commands_are_left_alone(): + assert pet_actions.parse("ls -la") is None + assert pet_actions.parse("systemctl restart nginx") is None + assert pet_actions.parse("") is None + # "petstore" must not be mistaken for the "pet" prefix + assert pet_actions.parse("petstore --list") is None + + +def test_move_to_an_anchor(): + assert pet_actions.parse("petctl move top-left") == {"action": "move", "anchor": "top-left"} + assert pet_actions.parse("petctl move bottom_right") == {"action": "move", "anchor": "bottom-right"} + + +def test_move_to_coordinates(): + assert pet_actions.parse("petctl move 300 120") == {"action": "move", "x": 300, "y": 120} + + +def test_move_rejects_nonsense_targets(): + with pytest.raises(pet_actions.ActionError): + pet_actions.parse("petctl move sideways") + with pytest.raises(pet_actions.ActionError): + pet_actions.parse("petctl move") + + +def test_emotes(): + assert pet_actions.parse("petctl emote wave") == {"action": "emote", "emote": "wave"} + with pytest.raises(pet_actions.ActionError): + pet_actions.parse("petctl emote moonwalk") + + +def test_say_keeps_the_whole_sentence(): + assert pet_actions.parse('petctl say "build is green"') == { + "action": "say", "text": "build is green" + } + assert pet_actions.parse("petctl say build is green")["text"] == "build is green" + + +def test_wander_and_nap_toggles(): + assert pet_actions.parse("petctl wander off") == {"action": "wander", "enabled": False} + assert pet_actions.parse("petctl nap on") == {"action": "nap", "enabled": True} + with pytest.raises(pet_actions.ActionError): + pet_actions.parse("petctl wander maybe") + + +def test_alternate_prefixes_and_verbs(): + assert pet_actions.parse("bolt-pet goto center")["anchor"] == "center" + assert pet_actions.parse("pet do hop")["emote"] == "hop" + + +def test_unknown_verb_is_an_error_not_a_shell_command(): + with pytest.raises(pet_actions.ActionError): + pet_actions.parse("petctl explode") + + +def test_describe_is_reported_back_to_the_server(): + assert "top-left" in pet_actions.describe({"action": "move", "anchor": "top-left"}) + assert "wave" in pet_actions.describe({"action": "emote", "emote": "wave"}) + assert pet_actions.describe({"action": "help"}) == pet_actions.HELP diff --git a/tests/test_pet_window_features.py b/tests/test_pet_window_features.py new file mode 100644 index 0000000..859dd60 --- /dev/null +++ b/tests/test_pet_window_features.py @@ -0,0 +1,181 @@ +"""Window-side behaviour for the newer features: emote curves, petctl +actions, edge snapping, napping, click-through. + +Needs a QApplication — run with QT_QPA_PLATFORM=offscreen. +""" + +import sys +from pathlib import Path + +import pytest +from PySide6.QtCore import QPoint +from PySide6.QtWidgets import QApplication + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from bolt_pet import config +from bolt_pet.state import PetState +from bolt_pet.ui.pet_window import _EMOTE_TICKS, PetWindow, emote_transform + + +@pytest.fixture(scope="module") +def qt_app(): + yield QApplication.instance() or QApplication([]) + + +@pytest.fixture +def pet(qt_app): + window = PetWindow() + yield window + window.close() + + +# ── emote curves (pure maths) ─────────────────────────────────────────────── + +@pytest.mark.parametrize("emote", ["wave", "hop", "bounce", "spin", "nod", "shake", "wiggle"]) +def test_every_emote_returns_the_sprite_to_rest(emote): + # Anything that doesn't land back at the identity transform leaves the pet + # permanently askew. (approx: the sine curves land on ~1e-16, not 0.0.) + rest = pytest.approx((0.0, 0.0, 0.0, 1.0), abs=1e-9) + assert emote_transform(emote, 1.0) == rest + assert emote_transform(emote, 0.0) == rest + + +def test_emotes_actually_move_the_sprite_mid_animation(): + for emote in ("wave", "hop", "spin", "nod", "shake"): + samples = [emote_transform(emote, i / 20) for i in range(1, 20)] + assert any(sample != (0.0, 0.0, 0.0, 1.0) for sample in samples), emote + + +def test_an_unknown_emote_is_a_no_op_not_a_crash(): + assert emote_transform("moonwalk", 0.5) == (0.0, 0.0, 0.0, 1.0) + + +def test_progress_is_clamped(): + assert emote_transform("hop", 5.0) == emote_transform("hop", 1.0) + assert emote_transform("hop", -3.0) == emote_transform("hop", 0.0) + + +def test_an_emote_finishes_and_clears_itself(pet): + pet.start_emote("spin") + assert pet._emote == "spin" + for _ in range(_EMOTE_TICKS + 2): + pet._advance_emote() + assert pet._emote is None + + +# ── petctl actions ────────────────────────────────────────────────────────── + +def test_move_action_sets_a_walk_target(pet): + pet.apply_action({"action": "move", "anchor": "top-left"}) + assert pet._wander_target is not None + assert pet._commanded_move is True + + +def test_commanded_moves_happen_even_while_talking(pet, monkeypatch): + monkeypatch.setattr(config, "PET_EDGE_SNAP", False) # snapping would move it again on arrival + pet.set_state(PetState.TALKING) + pet.apply_action({"action": "move", "anchor": "top-left"}) + target = pet._wander_target + if target is None: + pytest.skip("no usable screen geometry on this host") + for _ in range(2000): + pet._wander_tick() + if pet._wander_target is None: + break + assert pet.pos() == target + + +def test_a_commanded_move_survives_the_reply_arriving(pet): + # Real ordering: `petctl move` comes back as a tool call mid-turn, then + # the reply flips the pet to TALKING a moment later. That must not cancel + # the walk it was just told to make. + pet.apply_action({"action": "move", "anchor": "center"}) + pet.set_state(PetState.TALKING) + assert pet._wander_target is not None + + +def test_move_to_explicit_coordinates_is_clamped_on_screen(pet): + pet.apply_action({"action": "move", "x": -5000, "y": -5000}) + geo = pet._screen_geometry() + if geo is None: + pytest.skip("no usable screen geometry on this host") + assert pet._wander_target.x() >= geo.left() + assert pet._wander_target.y() >= geo.top() + + +def test_say_action_shows_the_bubble(pet): + pet.apply_action({"action": "say", "text": "build is green"}) + assert pet._bubble.text == "build is green" + + +def test_wander_and_nap_actions(pet): + pet.apply_action({"action": "wander", "enabled": False}) + assert pet._wander_enabled is False + pet.apply_action({"action": "nap", "enabled": True}) + assert pet.napping is True + + +def test_emote_action_starts_the_emote(pet): + pet.apply_action({"action": "emote", "emote": "wave"}) + assert pet._emote == "wave" + + +# ── napping ───────────────────────────────────────────────────────────────── + +def test_napping_dims_the_pet_and_stops_it_wandering(pet): + pet.set_napping(True) + assert pet.windowOpacity() < 1.0 + start = pet.pos() + pet.wander_now() + for _ in range(60): + pet._wander_tick() + assert pet.pos() == start + + pet.set_napping(False) + assert pet.windowOpacity() == 1.0 + + +# ── edge snapping ─────────────────────────────────────────────────────────── + +def test_snaps_flush_when_parked_near_an_edge(pet, monkeypatch): + geo = pet._screen_geometry() + if geo is None: + pytest.skip("no usable screen geometry on this host") + monkeypatch.setattr(config, "PET_EDGE_SNAP", True) + pet.move(geo.left() + 10, geo.top() + 10) + assert pet.snap_to_edge() is True + assert pet.pos() == QPoint(geo.left(), geo.top()) + + +def test_does_not_snap_from_the_middle_of_the_screen(pet, monkeypatch): + geo = pet._screen_geometry() + if geo is None: + pytest.skip("no usable screen geometry on this host") + monkeypatch.setattr(config, "PET_EDGE_SNAP", True) + middle = QPoint(geo.left() + geo.width() // 2, geo.top() + geo.height() // 2) + pet.move(middle) + assert pet.snap_to_edge() is False + assert pet.pos() == middle + + +def test_snapping_can_be_turned_off(pet, monkeypatch): + geo = pet._screen_geometry() + if geo is None: + pytest.skip("no usable screen geometry on this host") + monkeypatch.setattr(config, "PET_EDGE_SNAP", False) + pet.move(geo.left() + 10, geo.top() + 10) + assert pet.snap_to_edge() is False + + +# ── click-through ─────────────────────────────────────────────────────────── + +def test_click_through_toggles_mouse_transparency(pet): + from PySide6.QtCore import Qt + + pet.set_click_through(True) + assert pet.click_through is True + assert pet.testAttribute(Qt.WA_TransparentForMouseEvents) is True + + pet.set_click_through(False) + assert pet.testAttribute(Qt.WA_TransparentForMouseEvents) is False diff --git a/tests/test_quiet.py b/tests/test_quiet.py new file mode 100644 index 0000000..df1a7c3 --- /dev/null +++ b/tests/test_quiet.py @@ -0,0 +1,54 @@ +"""Quiet-hours parsing and matching, without waiting for 11pm.""" + +import sys +from datetime import time as dtime +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from bolt_pet import quiet + + +def test_no_spec_means_never_quiet(): + assert quiet.is_quiet("") is False + assert quiet.is_quiet(" ") is False + + +def test_simple_daytime_range(): + ranges = quiet.parse_ranges("13:00-14:00") + assert quiet.in_ranges(dtime(13, 30), ranges) is True + assert quiet.in_ranges(dtime(12, 59), ranges) is False + assert quiet.in_ranges(dtime(14, 0), ranges) is False # end is exclusive + + +def test_range_wrapping_past_midnight(): + ranges = quiet.parse_ranges("23:00-08:00") + for moment in (dtime(23, 0), dtime(23, 59), dtime(0, 0), dtime(7, 59)): + assert quiet.in_ranges(moment, ranges) is True, moment + for moment in (dtime(8, 0), dtime(12, 0), dtime(22, 59)): + assert quiet.in_ranges(moment, ranges) is False, moment + + +def test_multiple_ranges(): + ranges = quiet.parse_ranges("23:00-08:00, 13:00-14:00") + assert quiet.in_ranges(dtime(13, 15), ranges) is True + assert quiet.in_ranges(dtime(2, 0), ranges) is True + assert quiet.in_ranges(dtime(16, 0), ranges) is False + + +def test_zero_length_range_is_not_all_day(): + assert quiet.in_ranges(dtime(12, 0), quiet.parse_ranges("09:00-09:00")) is False + + +def test_malformed_specs_raise_when_parsed_directly(): + for spec in ("nonsense", "25:00-26:00", "13:00", "13:60-14:00"): + with pytest.raises(quiet.QuietHoursError): + quiet.parse_ranges(spec) + + +def test_malformed_spec_is_reported_but_never_mutes_the_pet(): + seen = [] + assert quiet.is_quiet("nonsense", now=dtime(3, 0), on_error=seen.append) is False + assert len(seen) == 1 diff --git a/tests/test_screen_context.py b/tests/test_screen_context.py new file mode 100644 index 0000000..bfc63aa --- /dev/null +++ b/tests/test_screen_context.py @@ -0,0 +1,56 @@ +"""Screen-context parsing/annotation. The subprocess probes are platform +specific; the parsing they feed is not, so that's what's tested here.""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from bolt_pet import screen_context + + +def test_parses_the_active_window_id(): + output = "_NET_ACTIVE_WINDOW(WINDOW): window id # 0x3c00007\n" + assert screen_context.parse_xprop_window_id(output) == "0x3c00007" + + +def test_no_active_window_id_when_nothing_is_focused(): + assert screen_context.parse_xprop_window_id("_NET_ACTIVE_WINDOW(WINDOW): window id # 0x0") is None + assert screen_context.parse_xprop_window_id("") is None + + +def test_parses_the_window_title(): + output = '_NET_WM_NAME(UTF8_STRING) = "bolt_pet/controller.py - Cursor"\n' + assert screen_context.parse_xprop_window_name(output) == "bolt_pet/controller.py - Cursor" + + +def test_unset_title_property(): + assert screen_context.parse_xprop_window_name("_NET_WM_NAME: not found") is None + + +def test_fullscreen_state_detection(): + assert screen_context.parse_xprop_fullscreen( + "_NET_WM_STATE(ATOM) = _NET_WM_STATE_FULLSCREEN, _NET_WM_STATE_FOCUSED") is True + assert screen_context.parse_xprop_fullscreen("_NET_WM_STATE(ATOM) = _NET_WM_STATE_FOCUSED") is False + + +def test_desktop_titles_are_treated_as_no_context(): + assert screen_context.clean_title("Desktop") is None + assert screen_context.clean_title(" ") is None + + +def test_long_titles_are_truncated(): + cleaned = screen_context.clean_title("x" * 500) + assert len(cleaned) <= 160 and cleaned.endswith("…") + + +def test_annotate_appends_context_as_an_aside(): + annotated = screen_context.annotate("what's this error?", "app.py — Traceback") + assert annotated.startswith("what's this error?") + assert "[on screen right now: app.py — Traceback]" in annotated + + +def test_annotate_is_a_no_op_without_a_title_or_text(): + assert screen_context.annotate("hello", None) == "hello" + assert screen_context.annotate("hello", "Desktop") == "hello" + assert screen_context.annotate("", "Firefox") == "" diff --git a/tests/test_server_client.py b/tests/test_server_client.py new file mode 100644 index 0000000..918e272 --- /dev/null +++ b/tests/test_server_client.py @@ -0,0 +1,86 @@ +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import pytest + +from bolt_pet import server_client + + +@pytest.fixture(autouse=True) +def _configure(monkeypatch): + monkeypatch.setattr(server_client.config, "SERVER_URL", "http://test-server:5002") + monkeypatch.setattr(server_client.config, "API_KEY", "test-key") + monkeypatch.setattr(server_client.config, "SESSION_ID", "pet-test") + + +def _mock_response(json_data, ok=True): + resp = MagicMock() + resp.json.return_value = json_data + resp.raise_for_status = MagicMock() if ok else MagicMock(side_effect=Exception("boom")) + return resp + + +def test_converse_returns_reply_directly(): + with patch.object(server_client.requests, "post") as post: + post.return_value = _mock_response({"type": "reply", "text": "hello there"}) + result = server_client.converse("hi") + assert result == "hello there" + post.assert_called_once() + args, kwargs = post.call_args + assert args[0] == "http://test-server:5002/desk/converse" + assert kwargs["json"] == {"session_id": "pet-test", "text": "hi"} + assert kwargs["headers"] == {"X-Desk-Api-Key": "test-key"} + + +def test_converse_relays_a_command_then_returns_reply(): + responses = [ + _mock_response({"type": "command", "command": "echo hi", "token": "tok1"}), + _mock_response({"type": "reply", "text": "done"}), + ] + with patch.object(server_client.requests, "post", side_effect=responses) as post: + on_command = MagicMock(return_value="[exit 0]\nhi") + result = server_client.converse("run echo hi", on_command=on_command) + assert result == "done" + on_command.assert_called_once_with("echo hi") + # second call was to /desk/tool_result with the command's output + second_call = post.call_args_list[1] + assert second_call.args[0] == "http://test-server:5002/desk/tool_result" + assert second_call.kwargs["json"] == { + "session_id": "pet-test", "token": "tok1", "output": "[exit 0]\nhi", + } + + +def test_converse_raises_server_error_on_error_payload(): + with patch.object(server_client.requests, "post") as post: + post.return_value = _mock_response({"type": "error", "error": "unauthorized"}) + with pytest.raises(server_client.ServerError, match="unauthorized"): + server_client.converse("hi") + + +def test_converse_raises_server_error_when_unreachable(): + with patch.object(server_client.requests, "post", side_effect=ConnectionError("no route")): + with pytest.raises(server_client.ServerError): + server_client.converse("hi") + + +def test_report_status_returns_reply_text_when_present(): + with patch.object(server_client.requests, "post") as post: + post.return_value = _mock_response({"reply": "don't forget your 3pm"}) + result = server_client.report_status() + assert result == "don't forget your 3pm" + + +def test_report_status_returns_none_when_nothing_pending(): + with patch.object(server_client.requests, "post") as post: + post.return_value = _mock_response({"ok": True}) + assert server_client.report_status() is None + + +def test_check_health_returns_parsed_json(): + with patch.object(server_client.requests, "get") as get: + get.return_value = _mock_response({"ok": True, "service": "bolt-desk-api"}) + result = server_client.check_health() + assert result == {"ok": True, "service": "bolt-desk-api"} diff --git a/tests/test_speech_text.py b/tests/test_speech_text.py new file mode 100644 index 0000000..dee7d75 --- /dev/null +++ b/tests/test_speech_text.py @@ -0,0 +1,59 @@ +"""Sanitizing chat-formatted replies into speakable prose. Pure string logic +— no audio hardware, no Qt (see the testing conventions in CLAUDE.md).""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from bolt_pet.speech_text import for_display, for_speech + + +def test_bold_markers_are_not_spoken(): + assert "*" not in for_speech("Here's the **maji-desktop** snapshot") + assert for_speech("Here's the **maji-desktop** snapshot") == "Here's the maji-desktop snapshot" + + +def test_italics_and_underscores_dropped(): + assert for_speech("that is _really_ odd") == "that is really odd" + assert for_speech("***everything*** is fine") == "everything is fine" + + +def test_bullet_list_becomes_sentences(): + spoken = for_speech( + "System status:\n" + "* **OS:** Linux 7.0.0\n" + "* **Uptime:** 1 day\n" + ) + assert "*" not in spoken + assert spoken == "System status: OS: Linux 7.0.0. Uptime: 1 day." + + +def test_emoji_and_symbols_removed(): + assert for_speech("✅ Done 🚀 — all good") == "Done all good" + assert for_speech("→ next step") == "to next step" + + +def test_urls_and_code_are_not_read_out_character_by_character(): + assert for_speech("see https://example.com/x?y=1 for docs") == "see link for docs" + assert for_speech("run `sudo reboot` now") == "run sudo reboot now" + assert for_speech("here:\n```\nls -la\n```\n") == "here: (code)." + + +def test_headings_quotes_and_rules_stripped(): + assert for_speech("## Summary\n---\n> quoted bit") == "Summary. quoted bit." + + +def test_ampersand_and_percent_are_spoken_as_words(): + assert for_speech("R&D at 50% capacity") == "R and D at 50 percent capacity" + + +def test_blank_and_symbol_only_input(): + assert for_speech("") == "" + assert for_speech(None) == "" + assert for_speech("***") == "" + + +def test_display_keeps_emoji_but_drops_markdown(): + assert for_display("**Done** ✅") == "Done ✅" + assert for_display("* one\n* two") == "• one • two" diff --git a/tests/test_state.py b/tests/test_state.py new file mode 100644 index 0000000..89be627 --- /dev/null +++ b/tests/test_state.py @@ -0,0 +1,73 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import pytest + +from bolt_pet.state import InvalidTransition, PetState, PetStateMachine + + +def test_starts_idle(): + sm = PetStateMachine() + assert sm.state == PetState.IDLE + + +def test_happy_path_transitions(): + sm = PetStateMachine() + sm.transition(PetState.LISTENING) + sm.transition(PetState.THINKING) + sm.transition(PetState.TALKING) + sm.transition(PetState.IDLE) + assert sm.state == PetState.IDLE + + +def test_idle_to_talking_is_allowed_for_proactive_announcements(): + # The heartbeat poll can make the pet speak unprompted (a reminder + # firing, a nudge from the server) with no preceding listen/think leg. + sm = PetStateMachine() + sm.transition(PetState.TALKING) + assert sm.state == PetState.TALKING + + +def test_invalid_transition_raises(): + sm = PetStateMachine() + with pytest.raises(InvalidTransition): + sm.transition(PetState.THINKING) # can't skip straight to thinking with no utterance + + +def test_same_state_transition_is_a_noop(): + calls = [] + sm = PetStateMachine(on_change=lambda old, new: calls.append((old, new))) + sm.transition(PetState.IDLE) # already idle + assert calls == [] + + +def test_on_change_callback_fires_with_old_and_new(): + calls = [] + sm = PetStateMachine(on_change=lambda old, new: calls.append((old, new))) + sm.transition(PetState.LISTENING) + assert calls == [(PetState.IDLE, PetState.LISTENING)] + + +def test_every_state_can_reach_error_and_recover(): + for path in ( + [PetState.LISTENING], + [PetState.LISTENING, PetState.THINKING], + [PetState.LISTENING, PetState.THINKING, PetState.TALKING], + ): + sm = PetStateMachine() + for step in path: + sm.transition(step) + sm.transition(PetState.ERROR) + sm.transition(PetState.IDLE) + assert sm.state == PetState.IDLE + + +def test_force_recovers_from_talking_directly_to_idle_without_validation(): + sm = PetStateMachine() + sm.transition(PetState.LISTENING) + sm.transition(PetState.THINKING) + sm.transition(PetState.TALKING) + sm.force(PetState.LISTENING) # not in TALKING's allowed set, but force skips the check + assert sm.state == PetState.LISTENING diff --git a/tests/test_tts_stream.py b/tests/test_tts_stream.py new file mode 100644 index 0000000..af78d04 --- /dev/null +++ b/tests/test_tts_stream.py @@ -0,0 +1,82 @@ +"""Streaming-TTS chunk reassembly and the near-miss log. Both are pure — +no network, no audio device, no ONNX model.""" + +import sys +from pathlib import Path + +import numpy as np + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from bolt_pet.audio.tts import chunks_to_int16 +from bolt_pet.audio.wake_word import NearMissLog + + +def _pcm(*values): + return np.array(values, dtype=np.int16).tobytes() + + +def test_whole_samples_pass_straight_through(): + chunks = list(chunks_to_int16([_pcm(1, 2), _pcm(3, 4)])) + assert np.concatenate(chunks).tolist() == [1, 2, 3, 4] + + +def test_a_sample_split_across_two_http_chunks_is_rejoined(): + # The killer bug this exists to prevent: an odd byte at a chunk boundary + # shifts everything after it by one byte and plays as static. + raw = _pcm(100, -200, 300, -400) + chunks = list(chunks_to_int16([raw[:3], raw[3:]])) + assert np.concatenate(chunks).tolist() == [100, -200, 300, -400] + + +def test_many_odd_boundaries_in_a_row(): + raw = _pcm(*range(1, 21)) + pieces = [raw[i:i + 3] for i in range(0, len(raw), 3)] # every boundary odd + assert np.concatenate(list(chunks_to_int16(pieces))).tolist() == list(range(1, 21)) + + +def test_empty_chunks_are_skipped(): + assert list(chunks_to_int16([b"", b""])) == [] + + +def test_a_dangling_byte_at_the_end_is_dropped_not_played(): + raw = _pcm(7, 8) + b"\x01" + assert np.concatenate(list(chunks_to_int16([raw]))).tolist() == [7, 8] + + +# ── wake-word near misses ─────────────────────────────────────────────────── + +def test_scores_just_under_the_threshold_are_recorded(): + log = NearMissLog(limit=10, margin=0.2) + assert log.observe(0.45, threshold=0.5, timestamp=1.0) is True + assert log.entries() == [(1.0, 0.45, 0.5)] + + +def test_detections_and_background_noise_are_not_near_misses(): + log = NearMissLog(limit=10, margin=0.2) + assert log.observe(0.90, threshold=0.5, timestamp=1.0) is False # it fired + assert log.observe(0.05, threshold=0.5, timestamp=2.0) is False # just noise + assert log.entries() == [] + + +def test_peak_tracks_every_score_not_just_near_misses(): + log = NearMissLog(limit=10, margin=0.2) + log.observe(0.30, threshold=0.5, timestamp=1.0) + log.observe(0.95, threshold=0.5, timestamp=2.0) + log.observe(0.10, threshold=0.5, timestamp=3.0) + assert log.peak == 0.95 + + +def test_the_log_is_bounded(): + log = NearMissLog(limit=3, margin=0.2) + for i in range(10): + log.observe(0.45, threshold=0.5, timestamp=float(i)) + assert len(log.entries()) == 3 + assert log.entries()[-1][0] == 9.0 + + +def test_clear_resets_peak_and_entries(): + log = NearMissLog(limit=3, margin=0.2) + log.observe(0.45, threshold=0.5, timestamp=1.0) + log.clear() + assert log.entries() == [] and log.peak == 0.0 diff --git a/tests/test_wake_word.py b/tests/test_wake_word.py new file mode 100644 index 0000000..b09ee2c --- /dev/null +++ b/tests/test_wake_word.py @@ -0,0 +1,148 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import numpy as np +import pytest + +from bolt_pet.audio import wake_word + + +class _FakeStream: + """Yields a fixed sequence of frames, then silence forever.""" + + def __init__(self, frames, frame_len): + self._frames = list(frames) + self._frame_len = frame_len + + def read(self, frames): + if self._frames: + frame = self._frames.pop(0) + else: + frame = np.zeros(self._frame_len, dtype=np.int16) + return frame.reshape(-1, 1), False + + +def _frame(frame_len): + return np.zeros(frame_len, dtype=np.int16) + + +class _FakeModel: + """Reports the given score sequence (one dict per predict() call, then + repeats the last entry) and records reset() calls.""" + + def __init__(self, score_sequence): + self._scores = list(score_sequence) + self.reset_calls = 0 + + def predict(self, frame): + if self._scores: + return self._scores.pop(0) + return {"thunderbolt": 0.0} + + def reset(self): + self.reset_calls += 1 + + +def test_returns_true_and_resets_on_detection(monkeypatch): + frame_len = 1280 + monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len) + monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000) + monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.08) # 1 frame + monkeypatch.setattr(wake_word.config, "WAKE_WORD_THRESHOLD", 0.5) + + stream = _FakeStream([_frame(frame_len)] * 3, frame_len) + model = _FakeModel([{"thunderbolt": 0.1}, {"thunderbolt": 0.9}]) + + detected = wake_word.listen_for_wake_word(stream, model=model) + + assert detected is True + assert model.reset_calls == 1 + + +def test_returns_false_when_should_continue_goes_false_first(): + frame_len = 1280 + stream = _FakeStream([_frame(frame_len)] * 5, frame_len) + model = _FakeModel([{"thunderbolt": 0.0}] * 5) + calls = {"n": 0} + + def should_continue(): + calls["n"] += 1 + return calls["n"] <= 3 + + detected = wake_word.listen_for_wake_word( + stream, should_continue=should_continue, model=model, + ) + assert detected is False + assert model.reset_calls == 0 + + +def test_custom_threshold_is_respected(monkeypatch): + frame_len = 1280 + monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len) + monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000) + monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.08) + + stream = _FakeStream([_frame(frame_len)] * 3, frame_len) + model = _FakeModel([{"thunderbolt": 0.6}] * 3) + calls = {"n": 0} + + def should_continue(): + calls["n"] += 1 + return calls["n"] <= 3 + + detected = wake_word.listen_for_wake_word( + stream, should_continue=should_continue, model=model, threshold=0.7, + ) + assert detected is False + + +def test_on_tick_fires_once_per_check_interval(monkeypatch): + frame_len = 1280 + monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len) + monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000) + monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.08) # 1 frame/check + + frames = [_frame(frame_len) for _ in range(5)] + stream = _FakeStream(frames, frame_len) + model = _FakeModel([{"thunderbolt": 0.0}] * 5) + ticks = {"n": 0} + state = {"i": 0} + + def should_continue(): + state["i"] += 1 + return state["i"] <= 5 + + wake_word.listen_for_wake_word( + stream, + should_continue=should_continue, + model=model, + on_tick=lambda: ticks.__setitem__("n", ticks["n"] + 1), + ) + assert ticks["n"] == 5 # one check-interval per frame at this config + + +def test_on_tick_interval_can_span_multiple_frames(monkeypatch): + frame_len = 1280 + monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len) + monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000) + monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.24) # 3 frames/check + + frames = [_frame(frame_len) for _ in range(6)] + stream = _FakeStream(frames, frame_len) + model = _FakeModel([{"thunderbolt": 0.0}] * 6) + ticks = {"n": 0} + state = {"i": 0} + + def should_continue(): + state["i"] += 1 + return state["i"] <= 6 + + wake_word.listen_for_wake_word( + stream, + should_continue=should_continue, + model=model, + on_tick=lambda: ticks.__setitem__("n", ticks["n"] + 1), + ) + assert ticks["n"] == 2 # 6 frames / 3 frames-per-check diff --git a/tests/test_wander.py b/tests/test_wander.py new file mode 100644 index 0000000..1dc8458 --- /dev/null +++ b/tests/test_wander.py @@ -0,0 +1,89 @@ +"""Autonomous wandering. Needs a QApplication, so run with +QT_QPA_PLATFORM=offscreen (same as test_controller.py).""" + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from PySide6.QtWidgets import QApplication + +from bolt_pet import config +from bolt_pet.state import PetState +from bolt_pet.ui.pet_window import PetWindow + + +@pytest.fixture(scope="module") +def qt_app(): + app = QApplication.instance() or QApplication([]) + yield app + + +@pytest.fixture +def pet(qt_app): + window = PetWindow() + window.set_wander_enabled(True) + yield window + window.close() + + +def _walk_until_done(pet, max_ticks=2000): + for _ in range(max_ticks): + pet._wander_tick() + if pet._wander_target is None: + return True + return False + + +def test_wanders_when_idle_and_reaches_its_target(pet, monkeypatch): + # Snapping is tested separately; here it would tug the pet off its target + # the moment it arrives near an edge. + monkeypatch.setattr(config, "PET_EDGE_SNAP", False) + pet.wander_now() + pet._wander_tick() + target = pet._wander_target + if target is None: + pytest.skip("no usable screen geometry for a stroll on this host") + assert _walk_until_done(pet), "pet never reached its wander target" + assert pet.pos() == target + + +def test_stays_put_while_talking(pet): + pet.set_state(PetState.TALKING) + start = pet.pos() + pet.wander_now() + for _ in range(50): + pet._wander_tick() + assert pet.pos() == start + assert pet._wander_target is None + + +def test_stays_put_while_bubble_is_up(pet): + pet.say("hello there", duration_ms=60000) + start = pet.pos() + pet.wander_now() + for _ in range(50): + pet._wander_tick() + assert pet.pos() == start + + +def test_disabling_wander_stops_movement(pet): + pet.set_wander_enabled(False) + start = pet.pos() + pet.wander_now() + for _ in range(50): + pet._wander_tick() + assert pet.pos() == start + + +def test_stroll_stays_on_screen(pet): + geo = pet._screen_geometry() + if geo is None: + pytest.skip("no usable screen geometry on this host") + for _ in range(10): + pet.wander_now() + pet._wander_tick() + assert _walk_until_done(pet) + assert geo.contains(pet.geometry()) diff --git a/thunderbolt.onnx b/thunderbolt.onnx new file mode 100644 index 0000000000000000000000000000000000000000..44e58681103917d5073ca357224278bcfc855a60 GIT binary patch literal 822569 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%0A6S2cHYDW#=Mn_A|j_ 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≥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%0AQJLuLtpJO|l36QrQ#|&2h`Bh^1!Q$^17{Zr z1KlnGuywQ#ym6+ucTFQccqa>F9(;g>(}UP$Z420KPMJ8iU?#h;bQmXvE3iVgxvb6P zajYi1g{{9ilWgx=4EnN6fI#sCo5v?X1hm4j@$n=0!vlL@cK!x?(ZCq&DA8i)ugb@& zmwJE!UXJ-Mo?|pjgMoSJd>nhw+`1z(lo=p4;FMSrqdK~X+bdhM1?>sU&+F@ebfX$D z6Tr+s{1SNO^GVRD_z2`oPi4iT3)ue_x`4eyH&~I7Ua-PSlAQ`K1~R>hWKF##Q_Tc`2aiIPmKVCif zl-(5d3j7;%#?LxzaA(;LW@XPfQ@=k4YzvYHk0e8xpX#qMeEbu88A^bOTXA^IyM~qR zl?HVqo57B+&v+>^Hf%t%IrF%~oOAn{VG;jIYmI+V?2K7aEPSj1AFm4m!_uFb;+z@4 zYUxKd%r}51_*RMW`}Yq!+f*@u-{0X?uDgJJ^f6{nj2Uk3Otzl+LXqSJngT^jQ9Ky< z6o1(TnDZOui7Q;l?BMBxEpJ?~WUCj}Q2oe`y3A#O!&5uQ3_6^Twxb+$6 z3_NNaXB5H;aNnNwpze(d9#Rcsm){crY8OMm8h1YY?56>#Dp-lnXJ<1jl_xO2&N{qC z&=t(w`Ir5k*To*vjb)?aD?utZ&HJ<;v#*UJ*z3mac$;ktp0iXCYH&Ol(N;sA?Oi=U zHGDaaPzf`3;R+D5>c(F@2Y{Pk7*5(@0B+QF;j#xxSSbKw8J~x^vt^3Cl8dm9M89j)Eve};&?(DY%Q;e0c5jc1H5Zjiy+!j@V4~Ibs}Ow=)fQ=-Zuwl-B?xGtSkxSkC%Z$%(AiVcbE?@kszQZ zoHtwIIGfzE9c(aKUU_H7YGyq5HJd15j1P(b2Q(wHaH5YculK_QE0A*lpTB#WQMdWc zgw~(NcXu}98JRy>$C(GQU-Lz5@#YNBiHYGgYwTw=zE5F?Fim#P+YJCIQ*5zY4QG7a zYu)|m4PG?098ZWA;cWg8;BGjIplVc~*Y-8i+AdQR+!0J>=j~CzX2RpFNWx1{{_Oz!`MVL;ZA)V= zk7ikYmu_X{Pyb-678P1|n9KvN8jF~g!pqFloaqondjrWG**Md&nfXsh2}_?NVBO0M zyi`gT&l4|TzLdnX%VT9(Dk4ap734Etvy9m%ed#-vi!j2D8@&x_G+F zmB2$^Qyl)o32Pnr!%mi5Wr`&P*pP7tY@N0W927o-Cqx#ojeNJ5)uTK-FCiB1xfI7- z>EDEnll_=>iRaeO?jB|@_YPVcFHL6iiUPogsaas5dos|L?_s-lvrK8pRbWve3uKcy z&((v~z$_sR8)(jl-UEWzHn{=BXWz!^u@AxDGuHTJ7}x1k6Cgtm58!kIBf#Hz5O#2K zICpbZFmI(Fjz7H}{O2TXy-{fk_Ry6B$7I78x8LE|a-tQ>U0a98vi;bmD~rMMaS70{ z{UaV>KjM?cRlxQ1R_iksO5pv85_Z4TQR~P}@r>s8aa>!xlwEc$0!W-p#5&7N@mW@% zIcpgTf@A)H;C>m9an}#OSa%(`F7D$U+M3BOQmg@4alP!nU)8u?^Z>r-QOavodkwxS z#$a`2b@s-E`CxX_B-0SX2M%pcz`s5BFnZGr*z3JY*si(^5BWy``}!=fNbL^Tuy=^3 zWfTcMr?19g>IZP1$Tqg$&J&weJ7ek839L6U0kj<-<8{Zx+4Z3bV6|i(9{cY$Hq8oW zywZa~Wcvm1R3U?BVlta|M6aR3F?%0Awz?Qx3|#^~WXXbc@4~R8>vQ0!@fc)yl~~)K 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