From 20e4603bf7efcc0ce4c585bb4dddb5256bb9cc16 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:28:19 -0700 Subject: [PATCH] refactor(tools): unify wake_word singleton teardown, tighten docstrings across slice A --- tools/voice_mode_transcript.py | 18 ++-- tools/wake_word.py | 187 +++++++++++---------------------- tools/wake_word_engines.py | 56 +++------- tools/working_diff.py | 20 ++-- tools/write_approval.py | 59 ++++------- 5 files changed, 111 insertions(+), 229 deletions(-) diff --git a/tools/voice_mode_transcript.py b/tools/voice_mode_transcript.py index b5706f2a9a..3c44955069 100644 --- a/tools/voice_mode_transcript.py +++ b/tools/voice_mode_transcript.py @@ -68,12 +68,9 @@ def _configured_stop_phrases() -> tuple: def is_voice_stop_phrase(transcript: str, stop_phrases: Optional[tuple] = None) -> bool: - """True when *transcript* is EXACTLY a configured stop phrase. - - Deliberately strict: the whole utterance — lowercased, surrounding punctuation - stripped — must equal a phrase, so "stop doing that and try again" still reaches - the agent. ``voice.stop_phrases: []`` disables. - """ + """True when *transcript* is EXACTLY a configured stop phrase. Deliberately strict: the whole + utterance — lowercased, surrounding punctuation stripped — must equal a phrase, so "stop doing + that and try again" still reaches the agent. ``voice.stop_phrases: []`` disables.""" cleaned = transcript.strip().lower().strip(".,!?;: \t\n\"'") if transcript else "" if not cleaned: return False @@ -126,11 +123,8 @@ def is_tts_echo(transcript: str, spoken_text: str, def voice_stop_hint() -> str: - """One-line 'Say "stop" to end the voice chat.' hint for voice-mode start. - - Uses the first ``voice.stop_phrases`` entry so a custom phrase renders correctly; - "" when stop phrases are disabled. Every surface announcing voice-mode start (CLI, - TUI, desktop) uses this one owner instead of hardcoding the wording. - """ + """One-line 'Say "stop" to end the voice chat.' hint for voice-mode start, using the first + ``voice.stop_phrases`` entry ("" when disabled). Every surface announcing voice-mode start + (CLI, TUI, desktop) uses this one owner instead of hardcoding the wording.""" phrases = _configured_stop_phrases() return f'Say "{phrases[0]}" to end the voice chat.' if phrases else "" diff --git a/tools/wake_word.py b/tools/wake_word.py index 10f1e4a44e..e72819054b 100644 --- a/tools/wake_word.py +++ b/tools/wake_word.py @@ -58,18 +58,12 @@ class WakeWordInUse(RuntimeError): # ── Config ── +# capture: "local" (PortAudio on the backend host), "client" (desktop/TUI streams int16 +# frames via wake.feed), or "auto" (local when a device exists, else client). _DEFAULTS: Dict[str, Any] = { - "enabled": False, - "surface": "auto", - "input_device": None, - # capture: "local" (PortAudio on the backend host), "client" (desktop/TUI streams - # int16 frames via wake.feed), or "auto" (local when a device exists, else client). - "capture": "auto", - "provider": "openwakeword", - "phrase": "hey hermes", - "sensitivity": 0.6, - "confirmation_frames": _DEFAULT_CONFIRMATION_FRAMES, - "start_new_session": True, + "enabled": False, "surface": "auto", "input_device": None, "capture": "auto", + "provider": "openwakeword", "phrase": "hey hermes", "sensitivity": 0.6, + "confirmation_frames": _DEFAULT_CONFIRMATION_FRAMES, "start_new_session": True, } # Bundled "hey hermes" model (tools/wakewords/) — the default; alias names resolve @@ -86,7 +80,6 @@ def _bundled_wakeword_path(framework: str = "onnx") -> str: def _is_macos_arm64() -> bool: import platform - return sys.platform == "darwin" and platform.machine() == "arm64" @@ -123,14 +116,11 @@ def resolve_inference_framework(cfg: Dict[str, Any]) -> str: def ensure_tflite_runtime() -> bool: - """Make ``import tflite_runtime.interpreter`` resolve, returning success. - - openWakeWord hardcodes that import but only declares ``tflite-runtime`` on Linux; - on macOS the wheel is ``ai-edge-litert``. Alias it in-process (site-packages untouched). - """ + """Make ``import tflite_runtime.interpreter`` resolve, returning success. openWakeWord hardcodes + that import but only declares ``tflite-runtime`` on Linux; on macOS the wheel is ``ai-edge-litert``, + so alias it in-process (site-packages untouched).""" try: import tflite_runtime.interpreter # noqa: F401 - return True except ImportError: pass @@ -139,7 +129,6 @@ def ensure_tflite_runtime() -> bool: except ImportError: return False import types - pkg = types.ModuleType("tflite_runtime") pkg.__path__ = [] # type: ignore[attr-defined] # mark as package sys.modules.setdefault("tflite_runtime", pkg) @@ -152,7 +141,6 @@ def load_wake_word_config() -> Dict[str, Any]: """Return the ``wake_word`` config section, shape-guarded to a dict.""" try: from hermes_cli.config import load_config - cfg = load_config().get("wake_word") except Exception: cfg = None @@ -202,13 +190,10 @@ def wake_phrase(cfg: Optional[Dict[str, Any]] = None) -> str: def resolve_capture_mode(cfg: Optional[Dict[str, Any]] = None, *, prefer_client: bool = False, force_local: bool = False) -> str: - """Return ``local`` or ``client`` capture mode for this arm. - - ``prefer_client`` is set by remote desktop; ``force_local`` keeps CLI/TUI on the - process mic. Under ``auto`` a working backend input always wins; client is the - fallback only for a preferring surface with no usable backend mic — CLI/TUI stay - local so status reports the real requirement rather than a path nothing will feed. - """ + """Return ``local`` or ``client`` capture mode for this arm. ``prefer_client`` is set by remote + desktop; ``force_local`` keeps CLI/TUI on the process mic. Under ``auto`` a working backend input + always wins; client is the fallback only for a preferring surface with no usable backend mic — + CLI/TUI stay local so status reports the real requirement rather than a path nothing will feed.""" cfg = cfg if cfg is not None else load_wake_word_config() if force_local: return "local" @@ -231,6 +216,9 @@ def _local_input_device_ready() -> bool: """True when PortAudio is importable and at least one input device exists.""" try: sd, _ = _import_audio() + except (ImportError, OSError): + return False + try: devices = sd.query_devices() if isinstance(devices, dict): return _input_channels(devices) > 0 @@ -243,11 +231,8 @@ def _local_input_device_ready() -> bool: def wake_surface_enabled(surface: str, cfg: Optional[Dict[str, Any]] = None) -> bool: - """Should ``surface`` (``cli`` / ``tui`` / ``gui``) host the listener? - - True when enabled and the configured ``surface`` is ``auto`` or this exact surface. - ``auto`` only makes a surface eligible; the ownership lock still admits one claimant. - """ + """Should ``surface`` (cli/tui/gui) host the listener? True when enabled and the configured + surface is ``auto`` or this one; ``auto`` only makes it eligible — the lock admits one claimant.""" cfg = cfg if cfg is not None else load_wake_word_config() if not cfg.get("enabled"): return False @@ -260,24 +245,19 @@ def wake_surface_enabled(surface: str, cfg: Optional[Dict[str, Any]] = None) -> def _active_profile_name() -> str: try: from hermes_cli.profiles import get_active_profile_name - return get_active_profile_name() or "default" except Exception: return "default" def enrolled_profile_phrases() -> Dict[str, str]: - """Map ``profile name -> wake phrase`` for every wake-enabled profile. - - Reads each profile's own ``config.yaml`` raw (``load_config()`` targets only the - ACTIVE profile). Phrase defaults to ``"hey "``; the sherpa engine listens - for all of them and routes the wake to the matching profile. Unreadable → skipped. - """ + """Map ``profile name -> wake phrase`` for every wake-enabled profile, reading each profile's own + ``config.yaml`` raw (``load_config()`` targets only the ACTIVE profile). Phrase defaults to + ``"hey "``; the sherpa engine listens for all and routes to the match. Unreadable → skipped.""" phrases: Dict[str, str] = {} try: from hermes_cli.config import read_user_config_raw from hermes_cli.profiles import get_profile_dir, list_profiles - for info in list_profiles(): name = getattr(info, "name", None) or str(info) try: @@ -299,7 +279,6 @@ def enrolled_profile_phrases() -> Dict[str, str]: def _import_audio(): import numpy as np import sounddevice as sd - return sd, np @@ -312,10 +291,8 @@ def _audio_available() -> bool: def _describe_input_device(sd, selector: int | str | None) -> Dict[str, Any]: - """Resolve a PortAudio selector into JSON-safe diagnostics. - - Diagnostic only: ``InputStream`` stays the authority on whether the device opens. - """ + """Resolve a PortAudio selector into JSON-safe diagnostics (``InputStream`` stays the + authority on whether the device actually opens).""" details: Dict[str, Any] = {"selector": selector} try: info = sd.query_devices(selector, "input") @@ -410,7 +387,6 @@ def _stt_ready() -> bool: mic but every utterance dies at transcription — same bar as ``check_voice_requirements``.)""" try: from tools.transcription_tools import _get_provider, _load_stt_config, is_stt_enabled - stt_config = _load_stt_config() return is_stt_enabled(stt_config) and _get_provider(stt_config) != "none" except Exception: @@ -421,15 +397,11 @@ _LAZY_TTS_FEATURES = {"edge": "tts.edge", "elevenlabs": "tts.elevenlabs", "mistr def _tts_ready() -> bool: - """Can the configured TTS provider run (or install at first use)? - - PROBE, not an installer: ``check_tts_requirements`` lazily pip-installs the provider - SDK, which froze wake.status polls for a whole pip run. Uninstalled deps count as - ready iff lazy installs are allowed; pip is never touched from here. - """ + """Can the configured TTS provider run (or install at first use)? PROBE, not an installer: + ``check_tts_requirements`` lazily pip-installs the SDK, which froze wake.status polls for a whole + pip run. Uninstalled deps count as ready iff lazy installs are allowed; pip is never touched here.""" try: from tools.tts_tool import _get_provider, _load_tts_config - provider = _get_provider(_load_tts_config()) except Exception: return False @@ -437,14 +409,12 @@ def _tts_ready() -> bool: if feature is not None: try: from tools import lazy_deps - if not lazy_deps.is_available(feature): return lazy_deps._allow_lazy_installs() except Exception: return False try: from tools.tts_tool import check_tts_requirements - return bool(check_tts_requirements()) except Exception: return False @@ -455,7 +425,6 @@ def check_wake_word_requirements(cfg: Optional[Dict[str, Any]] = None) -> Dict[s cfg = cfg if cfg is not None else load_wake_word_config() provider = _provider(cfg) from tools import lazy_deps - feature = _PROVIDERS.get(provider, ("", "wake.openwakeword"))[1] deps_ok = lazy_deps.is_available(feature) lazy_ok = lazy_deps._allow_lazy_installs() @@ -501,17 +470,11 @@ def check_wake_word_requirements(cfg: Optional[Dict[str, Any]] = None) -> Dict[s ) return { - "available": key_ok and stt_ok and tts_ok and tflite_ok and mic_ok, - "provider": provider, - "deps_available": deps_ok, - "audio_available": audio_ok, + "available": key_ok and stt_ok and tts_ok and tflite_ok and mic_ok, "provider": provider, + "deps_available": deps_ok, "audio_available": audio_ok, "local_input_available": _local_input_device_ready() if deps_ok else False, - "capture": capture_mode, - "access_key_set": key_ok, - "stt_available": stt_ok, - "tts_available": tts_ok, - "phrase": wake_phrase(cfg), - "hint": hint, + "capture": capture_mode, "access_key_set": key_ok, "stt_available": stt_ok, "tts_available": tts_ok, + "phrase": wake_phrase(cfg), "hint": hint, } @@ -546,11 +509,8 @@ class _Capture: class WakeWordDetector: - """Background hotword listener. Fires ``on_wake()`` when the phrase is heard. - - The engine is built once and kept alive across pause/resume; only the audio stream - + reader thread cycle, so toggling the mic for a voice turn is cheap. - """ + """Background hotword listener; fires ``on_wake()`` when the phrase is heard. The engine is built + once and kept across pause/resume — only the stream + reader thread cycle, so mic toggles are cheap.""" def __init__(self, engine: _Engine, on_wake: Callable[[], None], cooldown: float = _FIRE_COOLDOWN_SECONDS, @@ -558,28 +518,20 @@ class WakeWordDetector: input_device: int | str | None = None, external_audio: bool = False): import queue as _queue - - self.engine = engine - self.on_wake = on_wake - self.cooldown = cooldown - self.on_failure = on_failure - self.input_device = input_device - self.external_audio = bool(external_audio) + self.engine, self.on_wake, self.cooldown, self.on_failure = engine, on_wake, cooldown, on_failure + self.input_device, self.external_audio = input_device, bool(external_audio) self.input_device_details: Dict[str, Any] = ( {"selector": "client", "name": "client capture", "hostapi": "remote"} if self.external_audio else {"selector": input_device} ) self._thread: Optional[threading.Thread] = None - self._stop = threading.Event() - self._callback_inflight = threading.Event() - self._last_fire = 0.0 - self._lock = threading.Lock() + self._stop, self._callback_inflight = threading.Event(), threading.Event() + self._lock, self._last_fire = threading.Lock(), 0.0 # Client-capture PCM queue (int16 mono frames). Local mode ignores this. self._audio_q: "_queue.Queue[Any]" = _queue.Queue(maxsize=64) # True when the stream is open but every frame is (near-)silence, so status # surfaces can tell "armed" from "deaf". - self.audio_silent = False - self._silent_frames = 0 + self.audio_silent, self._silent_frames = False, 0 @property def running(self) -> bool: @@ -587,11 +539,8 @@ class WakeWordDetector: return t is not None and t.is_alive() def feed(self, pcm_int16) -> None: - """Enqueue one int16 mono frame (or raw bytes) for client capture. - - Short frames are zero-padded to ``engine.frame_length``; long frames are split. - On queue overflow the oldest frame is dropped to stay real-time. - """ + """Enqueue one int16 mono frame (or raw bytes) for client capture. Short frames are + zero-padded to ``engine.frame_length``, long ones split; on overflow the oldest is dropped.""" if not self.external_audio: return try: @@ -608,9 +557,7 @@ class WakeWordDetector: for offset in range(0, int(arr.shape[0]), fl): chunk = arr[offset : offset + fl] if chunk.shape[0] < fl: - pad = np.zeros(fl, dtype=np.int16) - pad[: chunk.shape[0]] = chunk - chunk = pad + chunk = np.pad(chunk, (0, fl - chunk.shape[0])) try: self._audio_q.put_nowait(chunk) except Exception: @@ -686,12 +633,10 @@ class WakeWordDetector: details = self.input_device_details = _describe_input_device(sd, self.input_device) cap = _Capture(np=np, rate=_capture_sample_rate(details)) cap.frame_length = max(1, int(round(frame_length * cap.rate / SAMPLE_RATE))) - logger.info( - "wake word: opening microphone device=%s selector=%r hostapi=%s " - "default_rate=%s capture_rate=%d engine_rate=%d", - details.get("name") or "system default", self.input_device, details.get("hostapi") or "unknown", - details.get("default_samplerate") or "unknown", cap.rate, SAMPLE_RATE, - ) + logger.info("wake word: opening microphone device=%s selector=%r hostapi=%s " + "default_rate=%s capture_rate=%d engine_rate=%d", details.get("name") or "system default", + self.input_device, details.get("hostapi") or "unknown", + details.get("default_samplerate") or "unknown", cap.rate, SAMPLE_RATE) try: cap.stream = sd.InputStream(device=self.input_device, samplerate=cap.rate, channels=1, dtype="int16", blocksize=cap.frame_length) @@ -791,7 +736,6 @@ _detector_lock = threading.Lock() def _lock_path() -> Path: from hermes_constants import get_default_hermes_root - return get_default_hermes_root() / "runtime" / "wake-word.lock" @@ -799,7 +743,6 @@ def _flock(handle, acquire: bool) -> None: """Non-blocking exclusive lock (or unlock) of one byte / whole file, per OS.""" if os.name == "nt": import msvcrt - if acquire: # msvcrt needs at least one byte to lock handle.seek(0, os.SEEK_END) if handle.tell() == 0: @@ -809,7 +752,6 @@ def _flock(handle, acquire: bool) -> None: msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK if acquire else msvcrt.LK_UNLCK, 1) else: import fcntl - fcntl.flock(handle.fileno(), (fcntl.LOCK_EX | fcntl.LOCK_NB) if acquire else fcntl.LOCK_UN) @@ -850,26 +792,27 @@ def _owned_detector(owner: object) -> Optional[WakeWordDetector]: return _detector if _detector is not None and _detector_owner is owner else None +def _teardown_locked(close: Callable[[], None]) -> None: + """Forget the singleton and run ``close``, always releasing the machine lease (caller holds the lock).""" + _, lock_handle = _clear_singleton_locked() + try: + close() + finally: + _release_machine_lock(lock_handle) + + def _detector_failed(detector: WakeWordDetector) -> None: """Release ownership if the active microphone stream dies unexpectedly.""" with _detector_lock: - if _detector is not detector: - return - _, lock_handle = _clear_singleton_locked() - try: - detector.engine.close() - finally: - _release_machine_lock(lock_handle) + if _detector is detector: + _teardown_locked(detector.engine.close) def start_listening(on_wake: Callable[[], None], *, owner: object, config: Optional[Dict[str, Any]] = None, external_audio: bool = False) -> WakeWordDetector: - """Claim, build, and start the detector. Idempotent for the same owner. - - Raises if engine construction fails (missing deps / access key / model); callers - should probe :func:`check_wake_word_requirements` first. A different owner, - including another process, receives :class:`WakeWordInUse`. - """ + """Claim, build, and start the detector. Idempotent for the same owner; a different owner + (or process) gets :class:`WakeWordInUse`. Raises if engine construction fails (missing deps / + access key / model) — callers should probe :func:`check_wake_word_requirements` first.""" if owner is None: raise ValueError("wake-word owner must not be None") @@ -927,13 +870,10 @@ def resume_listening(*, owner: object) -> bool: def stop_listening(*, owner: object) -> bool: """Fully stop the detector only when ``owner`` holds the lease.""" with _detector_lock: - if _owned_detector(owner) is None: + det = _owned_detector(owner) + if det is None: return False - det, lock_handle = _clear_singleton_locked() - try: - det.stop() - finally: - _release_machine_lock(lock_handle) + _teardown_locked(det.stop) return True @@ -990,8 +930,5 @@ def detector_frame_info() -> Dict[str, Any]: det = _current_detector() if det is None: return {"sample_rate": SAMPLE_RATE, "frame_length": 1280} - return { - "sample_rate": SAMPLE_RATE, - "frame_length": int(getattr(det.engine, "frame_length", 1280) or 1280), - "external_audio": bool(det.external_audio), - } + return {"sample_rate": SAMPLE_RATE, "external_audio": bool(det.external_audio), + "frame_length": int(getattr(det.engine, "frame_length", 1280) or 1280)} diff --git a/tools/wake_word_engines.py b/tools/wake_word_engines.py index 3a3afb4825..a36a9c4e39 100644 --- a/tools/wake_word_engines.py +++ b/tools/wake_word_engines.py @@ -17,13 +17,11 @@ logger = logging.getLogger("tools.wake_word") def _ww(): from tools import wake_word - return wake_word def _ensure_dep(feature: str) -> None: from tools import lazy_deps - lazy_deps.ensure(feature, prompt=False) @@ -56,12 +54,9 @@ def _sub(cfg: Dict[str, Any], key: str) -> Dict[str, Any]: class _OpenWakeWordEngine(_Engine): - """openWakeWord — free, local ONNX/tflite hotword detection. - - Scores one ~80 ms frame at a time; ``sensitivity`` IS the raw 0..1 threshold - (higher = stricter). A real utterance holds the score high across frames while a - stray phoneme spikes one, so ``confirmation_frames`` consecutive hits are required. - """ + """openWakeWord — free, local ONNX/tflite hotword detection. Scores one ~80 ms frame at a time; + ``sensitivity`` IS the raw 0..1 threshold (higher = stricter). A real utterance holds the score + high across frames while a stray phoneme spikes one, so ``confirmation_frames`` hits are required.""" frame_length = 1280 # openWakeWord recommends 80 ms frames. @@ -69,7 +64,6 @@ class _OpenWakeWordEngine(_Engine): _ensure_dep("wake.openwakeword") import openwakeword from openwakeword.model import Model - ww = _ww() model_ref = str(_sub(cfg, "openwakeword").get("model") or ww._BUNDLED_MODEL_NAME).strip() framework = self._usable_framework(ww.resolve_inference_framework(cfg)) @@ -90,13 +84,9 @@ class _OpenWakeWordEngine(_Engine): @staticmethod def _usable_framework(framework: str) -> str: - """Refuse openWakeWord's silent tflite→onnx downgrade. - - Without a tflite runtime openWakeWord falls back to onnx, which on macOS ARM64 - never fires — the listener would arm and stay deaf. Install + bridge the runtime - first (the platform gate lives here because dep specs can't carry PEP 508 - markers); on that Mac raise instead of downgrading. - """ + """Refuse openWakeWord's silent tflite→onnx downgrade: without a tflite runtime it falls back + to onnx, which on macOS ARM64 never fires (armed but deaf). Install + bridge the runtime first + (gate lives here because dep specs can't carry PEP 508 markers); on that Mac raise instead.""" ww = _ww() if framework != "tflite" or ww.ensure_tflite_runtime(): return framework @@ -107,10 +97,8 @@ class _OpenWakeWordEngine(_Engine): if ww.ensure_tflite_runtime(): return framework if ww._is_macos_arm64(): - raise RuntimeError( - "The wake word needs the tflite backend on this Mac, but its " - "runtime is missing. Install it with: pip install ai-edge-litert" - ) + raise RuntimeError("The wake word needs the tflite backend on this Mac, but its " + "runtime is missing. Install it with: pip install ai-edge-litert") logger.warning("wake word: no tflite runtime available — falling back to onnx") return "onnx" @@ -149,7 +137,6 @@ _SHERPA_KWS_MODEL_DIR = "sherpa-onnx-kws-zipformer-gigaspeech-3.3M-2024-01-01" def _sherpa_model_root() -> Path: from hermes_constants import get_hermes_home - return get_hermes_home() / "cache" / "wakewords" @@ -161,7 +148,6 @@ def _ensure_sherpa_model(root: Optional[Path] = None) -> Path: return target import tarfile import urllib.request - root.mkdir(parents=True, exist_ok=True) archive = root / f"{_SHERPA_KWS_MODEL_DIR}.tar.bz2" logger.info("wake word: downloading sherpa KWS model (one-time, ~13 MB)") @@ -175,11 +161,8 @@ def _ensure_sherpa_model(root: Optional[Path] = None) -> Path: class _SherpaKwsEngine(_Engine): - """sherpa-onnx open-vocabulary keyword spotting — any typed phrase, zero training. - - ``wake_word.phrase`` is BPE-tokenized at runtime against the model's vocabulary, - so here ``phrase`` is DETECTION config, not a cosmetic label. - """ + """sherpa-onnx open-vocabulary keyword spotting — any typed phrase, zero training. ``wake_word.phrase`` + is BPE-tokenized at runtime against the model's vocabulary: DETECTION config, not a cosmetic label.""" frame_length = 1280 # streaming zipformer accepts any chunk; match capture path. @@ -188,7 +171,6 @@ class _SherpaKwsEngine(_Engine): import sherpa_onnx import tempfile from sherpa_onnx import text2token - ww = _ww() model_dir = str(_sub(cfg, "sherpa").get("model_dir") or "").strip() d = Path(model_dir) if model_dir else _ensure_sherpa_model() @@ -230,19 +212,14 @@ class _SherpaKwsEngine(_Engine): return str(hits[0]) self._spotter = sherpa_onnx.KeywordSpotter( - tokens=str(d / "tokens.txt"), - encoder=_model_file("encoder-*[!8].onnx"), - decoder=_model_file("decoder-*[!8].onnx"), - joiner=_model_file("joiner-*[!8].onnx"), - keywords_file=self._keywords_file, - keywords_threshold=threshold, - num_threads=1, + tokens=str(d / "tokens.txt"), encoder=_model_file("encoder-*[!8].onnx"), + decoder=_model_file("decoder-*[!8].onnx"), joiner=_model_file("joiner-*[!8].onnx"), + keywords_file=self._keywords_file, keywords_threshold=threshold, num_threads=1, ) self._stream = self._spotter.create_stream() def process(self, frame) -> bool: import numpy as np - self._stream.accept_waveform(_ww().SAMPLE_RATE, np.asarray(frame, dtype=np.float32) / 32768.0) fired = False while self._spotter.is_ready(self._stream): @@ -276,13 +253,10 @@ class _PorcupineEngine(_Engine): def __init__(self, cfg: Dict[str, Any]): _ensure_dep("wake.porcupine") import pvporcupine - access_key = (os.getenv("PORCUPINE_ACCESS_KEY") or "").strip() if not access_key: - raise RuntimeError( - "Porcupine wake word requires PORCUPINE_ACCESS_KEY " - "(get a free key at https://console.picovoice.ai)." - ) + raise RuntimeError("Porcupine wake word requires PORCUPINE_ACCESS_KEY " + "(get a free key at https://console.picovoice.ai).") keyword = str(_sub(cfg, "porcupine").get("keyword") or "jarvis").strip() # Porcupine's `sensitivities` runs the OPPOSITE way to our shared knob (higher = # looser); invert so "higher = stricter" holds for every engine. diff --git a/tools/working_diff.py b/tools/working_diff.py index 2b6f74d92d..ae61a1fc21 100644 --- a/tools/working_diff.py +++ b/tools/working_diff.py @@ -28,13 +28,10 @@ VALID_MODES = tuple(_MODE_ARGS) def _run(args: List[str], cwd: str, timeout: int = _GIT_TIMEOUT): - """Run git, returning (returncode, stdout). Never raises on git failure. - - Hardened against a malicious repo's ``.git/config`` (GHSA-7x36-8jrh-v4pw): - ``noninteractive_git_env`` disables fsmonitor/hooks/pager/editor/credential sinks, - and ``harden_git_argv`` appends ``--no-ext-diff --no-textconv`` to diff-rendering - subcommands so attribute-scoped diff/textconv drivers can't execute either. - """ + """Run git, returning (returncode, stdout). Never raises on git failure. Hardened against a + malicious repo's ``.git/config`` (GHSA-7x36-8jrh-v4pw): ``noninteractive_git_env`` disables + fsmonitor/hooks/pager/editor/credential sinks and ``harden_git_argv`` appends ``--no-ext-diff + --no-textconv`` to diff-rendering subcommands so attribute-scoped drivers can't execute either.""" proc = subprocess.run( ["git", "-c", "core.quotePath=false", *harden_git_argv(args)], cwd=cwd, capture_output=True, text=True, timeout=timeout, encoding="utf-8", errors="replace", @@ -65,12 +62,9 @@ def _untracked_diff(cwd: str, files: List[str]) -> str: def collect_working_diff(cwd: str, mode: str = "working", paths: List[str] | None = None) -> Dict: - """Collect a git diff of the working directory. - - Returns ``{"success", "stat", "diff", "untracked", "empty"}`` on success or - ``{"success": False, "error": ...}`` when git is unavailable / not a repo. ``paths`` - restricts the diff to pathspecs (passed verbatim); untracked files are then skipped. - """ + """Collect a git diff of the working directory: ``{"success", "stat", "diff", "untracked", "empty"}`` + on success or ``{"success": False, "error": ...}`` when git is unavailable / not a repo. ``paths`` + restricts the diff to pathspecs (passed verbatim); untracked files are then skipped.""" if mode not in _MODE_ARGS: return {"success": False, "error": f"Unknown mode '{mode}'. Use: {', '.join(VALID_MODES)}"} if not shutil.which("git"): diff --git a/tools/write_approval.py b/tools/write_approval.py index 5adc70493b..7ee4d68493 100644 --- a/tools/write_approval.py +++ b/tools/write_approval.py @@ -34,6 +34,7 @@ _SUBSYSTEMS = (MEMORY, SKILLS) # Per-subsystem config key. Intentionally a single boolean with no "block all writes" # state — to disable a subsystem use its own enable flag (e.g. ``memory.memory_enabled``). CONFIG_KEY = "write_approval" +_TRUTHY_STRINGS = frozenset({"on", "true", "yes", "1", "approve", "enabled"}) # --- Config resolution --- @@ -55,9 +56,7 @@ def _normalize_enabled(value: Any) -> bool: covers hand-edited configs (YAML already parses bare on/off/yes/no).""" if isinstance(value, bool): return value - if isinstance(value, str): - return value.strip().lower() in {"on", "true", "yes", "1", "approve", "enabled"} - return False + return isinstance(value, str) and value.strip().lower() in _TRUTHY_STRINGS # --- Pending store (file-backed) --- @@ -75,22 +74,15 @@ def _read_record(path: Path) -> Dict[str, Any]: def stage_write(subsystem: str, payload: Dict[str, Any], *, summary: str, origin: str) -> Dict[str, Any]: - """Persist a pending write and return its record (``id`` + metadata). - - ``payload`` is the exact kwargs to replay the write on approval; ``origin`` is - ``foreground`` or ``background_review`` (audit). Best-effort: on disk failure it - logs and still returns a record — the write is lost, which is the safe failure - for an approval gate (nothing silently committed). - """ + """Persist a pending write and return its record (``id`` + metadata). ``payload`` is the exact + kwargs to replay the write on approval; ``origin`` is ``foreground`` or ``background_review``. + Best-effort: on disk failure it logs and still returns a record — the write is lost, which is + the safe failure for an approval gate (nothing silently committed).""" pid = uuid.uuid4().hex[:8] record = { - "id": pid, - "subsystem": subsystem, - "action": payload.get("action", ""), - "summary": (summary or "").strip(), - "origin": origin or "foreground", - "created_at": time.time(), - "payload": payload, + "id": pid, "subsystem": subsystem, "action": payload.get("action", ""), + "summary": (summary or "").strip(), "origin": origin or "foreground", + "created_at": time.time(), "payload": payload, } try: path = _pending_path(subsystem, pid) @@ -164,12 +156,9 @@ def current_origin() -> str: @dataclass(slots=True, kw_only=True) class GateDecision: - """Result of evaluating the write gate. Exactly one flag is True. - - ``allow`` proceed with the real write; ``blocked`` the user denied an inline prompt - (``message`` explains why); ``stage`` the caller must ``stage_write`` the payload - (``message`` is the user-facing "staged for approval" note). - """ + """Result of evaluating the write gate; exactly one flag is True. ``allow``: do the real write; + ``blocked``: user denied the inline prompt (``message`` says why); ``stage``: caller must + ``stage_write`` the payload (``message`` is the user-facing "staged for approval" note).""" allow: bool = False blocked: bool = False @@ -184,13 +173,10 @@ def _staged(subsystem: str) -> GateDecision: def evaluate_gate(subsystem: str, *, inline_summary: str = "", inline_detail: str = "") -> GateDecision: - """Decide what to do with a pending write for ``subsystem``. - - gate off → allow; gate on + skills (any origin) or background → stage; gate on + - memory + foreground → inline prompt when an interactive channel exists, else stage. - The gate only ever delays a write, never silently refuses it; ``blocked`` is - produced only when the user actively denies the inline prompt. - """ + """Decide what to do with a pending write: gate off → allow; gate on + skills (any origin) or + background → stage; gate on + memory + foreground → inline prompt when an interactive channel + exists, else stage. The gate only ever delays a write, never silently refuses it; ``blocked`` + is produced only when the user actively denies the inline prompt.""" if not write_approval_enabled(subsystem): return GateDecision(allow=True) # Skills are too big to review inline; a background write runs in a daemon thread with no user. @@ -205,14 +191,11 @@ def evaluate_gate(subsystem: str, *, inline_summary: str = "", inline_detail: st def _prompt_inline_memory_approval(summary: str, detail: str) -> Optional[bool]: - """Prompt inline for a memory write: True approved, False denied, None → stage. - - Uses the per-thread CLI approval callback (``tools.terminal_tool.set_approval_callback``) - directly rather than ``prompt_dangerous_approval``: that wrapper falls back to - ``input()`` (deadlock-prone under prompt_toolkit; silent deny in gateway sessions) - and turns callback errors into a deny, whereas here a missing channel or failed - prompt must stage instead. - """ + """Prompt inline for a memory write: True approved, False denied, None → stage. Uses the per-thread + CLI approval callback (``tools.terminal_tool.set_approval_callback``) directly, not + ``prompt_dangerous_approval``: that wrapper falls back to ``input()`` (deadlock-prone under + prompt_toolkit; silent deny in gateway sessions) and turns callback errors into a deny, whereas + here a missing channel or failed prompt must stage instead.""" try: from tools.terminal_tool import _get_approval_callback except Exception: