feat(voice): "Hey Hermes" wake word to start a hands-free session - #53378
feat(voice): "Hey Hermes" wake word to start a hands-free session#53378OutThisLife wants to merge 4 commits into
Conversation
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
13 |
unsupported-operator |
11 |
unresolved-import |
6 |
invalid-argument-type |
5 |
First entries
tools/wake_word.py:167: [unresolved-import] unresolved-import: Cannot resolve imported module `openwakeword`
tools/wake_word.py:168: [unresolved-import] unresolved-import: Cannot resolve imported module `openwakeword.model`
cli.py:11162: [unresolved-attribute] unresolved-attribute: Attribute `invalidate` is not defined on `None` in union `None | Unknown`
tools/wake_word.py:223: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `None` in union `Any | None | dict[Unknown, Unknown]`
tools/wake_word.py:114: [unresolved-import] unresolved-import: Cannot resolve imported module `numpy`
hermes_cli/mcp_startup.py:66: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
tools/browser_tool.py:1321: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
tests/cron/test_suggestions.py:213: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["monitor"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
tests/tools/test_browser_lightpanda.py:242: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["engine"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
hermes_cli/cli_commands_mixin.py:2678: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_wake_command` has no attribute `_start_wake_word_listener`
tests/tools/test_wake_word.py:12: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_destructive_slash_confirm_gate.py:32: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
tests/hermes_cli/test_aux_config.py:47: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["session_search"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
tests/tools/test_refresh_agent_mcp_tools.py:257: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
tests/hermes_cli/test_aux_config.py:54: [unresolved-attribute] unresolved-attribute: Attribute `keys` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
tests/cli/test_reasoning_command.py:552: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
hermes_cli/cli_commands_mixin.py:2676: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_wake_command` has no attribute `_stop_wake_word_listener`
tests/cli/test_resume_display.py:716: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["resume_display"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
hermes_cli/cli_commands_mixin.py:2680: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_wake_command` has no attribute `_show_wake_word_status`
tools/wake_word.py:213: [unresolved-import] unresolved-import: Cannot resolve imported module `pvporcupine`
tests/tools/test_web_providers.py:219: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["extract_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
tests/hermes_cli/test_kanban_core_functionality.py:3431: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
tools/wake_word.py:115: [unresolved-import] unresolved-import: Cannot resolve imported module `sounddevice`
hermes_cli/config.py:5264: [unresolved-attribute] unresolved-attribute: Attribute `items` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
tests/tools/test_browser_console.py:341: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["record_sessions"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
... and 10 more
✅ Fixed issues (23):
| Rule | Count |
|---|---|
unsupported-operator |
11 |
unresolved-attribute |
8 |
invalid-argument-type |
4 |
First entries
tools/browser_tool.py:1321: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/cli/test_fast_command.py:484: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/hermes_cli/test_destructive_slash_confirm_gate.py:32: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/agent/test_curator.py:1208: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["curator"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/tools/test_refresh_agent_mcp_tools.py:257: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/hermes_cli/test_aux_config.py:54: [unresolved-attribute] unresolved-attribute: Attribute `keys` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/hermes_cli/test_kanban_core_functionality.py:3431: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
hermes_cli/config.py:5233: [unresolved-attribute] unresolved-attribute: Attribute `items` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/run_agent/test_in_place_compaction.py:257: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/tools/test_web_providers.py:217: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
hermes_cli/config.py:5243: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/hermes_cli/test_mcp_reload_confirm_gate.py:33: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/gateway/test_whatsapp_reply_prefix.py:119: [unsupported-operator] unsupported-operator: Operator `>=` is not supported between objects of type `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements` and `int`
tests/cron/test_suggestions.py:213: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["monitor"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/cli/test_reasoning_command.py:552: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/tools/test_browser_console.py:341: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["record_sessions"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/tools/test_browser_lightpanda.py:242: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["engine"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/cli/test_resume_display.py:716: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["resume_display"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/hermes_cli/test_aux_config.py:37: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["title_generation"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/tools/test_web_providers.py:219: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["extract_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
hermes_cli/mcp_startup.py:66: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/tools/test_web_providers.py:218: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["search_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
tests/hermes_cli/test_aux_config.py:47: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["session_search"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 36 union elements`
Unchanged: 6118 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Comment (high surface area)
This PR adds 1,433 lines (68KB diff) introducing voice features across desktop, gateway, and agent layers. Changes span:
- Desktop: New voice start event, composer integration, wake word handling
- Gateway: Voice session management, gateway state updates
- Agent: Voice provider integration
While the feature is well-scoped to voice interaction, the cross-cutting nature (desktop + gateway + agent) and the 1,433-line addition size warrant human review. Key concerns:
- New event system (VOICE_START_EVENT) needs integration verification
- Gateway state management changes need testing
- Voice session lifecycle needs validation
Recommendation: Defer to human review for this cross-cutting feature.
Reviewed by Hermes Agent
71c2e51 to
6f19888
Compare
૮ >ﻌ< ა ci reviewran on 17dc350 all good! |
6462977 to
c7cd8f0
Compare
c7cd8f0 to
5d64d30
Compare
… model tools/wake_word.py is a shared, engine-pluggable detector (openWakeWord default, free/local; Porcupine premium) over the existing 16 kHz sounddevice capture. A background daemon thread with pause()/resume() yields the mic during a voice turn, and reset() on every (re)start keeps a resume from re-firing on stale audio. Ships a bundled "hey hermes" openWakeWord model (tools/wakewords/, Apache-2.0) as the default; a built-in name or a custom .onnx/.tflite path still works. download_models() runs for any model so a fresh install fetches the shared feature models instead of crashing on a missing melspectrogram.onnx. The wake deps lazy-install on first use, or via the [wake] extra. Packaging ships the bundled model in both wheel and sdist, guarded by a metadata test.
Adds the wake_word config block (surface, provider, phrase, sensitivity, and per-engine options) with wake_surface_enabled() so exactly one surface owns the listener and the session it opens. In the CLI the detector runs in-process; on wake it opens a fresh session and captures a single utterance through the existing voice pipeline, with an idle watchdog that re-arms the mic. The /wake [on|off|status] command reports what is configured and what is missing.
…-free voice The TUI and desktop GUI share the Python tui_gateway, which runs the detector server-side and exposes wake.start/stop/pause/resume/status plus a wake.detected event routed back over the same transport that armed it. Clients arm it on connect; on wake the desktop opens a fresh session, starts voice, and hands the mic between the detector and its browser voice loop. An empty STT transcript (silence) is treated as a quiet re-listen rather than a "transcription failed" toast.
Adds the wake-word user guide (setup, config, per-engine phrase changes, requirements, limits) and links it from the features overview.
5d64d30 to
17dc350
Compare
|
Closing in favor of #58539, which is an explicit replacement for this PR: it rebases these commits onto current All the substantive work here — the shared detector, bundled "hey hermes" model, |
…box default From #53378: ships hey_hermes.onnx/.tflite (openWakeWord pipeline, Apache-2.0) under tools/wakewords/, resolves the default (and hey_hermes aliases) to the bundled file, ensures openWakeWord base feature models are fetched for custom paths too, and updates config defaults + docs from hey_jarvis to hey hermes.
…box default From #53378: ships hey_hermes.onnx/.tflite (openWakeWord pipeline, Apache-2.0) under tools/wakewords/, resolves the default (and hey_hermes aliases) to the bundled file, ensures openWakeWord base feature models are fetched for custom paths too, and updates config defaults + docs from hey_jarvis to hey hermes.
…box default From #53378: ships hey_hermes.onnx/.tflite (openWakeWord pipeline, Apache-2.0) under tools/wakewords/, resolves the default (and hey_hermes aliases) to the bundled file, ensures openWakeWord base feature models are fetched for custom paths too, and updates config defaults + docs from hey_jarvis to hey hermes.
…box default From #53378: ships hey_hermes.onnx/.tflite (openWakeWord pipeline, Apache-2.0) under tools/wakewords/, resolves the default (and hey_hermes aliases) to the bundled file, ensures openWakeWord base feature models are fetched for custom paths too, and updates config defaults + docs from hey_jarvis to hey hermes.
…box default From NousResearch#53378: ships hey_hermes.onnx/.tflite (openWakeWord pipeline, Apache-2.0) under tools/wakewords/, resolves the default (and hey_hermes aliases) to the bundled file, ensures openWakeWord base feature models are fetched for custom paths too, and updates config defaults + docs from hey_jarvis to hey hermes.
Summary
Adds an opt-in, fully on-device "Hey Hermes" wake word — the "Hey Siri" pattern — across all three local surfaces (CLI, TUI, desktop GUI), with one configurable owner. Say the phrase → Hermes opens a fresh session and starts hands-free voice; talk back-and-forth; end it and the wake word re-arms.
"Hey Hermes" works out of the box — a trained openWakeWord model ships with Hermes and is the default. Off by default though; nothing listens until you turn it on.
Footprint
.envonly for the secret. All behavior is inconfig.yaml; onlyPORCUPINE_ACCESS_KEY(a credential) is an env var.How it works
tools/wake_word.pyis the shared, engine-pluggable detector over the existing 16 kHzsounddevicecapture path (background daemon thread,pause()/resume()so it yields the mic during a voice turn,reset()on every (re)start so a resume can't re-fire on stale audio).tui_gateway, which runs the detector server-side and exposeswake.start/stop/pause/resume/status+ awake.detectedevent. Clients arm it on connect and react towake.detected(open a fresh session, start voice). Events route back over the same transport that armed it (WS for the desktop), and the desktop pauses/resumes the detector around its browser-mic loop.Configuration
surfaceis the single gate (wake_surface_enabled) every surface consults, so exactly one owns the listener and the new session it opens.Using a different phrase
"Hey Hermes" is the default. To wake on something else:
hey_jarvis,alexa, …) or train your own (~75–90 min on a free Colab GPU) and pointopenwakeword.modelat the.onnx. Refs: openWakeWord, 2026 training Colab. Tip: a distinctive 2-word phrase generalizes far better than common words..ppn, setPORCUPINE_ACCESS_KEYin~/.hermes/.env, and pointporcupine.keywordat it.Both engines lazy-install on first use, or:
uv pip install 'hermes-agent[wake]'. On first use openWakeWord fetches its shared feature-extraction models (a small one-time download).Requirements
sounddevice/numpystack (shared with voice mode).faster-whisperworks out of the box).[wake]extra)./wake status(CLI) reports exactly what's missing.Notes / limits
Also in this PR
tools/wakewords/, trained with the openWakeWord pipeline, Apache-2.0) + packaging so it ships in the wheel and sdist..onnxpath used to skipdownload_models(), so a fresh install crashed on a missingmelspectrogram.onnx; the base feature models are now ensured for any model.Test plan
scripts/run_tests.sh tests/tools/test_wake_word.py— config/surface gate, engine dispatch, requirements probe, detector fire/cooldown/reset, singleton lifecycle, bundled-model resolution + base-model fetch (all mocked, no live audio/network).tests/test_packaging_metadata.pyguards the bundled model shipping in both wheel and sdist.tests/tools/test_lazy_deps.py,tests/hermes_cli/test_commands.py,tests/hermes_cli/test_config.pygreen.tsc --noEmitclean for bothui-tuiandapps/desktop.wake.start/detected/pause/resumein gui.log).