Wake-word barge-in, Gitea auto-updater, hard_reset fix
This commit is contained in:
@@ -79,11 +79,16 @@ logs a missing-config message and exits its thread instead of starting.
|
||||
`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.
|
||||
PCM then offline `pyttsx3`), `barge_in.py` (two detectors behind one
|
||||
`reset()`/`check()` shape, chosen by `BARGE_IN_MODE` via `make_detector`:
|
||||
**wake** (default) scores every frame with the same openWakeWord model the
|
||||
idle listener uses, so only the wake phrase cuts playback; **energy** is the
|
||||
original N-consecutive-loud-frames rule, threshold ~4x the VAD one because
|
||||
the mic hears the pet's own voice. Wake mode shares `_default_model` with
|
||||
the idle listener — the two never run concurrently — and `reset()`s it on
|
||||
detection so the tail of one reply can't count toward the next). 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
|
||||
@@ -103,6 +108,25 @@ logs a missing-config message and exits its thread instead of starting.
|
||||
`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.
|
||||
- **`updater.py`** — self-update from the Gitea releases API. Polls
|
||||
`<UPDATE_REPO_API>/releases/latest` for a tag newer than
|
||||
`bolt_pet.__version__` and moves the checkout to it with
|
||||
`git fetch --tags` + `git checkout tags/<tag>`, so "downloading an update"
|
||||
is just git and rolling back is one command. Three safety rules: a **dirty
|
||||
working tree is skipped, never stashed** (silently discarding your
|
||||
work-in-progress beats running an old version); everything after the
|
||||
checkout — dependency install, then an **import smoke test in a
|
||||
subprocess** (this process still has the old modules loaded, so importing
|
||||
in-process would prove nothing) — is guarded, and any failure rolls back to
|
||||
the exact ref that was live before, branch name or SHA; and the restart only
|
||||
happens once the new code imports, so a broken release costs a log line
|
||||
rather than a pet that won't start. Git goes through an injectable
|
||||
`run(args) -> (code, output)` callable so apply/rollback is unit-tested
|
||||
against a fake git; version comparison and release parsing are pure.
|
||||
`controller._maybe_update` drives it from the wake-listener tick (so the pet
|
||||
is IDLE and between turns by construction) and the actual `os.execv` happens
|
||||
in `ui/app.py` *after* `app.exec()` returns — that ordering is what
|
||||
guarantees the mic is released before the new process opens it.
|
||||
- **`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
|
||||
@@ -155,6 +179,22 @@ 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.
|
||||
|
||||
**openwakeword's `Model.reset()` is not enough to forget a detection.** It
|
||||
clears the *prediction* buffer only; the rolling audio window the classifier
|
||||
actually scores lives in `model.preprocessor` (`raw_data_buffer` — 10s of raw
|
||||
audio — plus `melspectrogram_buffer` and a ~120-frame `feature_buffer`) and
|
||||
`AudioFeatures` has no reset method at all. So after a detection the wake
|
||||
phrase is still in the window, and the next frame fed to the model re-fires on
|
||||
it. Symptom when this bites: the pet cuts itself off a word into every reply,
|
||||
because wake-mode barge-in resumes feeding the model and instantly matches the
|
||||
"thunderbolt" that *started* the turn. `wake_word.hard_reset(model)` restores
|
||||
the preprocessor to its as-constructed (silence) state and is what both
|
||||
`listen_for_wake_word` and `WakeWordBargeIn.reset()` call — use it, not
|
||||
`reset()`, anywhere a detection needs to be genuinely forgotten. The blank
|
||||
state is cached on the preprocessor object (not in an `id()`-keyed dict —
|
||||
CPython reuses ids after GC), since rebuilding it costs an ONNX pass over 10s
|
||||
of silence.
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user