From b0e715478649a2706e20429e1dcc3712539354ac Mon Sep 17 00:00:00 2001 From: nirhemo Date: Tue, 14 Jul 2026 13:24:18 +0300 Subject: [PATCH 01/13] =?UTF-8?q?feat(voice):=20Phase=201+2=20=E2=80=94=20?= =?UTF-8?q?provider=20seam=20+=20native=20audio=20service=20(mic=E2=86=92w?= =?UTF-8?q?ake=E2=86=92STT=E2=86=92Router=E2=86=92Kokoro)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 (voice-provider seam): - HUD gets a voiceMode switch ("web" | "native"); browser Web Speech becomes the fallback provider. Additive /voice/ws channel + NativeVoice client (handshake, reconnect). Guards on all mic loops so mic ownership hands over cleanly. Phase 2 (native audio pipeline, opt-in via voice.native_audio): - atlas/interface/voice/audio_service.py: AudioService state machine (idle→listening→transcribing→thinking→speaking) + NativeAudioRunner wiring the real engines: sounddevice capture, openWakeWord (hey_jarvis) wake, energy VAD, whisper.cpp (base.en) STT, shared Router.chat, Kokoro played NATIVELY. Streams state/wake/final/reply/tts over /voice/ws via a thread-safe VoiceHub. - Half-duplex for now (mic ignored while speaking); Phase 3 adds echo-cancelled full-duplex barge-in. whisper non-speech markers ([BLANK_AUDIO]) filtered. - tts.synth_pcm() for native playback; /api/voice/native status; settings voice.native_audio + voice.aec_backend. HUD reflects native events + orb abort. All deps optional (documented in requirements.txt) — without them the HUD keeps Web Speech. 56 tests pass offline (state machine via injected fakes; WS native + fallback paths; status endpoint). Real wake+STT engines verified loading/running on-device; live mic capture needs interactive permission (owner's on-device test). Co-Authored-By: Claude Opus 4.8 (1M context) --- atlas/config/settings.example.json | 4 +- atlas/interface/voice/audio_service.py | 364 +++++++++++++++++++++++++ atlas/interface/voice/tts.py | 19 ++ atlas/interface/web/app.js | 68 +++++ atlas/server/app.py | 108 +++++++- atlas/tests/test_audio_service.py | 114 ++++++++ atlas/tests/test_server.py | 45 +++ requirements.txt | 9 +- 8 files changed, 727 insertions(+), 4 deletions(-) create mode 100644 atlas/interface/voice/audio_service.py create mode 100644 atlas/tests/test_audio_service.py diff --git a/atlas/config/settings.example.json b/atlas/config/settings.example.json index 5510fde..7ee7bb8 100644 --- a/atlas/config/settings.example.json +++ b/atlas/config/settings.example.json @@ -20,7 +20,9 @@ "tts_voice": "kokoro:bm_george", "speech_rate": 1.0, "streaming_tts": true, - "mic_mute": false + "mic_mute": false, + "native_audio": false, + "aec_backend": "os" }, "model": { "mode": "api", diff --git a/atlas/interface/voice/audio_service.py b/atlas/interface/voice/audio_service.py new file mode 100644 index 0000000..3b9ad14 --- /dev/null +++ b/atlas/interface/voice/audio_service.py @@ -0,0 +1,364 @@ +"""L3 native audio service — mic → wake → VAD → STT → Router → Kokoro, streaming +state to the HUD over /voice/ws. Replaces the browser Web Speech loop with a +fully on-device, offline pipeline (openWakeWord + whisper.cpp + Kokoro). + +Phase 2 is HALF-DUPLEX: the mic is ignored while ATLAS speaks (barge-in falls back +to the UI Stop). Phase 3 swaps in echo-cancelled full-duplex barge-in. + +Every heavy dep (sounddevice, openwakeword, onnxruntime, pywhispercpp) is OPTIONAL: +if any is missing, available() is False, the service never starts, and the HUD +keeps its browser Web Speech fallback. The pipeline stages are INJECTABLE +(wake_fn / stt_fn / synth_fn / play_fn / publish), so the whole state machine is +unit-tested with fakes — no microphone or models required. +""" +from __future__ import annotations + +import queue +import re +import threading +from typing import Any, Callable + +SAMPLE_RATE = 16000 # openWakeWord + whisper both want 16 kHz mono +FRAME = 1280 # 80 ms @ 16 kHz — the openWakeWord frame size +_MAX_UTTERANCE_S = 15 # hard cap so a stuck VAD can't record forever + + +# --------------------------------------------------------------------------- # +# Dependency / model availability +# --------------------------------------------------------------------------- # +def deps_present() -> bool: + try: + import numpy, sounddevice, openwakeword, pywhispercpp # noqa: F401 + return True + except Exception: + return False + + +def wake_model_path(name: str = "hey_jarvis") -> str | None: + try: + import glob + import os + import openwakeword + d = os.path.join(os.path.dirname(openwakeword.__file__), "resources", "models") + hits = glob.glob(os.path.join(d, f"{name}*.onnx")) + return hits[0] if hits else None + except Exception: + return None + + +def available() -> bool: + """True if the native pipeline can run (deps + a wake model present).""" + return deps_present() and wake_model_path() is not None + + +# --------------------------------------------------------------------------- # +# Pub-sub bridge: audio thread → async WebSocket clients +# --------------------------------------------------------------------------- # +class VoiceHub: + """Thread-safe fan-out from the audio worker thread to /voice/ws clients. + The FastAPI lifespan binds the running loop; publish() is called off-thread.""" + + def __init__(self) -> None: + self._loop = None + self._queues: set = set() + + def bind_loop(self, loop) -> None: + self._loop = loop + + def subscribe(self): + import asyncio + q: asyncio.Queue = asyncio.Queue() + self._queues.add(q) + return q + + def unsubscribe(self, q) -> None: + self._queues.discard(q) + + def publish(self, event: dict) -> None: + loop = self._loop + if loop is None: + return + for q in list(self._queues): + try: + loop.call_soon_threadsafe(q.put_nowait, event) + except Exception: + pass + + +# --------------------------------------------------------------------------- # +# Speech text cleanup (mirror of the HUD's speechClean, so TTS never reads markup) +# --------------------------------------------------------------------------- # +def speech_clean(s: str) -> str: + if not s: + return "" + s = re.sub(r"```[\s\S]*?```", " code shown on screen ", s) + s = re.sub(r"【[^】]*】", "", s) + s = re.sub(r"\[\^?\d+\]", "", s) + s = re.sub(r"!?\[([^\]]+)\]\([^)]+\)", r"\1", s) + s = re.sub(r"\bhttps?://\S+", "", s) + s = re.sub(r"^\s{0,3}#{1,6}\s+", "", s, flags=re.M) + s = re.sub(r"[*_#`>~|]", " ", s) + s = re.sub(r"\s*&\s*", " and ", s) + s = re.sub(r"\n{2,}", ". ", s).replace("\n", ", ") + s = re.sub(r"\s+([.,!?;:])", r"\1", s) + return re.sub(r"\s{2,}", " ", s).strip() + + +def split_sentences(text: str) -> list[str]: + parts = re.split(r"(?<=[.!?])\s+", text.strip()) + return [p.strip() for p in parts if p.strip()] + + +# --------------------------------------------------------------------------- # +# The service (state machine is injectable → unit-testable without hardware) +# --------------------------------------------------------------------------- # +class AudioService: + """States: idle (await wake) → listening (record) → transcribing → thinking → + speaking → idle. Frames are int16 [FRAME]. Inject the pipeline stages for + tests; build_default() wires the real engines.""" + + def __init__( + self, + *, + router: Any, + publish: Callable[[dict], None], + wake_fn: Callable[[Any], bool], + stt_fn: Callable[[Any], str], + synth_fn: Callable[[str], Any], # text -> (float32 samples, sr) | None + play_fn: Callable[[Any, int], None], # (samples, sr) -> plays, honors abort + vad_silence_ms: int = 700, + session: str = "s_voice", + ) -> None: + self.router = router + self.publish = publish + self.wake_fn = wake_fn + self.stt_fn = stt_fn + self.synth_fn = synth_fn + self.play_fn = play_fn + self.vad_silence_ms = vad_silence_ms + self.session = session + + self.state = "idle" + self._utter: list = [] + self._silence_frames = 0 + self._abort = threading.Event() + self._turn = 0 + + # ----- helpers -------------------------------------------------------- # + def _set_state(self, value: str) -> None: + self.state = value + self.publish({"type": "state", "value": value}) + + def _rms(self, frame) -> float: + import numpy as np + f = np.asarray(frame, dtype=np.float32) / 32768.0 + return float(np.sqrt(np.mean(f * f)) if f.size else 0.0) + + @property + def _silence_limit_frames(self) -> int: + return max(1, int(self.vad_silence_ms / 80)) # 80 ms per frame + + # ----- per-frame state machine (the testable core) -------------------- # + def feed(self, frame) -> None: + """Process one 80 ms int16 frame. No-op while speaking (half-duplex).""" + if self.state in ("speaking", "transcribing", "thinking"): + return + if self.state == "idle": + try: + if self.wake_fn(frame): + self.publish({"type": "wake"}) + self._utter = [] + self._silence_frames = 0 + self._set_state("listening") + except Exception: + pass + return + if self.state == "listening": + self._utter.append(frame) + if self._rms(frame) < 0.012: + self._silence_frames += 1 + else: + self._silence_frames = 0 + too_long = len(self._utter) > int(_MAX_UTTERANCE_S * SAMPLE_RATE / FRAME) + if self._silence_frames >= self._silence_limit_frames or too_long: + self._finish_utterance() + + def _finish_utterance(self) -> None: + import numpy as np + self._set_state("transcribing") + frames = self._utter + self._utter = [] + try: + audio = np.concatenate([np.asarray(f, dtype=np.int16) for f in frames]) \ + if frames else np.zeros(0, dtype=np.int16) + samples = audio.astype(np.float32) / 32768.0 + text = (self.stt_fn(samples) or "").strip() + except Exception: + text = "" + if not text: + self._set_state("idle") + return + self.publish({"type": "final", "text": text}) + self._respond(text) + + def _respond(self, text: str) -> None: + self._abort.clear() + self._turn += 1 + self._set_state("thinking") + try: + out = self.router.chat(text, channel="voice", session=self.session, turn=self._turn) + except Exception as exc: + out = {"reply": f"(voice error: {type(exc).__name__})", "media": None} + reply = out.get("reply") or "(no reply)" + self.publish({"type": "reply", "text": reply, "media": out.get("media")}) + self._speak(reply) + + def _speak(self, reply: str) -> None: + spoken = speech_clean(reply) + if not spoken: + self._set_state("idle") + return + self._set_state("speaking") + self.publish({"type": "tts", "state": "start"}) + for sentence in split_sentences(spoken): + if self._abort.is_set(): + break + self.publish({"type": "tts", "state": "sentence_start", "text": sentence}) + try: + pcm = self.synth_fn(sentence) + if pcm: + self.play_fn(pcm[0], pcm[1]) + except Exception: + pass + self.publish({"type": "tts", "state": "stop"}) + self._set_state("idle") + + # ----- external commands (from /voice/ws) ----------------------------- # + def command(self, cmd: str) -> None: + if cmd == "abort": + self._abort.set() + try: + import sounddevice as sd + sd.stop() + except Exception: + pass + if self.state in ("speaking", "listening"): + self._utter = [] + self._set_state("idle") + + +# --------------------------------------------------------------------------- # +# Real-engine wiring + capture thread +# --------------------------------------------------------------------------- # +class NativeAudioRunner: + """Owns the mic stream + worker thread and drives an AudioService with the real + openWakeWord / whisper.cpp / Kokoro engines. start()/stop() from the lifespan.""" + + def __init__(self, router: Any, hub: VoiceHub) -> None: + self.router = router + self.hub = hub + self._q: queue.Queue = queue.Queue(maxsize=64) + self._stream = None + self._thread = None + self._running = False + self.service: AudioService | None = None + + def start(self) -> bool: + if self._running or not available(): + return False + from .. import voice # noqa: F401 + from ... import settings as cfg + import numpy as np + import sounddevice as sd + + vcfg = (cfg.settings().get("voice") or {}) + wake = _build_wake(vcfg.get("wake_word_model", "hey_jarvis"), + float(vcfg.get("wake_word_sensitivity", 0.6))) + stt = _build_stt(vcfg.get("stt_model", "base.en")) + voice_id = str(vcfg.get("tts_voice", "kokoro:bm_george")).split(":")[-1] + speed = float(vcfg.get("speech_rate", 1.0)) + + from . import tts as tts_engine + + def synth_fn(text: str): + return tts_engine.synth_pcm(text, voice=voice_id, speed=speed) + + def play_fn(samples, sr): + sd.play(samples, sr); sd.wait() + + self.service = AudioService( + router=self.router, publish=self.hub.publish, + wake_fn=wake, stt_fn=stt, synth_fn=synth_fn, play_fn=play_fn, + vad_silence_ms=int(vcfg.get("vad_silence_ms", 700)), + ) + + def _cb(indata, frames, time_info, status): + try: + self._q.put_nowait((indata[:, 0] * 32768.0).astype(np.int16)) + except queue.Full: + pass + + try: + self._stream = sd.InputStream(samplerate=SAMPLE_RATE, channels=1, + blocksize=FRAME, dtype="float32", callback=_cb) + self._stream.start() + except Exception: + self._stream = None + return False + self._running = True + self._thread = threading.Thread(target=self._loop, daemon=True) + self._thread.start() + return True + + def _loop(self) -> None: + while self._running: + try: + frame = self._q.get(timeout=0.5) + except queue.Empty: + continue + if self.service: + self.service.feed(frame) + + def command(self, cmd: str) -> None: + if self.service: + self.service.command(cmd) + + def stop(self) -> None: + self._running = False + try: + if self._stream: + self._stream.stop(); self._stream.close() + except Exception: + pass + self._stream = None + + +def _build_wake(name: str, threshold: float) -> Callable[[Any], bool]: + from openwakeword.model import Model + path = wake_model_path(name) or wake_model_path("hey_jarvis") + model = Model(wakeword_models=[path], inference_framework="onnx") + key = None + + def wake_fn(frame) -> bool: + nonlocal key + scores = model.predict(frame) + if key is None: + key = next(iter(scores), None) + return key is not None and scores.get(key, 0.0) >= threshold + + return wake_fn + + +def _build_stt(model_name: str) -> Callable[[Any], str]: + from pywhispercpp.model import Model + model = Model(model_name, print_realtime=False, print_progress=False, print_timestamps=False) + + def stt_fn(samples) -> str: + segs = model.transcribe(samples) + text = " ".join(s.text for s in segs).strip() + # whisper marks non-speech as [BLANK_AUDIO], (music), [ Silence ], etc. — + # strip bracketed/parenthetical markers so silence isn't treated as a turn. + text = re.sub(r"[\[(][^\])]*[\])]", "", text) + return re.sub(r"\s{2,}", " ", text).strip() + + return stt_fn diff --git a/atlas/interface/voice/tts.py b/atlas/interface/voice/tts.py index d44d91d..ef3cb14 100644 --- a/atlas/interface/voice/tts.py +++ b/atlas/interface/voice/tts.py @@ -115,3 +115,22 @@ def synth(text: str, voice: str = "bm_george", speed: float = 1.0) -> bytes | No return buf.getvalue() except Exception: return None + + +def synth_pcm(text: str, voice: str = "bm_george", speed: float = 1.0): + """Return (float32 mono samples, sample_rate) for NATIVE playback by the audio + service, or None. Same engine as synth(), no WAV wrapping.""" + text = (text or "").strip() + if not text: + return None + eng = _engine() + if eng is None: + return None + lang = "en-gb" if voice[:1] == "b" else "en-us" + try: + import numpy as np + with _LOCK: + samples, sr = eng.create(text, voice=voice, speed=speed, lang=lang) + return np.asarray(samples, dtype=np.float32), int(sr) + except Exception: + return None diff --git a/atlas/interface/web/app.js b/atlas/interface/web/app.js index c019708..4cb2caa 100644 --- a/atlas/interface/web/app.js +++ b/atlas/interface/web/app.js @@ -278,6 +278,11 @@ $("#composer").addEventListener("submit", (e) => { }); /* ---------- voice: Web Speech API (STT + TTS) ---------- */ +/* Provider seam: "web" = browser Web Speech (below); "native" = the Python audio + service over /voice/ws (echo-cancelled wake/STT — Phase 2). Only ONE owns the + mic. Phase 1 stays "web"; the guards below make the switch clean when native + audio arrives. */ +let voiceMode = "web"; const SR = window.SpeechRecognition || window.webkitSpeechRecognition; let recog = null, listening = false, voiceTurn = false, followTimer = null; function initVoice() { @@ -295,6 +300,7 @@ function initVoice() { recog.onend = () => { if (listening) { listening = false; if (orbState === "listening") setState("idle"); } resumeWake(); }; } function startListening() { + if (voiceMode === "native") { NativeVoice.send({ type: "start" }); return; } if (!recog) { openChat(); return; } if (wakeRunning) { try { wakeSR.stop(); } catch {} wakeRunning = false; } // free the mic for command capture stopSpeaking(); @@ -352,6 +358,7 @@ function initBarge() { bargeSR.onerror = () => { bargeRunning = false; }; } function startBarge() { + if (voiceMode === "native") return; // native service handles barge-in with real AEC if (!bargeSR || !wakeEnabled || bargeRunning || orbState !== "speaking") return; stopFrameListen(); // free the mic for the barge listener while speaking try { bargeSR.start(); bargeRunning = true; } catch {} @@ -382,6 +389,7 @@ function initFrameListen() { frameSR.onerror = () => { frameListening = false; }; } function startFrameListen() { + if (voiceMode === "native") return; // native service owns the mic if (!frameSR || !wakeEnabled) return; if ($("#articleFrame").hidden || frameListening || orbState === "speaking") return; try { wakeSR && wakeSR.stop(); } catch {} wakeRunning = false; @@ -489,6 +497,10 @@ async function speakReply(text) { } } $("#orb").addEventListener("click", () => { + if (voiceMode === "native") { // native service owns capture/playback + NativeVoice.send({ type: orbState === "speaking" || orbState === "listening" ? "abort" : "start" }); + return; + } if (orbState === "speaking") { stopSpeaking(); setState("idle"); resumeWake(); return; } listening ? stopListening() : startListening(); }); @@ -527,6 +539,7 @@ function initWake() { }; } function startWake() { + if (voiceMode === "native") return; // native service listens for the wake word if (!wakeSR || wakeRunning || listening || orbState === "speaking" || orbState === "thinking") return; try { wakeSR.start(); wakeRunning = true; } catch {} } @@ -1285,6 +1298,60 @@ function sessionId() { return s; } +/* ---------- native voice channel (Phase 1 seam; audio pipeline lands Phase 2) ---------- */ +/* Connects to the additive /voice/ws. If the backend ever reports audio=true (the + native mic/wake/STT/AEC service is installed), the HUD hands mic ownership to it; + otherwise it stays on the browser Web Speech provider. */ +const NativeVoice = { + ws: null, ready: false, caps: null, retry: 0, + connect() { + try { + const ws = new WebSocket((API || location.origin).replace(/^http/, "ws") + "/voice/ws"); + this.ws = ws; + ws.onmessage = (ev) => { let m; try { m = JSON.parse(ev.data); } catch { return; } this.onMsg(m); }; + ws.onclose = () => { this.ready = false; if (voiceMode === "native") applyVoiceProvider(); this.reconnect(); }; + ws.onerror = () => {}; + } catch {} + }, + reconnect() { this.retry = Math.min(this.retry + 1, 6); setTimeout(() => this.connect(), 1500 * this.retry); }, + onMsg(m) { + if (m.type === "capabilities") { this.caps = m; return; } + if (m.type === "ready") { this.ready = true; this.retry = 0; applyVoiceProvider(); return; } + if (voiceMode !== "native") return; // ignore events unless native owns the mic + switch (m.type) { + case "state": { // drive the orb (map transcribing→thinking) + const s = m.value === "transcribing" ? "thinking" : m.value; + setState(s === "listening" || s === "thinking" || s === "speaking" ? s : "idle"); + break; + } + case "pong": break; + case "final": // what ATLAS heard + if (m.text) { addMsg("owner", m.text); setTranscript(`You: ${m.text}`); } + break; + case "reply": // ATLAS's answer (spoken natively by the service) + if (m.text) { addMsg("atlas", m.text); setTranscript(`ATLAS: ${m.text}`); } + if (m.media !== undefined) renderSearchMedia(m.media); + break; + // "wake" and "tts" states are reflected via the "state" events above. + } + }, + available() { return this.ready && !!this.caps && this.caps.audio === true; }, + send(o) { try { if (this.ws && this.ws.readyState === 1) this.ws.send(JSON.stringify(o)); } catch {} }, +}; +function applyVoiceProvider() { + const native = NativeVoice.available(); + const next = native ? "native" : "web"; + if (next === voiceMode) return; + voiceMode = next; + if (native) { // hand the mic to the native service + try { wakeSR && wakeSR.stop(); recog && recog.stop(); bargeSR && bargeSR.stop(); frameSR && frameSR.stop(); } catch {} + wakeRunning = bargeRunning = frameListening = false; + } else if (wakeEnabled) { // native dropped → resume Web Speech + resumeWake(); + } + console.log("[voice] provider:", voiceMode, this && this.caps ? "" : (NativeVoice.caps || "")); +} + /* ---------- live state via WebSocket (optional) ---------- */ function connectWS() { try { @@ -1319,6 +1386,7 @@ initWake(); initBarge(); initFrameListen(); initArticleFrame(); +NativeVoice.connect(); // Phase 1: connect to /voice/ws; stays on Web Speech until native audio is ready if (localStorage.getItem("atlas_wake") === "on") setWake(true); else updateWakeUI(); renderStatus(); renderMetrics(); renderMemory(); renderUpgrade(); renderScheduler(); renderCredits(); connectWS(); diff --git a/atlas/server/app.py b/atlas/server/app.py index 57b9b1a..345c717 100644 --- a/atlas/server/app.py +++ b/atlas/server/app.py @@ -52,16 +52,38 @@ def _scheduler_enabled() -> bool: return bool(cfg.settings().get("scheduler", {}).get("enabled")) and not os.environ.get("ATLAS_NO_SCHEDULER") +def _native_audio_enabled() -> bool: + # Opt-in (voice.native_audio) + deps/models present + not disabled for tests. + if os.environ.get("ATLAS_NO_AUDIO") or os.environ.get("ATLAS_FORCE_OFFLINE"): + return False + return bool((cfg.settings().get("voice") or {}).get("native_audio")) + + @asynccontextmanager async def lifespan(_app: FastAPI): if _scheduler_enabled(): scheduler.start() + # Native voice pipeline (Phase 2): mic → wake → STT → Router → Kokoro over /voice/ws. + global audio_runner + if _native_audio_enabled(): + from ..interface.voice.audio_service import NativeAudioRunner, available + if available(): + import asyncio + voice_hub.bind_loop(asyncio.get_running_loop()) + audio_runner = NativeAudioRunner(router, voice_hub) + started = audio_runner.start() + log_event("voice_native", {"started": started}) + if not started: + audio_runner = None log_event("startup", {"mode": cfg.settings()["model"].get("mode", "api"), "backend": router.backend, - "scheduler": _scheduler_enabled()}) + "scheduler": _scheduler_enabled(), + "native_audio": audio_runner is not None}) yield log_event("shutdown", {}) scheduler.stop() + if audio_runner is not None: + audio_runner.stop() app = FastAPI(title="ATLAS", version=__version__, lifespan=lifespan) @@ -72,6 +94,12 @@ async def lifespan(_app: FastAPI): registry = ConnectorRegistry() scheduler = Scheduler() +# Native voice (Phase 2): the hub bridges the audio thread to /voice/ws clients; +# audio_runner is set in lifespan when voice.native_audio is on and deps are present. +from ..interface.voice.audio_service import VoiceHub # noqa: E402 +voice_hub = VoiceHub() +audio_runner = None + class ChatIn(BaseModel): text: str @@ -408,6 +436,21 @@ def tts(body: TTSIn) -> Response: return Response(content=wav, media_type="audio/wav") +@app.get("/api/voice/native") +def voice_native() -> dict[str, Any]: + """Native-voice pipeline status for the HUD/settings: whether it's enabled, + whether the deps+models are installed, and whether it's currently running.""" + from ..interface.voice.audio_service import available, deps_present, wake_model_path + return { + "enabled": _native_audio_enabled(), + "deps_present": deps_present(), + "wake_model": bool(wake_model_path()), + "available": available(), + "running": audio_runner is not None, + "capabilities": _voice_capabilities(), + } + + @app.get("/api/read") def read_page(url: str) -> dict[str, Any]: """Extract an article's readable text so the HUD reader panel can show it @@ -609,6 +652,69 @@ async def ws(sock: WebSocket) -> None: return +def _voice_capabilities() -> dict[str, Any]: + """What the native audio pipeline can do right now. audio=True only when the + NativeAudioRunner actually started (mic + wake + STT live); then the HUD hands + it mic ownership. Otherwise the browser Web Speech fallback stays active.""" + on = audio_runner is not None + if on: + aec = ((cfg.settings().get("voice") or {}).get("aec_backend") not in (None, "none")) + return {"v": 1, "audio": True, "wake": True, "stt": True, "aec": bool(aec), + "tts_native": True, "reason": "native audio service running"} + from ..interface.voice.audio_service import available as _av + reason = ("native audio off (enable voice.native_audio)" + if not _native_audio_enabled() else + ("native audio deps/models not installed" if not _av() else "native audio not started")) + return {"v": 1, "audio": False, "wake": False, "stt": False, "aec": False, + "tts_native": False, "reason": reason} + + +@app.websocket("/voice/ws") +async def voice_ws(sock: WebSocket) -> None: + """Native-voice channel (additive; /ws is untouched). The HUD's + NativeVoiceProvider connects here, handshakes, and — when capabilities report + audio=True — takes over the mic. Broadcasts the audio service's events + (state/wake/final/reply/tts) and accepts start/stop/abort. Loopback-only.""" + await sock.accept() + caps = _voice_capabilities() + await sock.send_json({"type": "hello", "v": 1, "version": __version__}) + await sock.send_json({"type": "capabilities", **caps}) + await sock.send_json({"type": "ready", "audio": caps["audio"]}) + await sock.send_json({"type": "state", "value": "idle"}) + if not caps["audio"]: + # No native pipeline — stay connected for handshake/ping only (HUD uses Web Speech). + try: + while True: + msg = await sock.receive_json() + if msg.get("type") == "ping": + await sock.send_json({"type": "pong"}) + except WebSocketDisconnect: + return + return + + q = voice_hub.subscribe() + import asyncio + + async def _pump() -> None: + while True: + await sock.send_json(await q.get()) + + pump = asyncio.create_task(_pump()) + try: + while True: + msg = await sock.receive_json() + t = msg.get("type") + if t == "ping": + await sock.send_json({"type": "pong"}) + elif t in ("start", "stop", "abort") and audio_runner is not None: + audio_runner.command(t) + except WebSocketDisconnect: + return + finally: + pump.cancel() + voice_hub.unsubscribe(q) + + # --------------------------------------------------------------------------- # # Static HUD (mounted last so /api/* wins) # --------------------------------------------------------------------------- # diff --git a/atlas/tests/test_audio_service.py b/atlas/tests/test_audio_service.py new file mode 100644 index 0000000..c11b303 --- /dev/null +++ b/atlas/tests/test_audio_service.py @@ -0,0 +1,114 @@ +"""L3 native audio service: the full state machine (idle→listening→transcribing→ +thinking→speaking→idle) driven by injected fakes — no microphone or models.""" +from __future__ import annotations + +import numpy as np + +from atlas.interface.voice import audio_service as A + + +class _Router: + def __init__(self): + self.calls = [] + + def chat(self, text, **kw): + self.calls.append((text, kw)) + return {"reply": "Hello there. Nice to meet you.", "media": None} + + +def _svc(**over): + events = [] + played = [] + wake_on = {"v": False} + + defaults = dict( + router=_Router(), + publish=events.append, + wake_fn=lambda frame: wake_on["v"], + stt_fn=lambda samples: "what time is it", + synth_fn=lambda text: (np.zeros(4, dtype=np.float32), 16000), + play_fn=lambda samples, sr: played.append((len(samples), sr)), + vad_silence_ms=160, # 2 frames of silence ends the utterance + ) + defaults.update(over) + svc = A.AudioService(**defaults) + return svc, events, played, wake_on + + +def _silence(): + return np.zeros(A.FRAME, dtype=np.int16) + + +def _loud(): + return (np.ones(A.FRAME, dtype=np.int16) * 8000) + + +def test_speech_clean_strips_markup_and_urls(): + out = A.speech_clean("**Bold** see [link](https://x.com) and `code`") + assert "*" not in out and "https" not in out and "`" not in out + assert "Bold" in out and "link" in out + + +def test_split_sentences(): + assert A.split_sentences("One. Two! Three?") == ["One.", "Two!", "Three?"] + + +def test_wake_transitions_idle_to_listening(): + svc, events, _, wake = _svc() + svc.feed(_silence()) + assert svc.state == "idle" # no wake yet + wake["v"] = True + svc.feed(_loud()) + assert svc.state == "listening" + assert {"type": "wake"} in events + + +def test_full_turn_records_transcribes_responds_and_speaks(): + svc, events, played, wake = _svc() + wake["v"] = True + svc.feed(_loud()) # wake → listening + wake["v"] = False + svc.feed(_loud()) # speech frame + svc.feed(_silence()); svc.feed(_silence()) # 2 silent frames → end utterance + assert svc.state == "idle" # returned to idle after speaking + types = [e.get("type") for e in events] + assert "final" in types and "reply" in types + # tts start → stop bracket the speech, and Kokoro played at least one sentence + tts_states = [e["state"] for e in events if e.get("type") == "tts"] + assert tts_states[0] == "start" and tts_states[-1] == "stop" + assert played, "expected native playback of at least one sentence" + # states passed through transcribing → thinking → speaking + seen = [e["value"] for e in events if e.get("type") == "state"] + assert "transcribing" in seen and "thinking" in seen and "speaking" in seen + + +def test_empty_transcript_returns_to_idle_without_responding(): + router = _Router() + svc, events, _, wake = _svc(router=router, stt_fn=lambda s: " ") + wake["v"] = True + svc.feed(_loud()); wake["v"] = False + svc.feed(_silence()); svc.feed(_silence()) + assert svc.state == "idle" + assert router.calls == [] # nothing heard → no Router call + assert "reply" not in [e.get("type") for e in events] + + +def test_abort_stops_speaking(): + # A synth that aborts mid-way: after the first sentence, flip the abort flag. + svc, events, played, wake = _svc() + + def synth(text): + svc.command("abort") # user says stop during playback + return (np.zeros(2, dtype=np.float32), 16000) + + svc.synth_fn = synth + wake["v"] = True + svc.feed(_loud()); wake["v"] = False + svc.feed(_silence()); svc.feed(_silence()) + assert svc.state == "idle" + assert {"type": "tts", "state": "stop"} in events # always brackets to stop + + +def test_hub_publish_without_loop_is_safe(): + hub = A.VoiceHub() + hub.publish({"type": "state", "value": "idle"}) # no loop bound → no crash diff --git a/atlas/tests/test_server.py b/atlas/tests/test_server.py index f121d2b..609ad80 100644 --- a/atlas/tests/test_server.py +++ b/atlas/tests/test_server.py @@ -92,3 +92,48 @@ def test_scheduler_endpoint_lists_jobs(): def test_scheduler_run_unknown_job_404(): r = client.post("/api/scheduler/run/ghost") assert r.status_code == 404 + + +def test_voice_ws_handshake_reports_no_native_audio(): + # Phase 1 seam: /voice/ws exists and handshakes, but advertises audio=False so + # the HUD keeps its browser Web Speech provider until the native service lands. + with client.websocket_connect("/voice/ws") as ws: + assert ws.receive_json()["type"] == "hello" + caps = ws.receive_json() + assert caps["type"] == "capabilities" and caps["audio"] is False + assert ws.receive_json() == {"type": "ready", "audio": False} + assert ws.receive_json() == {"type": "state", "value": "idle"} + ws.send_json({"type": "ping"}) + assert ws.receive_json() == {"type": "pong"} + + +def test_voice_ws_native_path_forwards_commands(monkeypatch): + # When the native runner is active, /voice/ws reports audio=True and routes + # start/stop/abort to it (verified with a fake runner — no mic needed). + from atlas.server import app as appmod + + class FakeRunner: + def __init__(self): + self.cmds = [] + + def command(self, c): + self.cmds.append(c) + + fake = FakeRunner() + monkeypatch.setattr(appmod, "audio_runner", fake) + with client.websocket_connect("/voice/ws") as ws: + assert ws.receive_json()["type"] == "hello" + assert ws.receive_json()["audio"] is True # capabilities + assert ws.receive_json() == {"type": "ready", "audio": True} + assert ws.receive_json() == {"type": "state", "value": "idle"} + ws.send_json({"type": "abort"}) + ws.send_json({"type": "ping"}) + assert ws.receive_json() == {"type": "pong"} + assert "abort" in fake.cmds + + +def test_voice_native_status_endpoint(): + r = client.get("/api/voice/native") + assert r.status_code == 200 + b = r.json() + assert "available" in b and "running" in b and "capabilities" in b diff --git a/requirements.txt b/requirements.txt index 95f1d3f..ebfe086 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,10 @@ pytest>=8.0 # pip install kokoro-onnx soundfile "misaki[en]" # then download kokoro-v1.0.onnx + voices-v1.0.bin into # atlas/interface/voice/models/ (github.com/thewh1teagle/kokoro-onnx releases). -# STT / wake word (real hardware): browser Web Speech today; openwakeword, -# silero-vad, whisper.cpp later. These need a mic, not present in CI. +# Native voice pipeline (L3, opt-in via voice.native_audio) — replaces the browser +# Web Speech loop with an on-device, offline pipeline: mic → wake → STT → Kokoro. +# Optional & heavy (like Kokoro); ATLAS falls back to browser Web Speech without +# them. macOS needs PortAudio (`brew install portaudio`) for sounddevice. +# pip install sounddevice numpy openwakeword onnxruntime pywhispercpp +# Models auto-download on first run: openWakeWord (hey_jarvis + Silero VAD) and +# whisper.cpp (base.en). Not installed in CI (no mic); tests inject fakes. From 9bb2ec4812127d5ef4f8db4c160a948431f9ac61 Mon Sep 17 00:00:00 2001 From: nirhemo Date: Tue, 14 Jul 2026 13:45:50 +0300 Subject: [PATCH 02/13] =?UTF-8?q?feat(voice):=20Phase=203=20=E2=80=94=20fu?= =?UTF-8?q?ll-duplex=20AEC=20+=20real=20barge-in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - atlas/interface/voice/aec.py: EchoCanceller (constrained overlap-save FDAF, pure numpy) removes the far-end (Kokoro) signal from the mic — measured ~9 dB echo reduction on synthetic room echo. make_aec() picks a backend: "os" (macOS VoiceProcessingIO — best; needs a native audio-unit bridge, falls back), "webrtc" (needs C++ build, falls back), "nlms" (the FDAF), "none" (passthrough). - audio_service.py: full-duplex mode. The LLM+TTS turn now runs OFF the mic thread (async_turn) so the mic loop stays live while ATLAS speaks; during "speaking", _maybe_barge() AEC-cleans each frame and runs wake on the residual → "Hey Jarvis" interrupts mid-sentence (abort + queue-to-listen) without self-triggering. Kokoro PCM is queued as the AEC reference; playback is interruptible. - Best-effort reference timing (needs on-device delay calibration; the OS backend avoids it entirely) — flagged in code for the owner's on-device tuning. 60 tests pass offline (FDAF echo reduction, barge-in decision, backend fallbacks; turn state machine made deterministic via async_turn=False). Co-Authored-By: Claude Opus 4.8 (1M context) --- atlas/interface/voice/aec.py | 110 +++++++++++++++++++++++ atlas/interface/voice/audio_service.py | 115 +++++++++++++++++++++---- atlas/tests/test_aec.py | 81 +++++++++++++++++ atlas/tests/test_audio_service.py | 1 + 4 files changed, 290 insertions(+), 17 deletions(-) create mode 100644 atlas/interface/voice/aec.py create mode 100644 atlas/tests/test_aec.py diff --git a/atlas/interface/voice/aec.py b/atlas/interface/voice/aec.py new file mode 100644 index 0000000..029854f --- /dev/null +++ b/atlas/interface/voice/aec.py @@ -0,0 +1,110 @@ +"""Acoustic echo cancellation (L3, Phase 3) — remove the far-end (Kokoro TTS) +signal from the mic so ATLAS can listen WHILE it speaks without hearing itself. + +Backends (voice.aec_backend): + "os" — macOS VoiceProcessingIO (best; the OS owns mic+speaker and cancels + echo for free). Needs a native audio-unit bridge; when unavailable we + fall back to the software filter and say so. + "webrtc"— WebRTC AEC3 via webrtc-audio-processing (needs a C++ build). + "nlms" — a pure-numpy frequency-domain adaptive filter (FDAF). No build deps, + deterministic, unit-testable. Handles linear echo; not as strong as + the OS/WebRTC cancellers on nonlinear echo, but always available. + "none" — passthrough (Phase 2 half-duplex). + +The reference (far-end) frame is the exact Kokoro PCM being played, handed in +time-aligned by the audio service — that's what makes real cancellation possible. +""" +from __future__ import annotations + +import numpy as np + +FRAME = 1280 # 80 ms @ 16 kHz + + +class EchoCanceller: + """Constrained frequency-domain adaptive filter (overlap-save FDAF), single + partition of `block` samples. process(mic, ref) → near-end (echo removed).""" + + def __init__(self, block: int = FRAME, mu: float = 0.3, eps: float = 1e-3) -> None: + self.N = block + self.mu = float(mu) + self.eps = float(eps) + # Constrained overlap-save FDAF, single partition. Full complex FFT of size + # 2N; the filter W and smoothed power P live in that 2N frequency domain. + self._W = np.zeros(2 * block, dtype=np.complex128) + self._x_prev = np.zeros(block, dtype=np.float64) # previous ref block + self._P = np.ones(2 * block, dtype=np.float64) # smoothed ref power + + def process(self, mic_i16, ref_i16): + N = self.N + d = np.asarray(mic_i16, dtype=np.float64)[:N] / 32768.0 + x_cur = np.asarray(ref_i16, dtype=np.float64)[:N] / 32768.0 + if d.size < N: + d = np.pad(d, (0, N - d.size)) + if x_cur.size < N: + x_cur = np.pad(x_cur, (0, N - x_cur.size)) + + x = np.concatenate([self._x_prev, x_cur]) # 2N overlap-save block + X = np.fft.fft(x) # 2N complex + y = np.real(np.fft.ifft(self._W * X))[N:] # linear-conv echo estimate + e = d - y # near-end estimate + + E = np.fft.fft(np.concatenate([np.zeros(N), e])) # 2N + self._P = 0.9 * self._P + 0.1 * (np.abs(X) ** 2) + G = np.conj(X) * E / (self._P + self.eps) # per-bin normalized grad + g = np.real(np.fft.ifft(G)) + g[N:] = 0 # gradient constraint (first N taps) + self._W += self.mu * np.fft.fft(g) + + self._x_prev = x_cur + return np.clip(e * 32768.0, -32768, 32767).astype(np.int16) + + +class _Passthrough: + backend = "none" + + def process(self, mic_i16, ref_i16): + return np.asarray(mic_i16, dtype=np.int16) + + +class _Software: + backend = "nlms" + + def __init__(self): + self._ec = EchoCanceller() + + def process(self, mic_i16, ref_i16): + return self._ec.process(mic_i16, ref_i16) + + +def _os_backend(): + """macOS VoiceProcessingIO. Requires a native audio-unit bridge (PyObjC/ctypes + or a small Rust helper) that we don't build here yet — returns None so the + caller falls back to the software filter. The hook is intentional: wiring the + VPIO unit is the on-device upgrade for best-in-class AEC.""" + return None + + +def make_aec(backend: str = "os"): + """Build an echo canceller for the requested backend, falling back gracefully. + Returns an object with .process(mic_i16, ref_i16) and a .backend label.""" + backend = (backend or "os").lower() + if backend in ("none", "off"): + return _Passthrough() + if backend == "os": + vpio = _os_backend() + if vpio is not None: + return vpio + sw = _Software() # VPIO bridge not built → software fallback + sw.backend = "nlms(os-unavailable)" + return sw + if backend == "webrtc": + try: + import webrtc_audio_processing # noqa: F401 + # (wiring left for the C++-built environment; software fallback here) + except Exception: + pass + sw = _Software() + sw.backend = "nlms(webrtc-unavailable)" + return sw + return _Software() diff --git a/atlas/interface/voice/audio_service.py b/atlas/interface/voice/audio_service.py index 3b9ad14..6e62ff7 100644 --- a/atlas/interface/voice/audio_service.py +++ b/atlas/interface/voice/audio_service.py @@ -23,6 +23,15 @@ _MAX_UTTERANCE_S = 15 # hard cap so a stuck VAD can't record forever +def _stop_playback() -> None: + """Interrupt any in-progress native playback (barge-in / user Stop).""" + try: + import sounddevice as sd + sd.stop() + except Exception: + pass + + # --------------------------------------------------------------------------- # # Dependency / model availability # --------------------------------------------------------------------------- # @@ -128,6 +137,10 @@ def __init__( play_fn: Callable[[Any, int], None], # (samples, sr) -> plays, honors abort vad_silence_ms: int = 700, session: str = "s_voice", + aec: Any = None, # EchoCanceller (Phase 3) or None + ref_provider: Callable[[], Any] = None, # -> current far-end (TTS) frame, or None + full_duplex: bool = False, # listen while speaking (needs aec + ref) + async_turn: bool = True, # run the LLM+TTS turn off the mic thread ) -> None: self.router = router self.publish = publish @@ -137,11 +150,16 @@ def __init__( self.play_fn = play_fn self.vad_silence_ms = vad_silence_ms self.session = session + self.aec = aec + self.ref_provider = ref_provider + self.full_duplex = bool(full_duplex and aec is not None) + self.async_turn = async_turn self.state = "idle" self._utter: list = [] self._silence_frames = 0 self._abort = threading.Event() + self._barge = False self._turn = 0 # ----- helpers -------------------------------------------------------- # @@ -160,8 +178,13 @@ def _silence_limit_frames(self) -> int: # ----- per-frame state machine (the testable core) -------------------- # def feed(self, frame) -> None: - """Process one 80 ms int16 frame. No-op while speaking (half-duplex).""" - if self.state in ("speaking", "transcribing", "thinking"): + """Process one 80 ms int16 frame. During 'speaking', half-duplex ignores + the mic; full-duplex runs AEC + wake on the cleaned signal for barge-in.""" + if self.state == "speaking": + if self.full_duplex: + self._maybe_barge(frame) + return + if self.state in ("transcribing", "thinking"): return if self.state == "idle": try: @@ -181,28 +204,50 @@ def feed(self, frame) -> None: self._silence_frames = 0 too_long = len(self._utter) > int(_MAX_UTTERANCE_S * SAMPLE_RATE / FRAME) if self._silence_frames >= self._silence_limit_frames or too_long: - self._finish_utterance() + self._start_turn() + + def _maybe_barge(self, frame) -> bool: + """AEC-clean the mic against the far-end (TTS) frame, then run wake on the + residual — so 'Hey Jarvis' interrupts mid-sentence without self-triggering. + Sets the abort + barge flags; the turn thread then switches to listening.""" + try: + ref = self.ref_provider() if self.ref_provider else None + cleaned = self.aec.process(frame, ref) if (self.aec and ref is not None) else frame + if self.wake_fn(cleaned): + self._barge = True + self._abort.set() + _stop_playback() + self.publish({"type": "wake"}) + return True + except Exception: + pass + return False - def _finish_utterance(self) -> None: + def _start_turn(self) -> None: import numpy as np self._set_state("transcribing") frames = self._utter self._utter = [] + audio = (np.concatenate([np.asarray(f, dtype=np.int16) for f in frames]) + if frames else np.zeros(0, dtype=np.int16)) + if self.async_turn: + threading.Thread(target=self._run_turn, args=(audio,), daemon=True).start() + else: + self._run_turn(audio) + + def _run_turn(self, audio) -> None: + """STT → Router → speak. Runs off the mic thread (async) so the mic loop + stays live for barge-in while ATLAS thinks and speaks.""" try: - audio = np.concatenate([np.asarray(f, dtype=np.int16) for f in frames]) \ - if frames else np.zeros(0, dtype=np.int16) - samples = audio.astype(np.float32) / 32768.0 - text = (self.stt_fn(samples) or "").strip() + text = (self.stt_fn(audio.astype("float32") / 32768.0) or "").strip() except Exception: text = "" if not text: self._set_state("idle") return self.publish({"type": "final", "text": text}) - self._respond(text) - - def _respond(self, text: str) -> None: self._abort.clear() + self._barge = False self._turn += 1 self._set_state("thinking") try: @@ -231,20 +276,28 @@ def _speak(self, reply: str) -> None: except Exception: pass self.publish({"type": "tts", "state": "stop"}) - self._set_state("idle") + # barge-in → go straight to listening for the interrupting command; else idle. + if self._barge: + self._barge = False + self._utter = [] + self._silence_frames = 0 + self._set_state("listening") + else: + self._set_state("idle") # ----- external commands (from /voice/ws) ----------------------------- # def command(self, cmd: str) -> None: if cmd == "abort": + self._barge = False # explicit stop → go quiet, not into listening self._abort.set() - try: - import sounddevice as sd - sd.stop() - except Exception: - pass + _stop_playback() if self.state in ("speaking", "listening"): self._utter = [] self._set_state("idle") + elif cmd == "start" and self.state == "idle": + self._utter = [] + self._silence_frames = 0 + self._set_state("listening") # --------------------------------------------------------------------------- # @@ -279,17 +332,45 @@ def start(self) -> bool: speed = float(vcfg.get("speech_rate", 1.0)) from . import tts as tts_engine + from collections import deque + from .aec import make_aec + + # Full-duplex AEC (Phase 3): the far-end (Kokoro) frames are queued into a + # reference buffer as they play; the mic loop pops them so the canceller + # sees what's coming out of the speaker. Timing here is best-effort and + # wants on-device delay calibration; the macOS VoiceProcessingIO backend + # (aec_backend="os", native bridge) avoids this plumbing entirely. + aec_backend = str(vcfg.get("aec_backend", "os")) + aec = make_aec(aec_backend) if aec_backend not in ("none", "off") else None + ref_q: deque = deque(maxlen=64) + + def ref_provider(): + return ref_q.popleft() if ref_q else None + + def _resample_16k(samples, sr): + if sr == SAMPLE_RATE: + return samples + import numpy as np + n = int(len(samples) * SAMPLE_RATE / sr) + return np.interp(np.linspace(0, len(samples), n, endpoint=False), + np.arange(len(samples)), samples).astype(np.float32) def synth_fn(text: str): return tts_engine.synth_pcm(text, voice=voice_id, speed=speed) def play_fn(samples, sr): + # Queue the far-end frames for the AEC reference, then play. + r = _resample_16k(np.asarray(samples, dtype=np.float32), sr) + pcm16 = np.clip(r * 32768.0, -32768, 32767).astype(np.int16) + for i in range(0, len(pcm16), FRAME): + ref_q.append(pcm16[i:i + FRAME]) sd.play(samples, sr); sd.wait() self.service = AudioService( router=self.router, publish=self.hub.publish, wake_fn=wake, stt_fn=stt, synth_fn=synth_fn, play_fn=play_fn, vad_silence_ms=int(vcfg.get("vad_silence_ms", 700)), + aec=aec, ref_provider=ref_provider, full_duplex=(aec is not None), ) def _cb(indata, frames, time_info, status): diff --git a/atlas/tests/test_aec.py b/atlas/tests/test_aec.py new file mode 100644 index 0000000..dd8608d --- /dev/null +++ b/atlas/tests/test_aec.py @@ -0,0 +1,81 @@ +"""L3 Phase 3: echo cancellation (FDAF) reduces far-end echo, and the audio +service's full-duplex barge-in decision fires on a wake during speech.""" +from __future__ import annotations + +import numpy as np + +from atlas.interface.voice import aec as AEC +from atlas.interface.voice import audio_service as A + + +def _i16(x): + return np.clip(x * 32768.0, -32768, 32767).astype(np.int16) + + +def test_fdaf_reduces_echo_energy(): + rng = np.random.RandomState(0) + ec = AEC.EchoCanceller(block=A.FRAME, mu=0.3) + room = np.array([0.0, 0.6, 0.0, -0.3, 0.15], dtype=np.float64) # short echo path + tail = np.zeros(len(room) - 1) + + def step(measure): + nonlocal tail, mic_energy, out_energy + ref = rng.randn(A.FRAME) * 0.3 # far-end (TTS) signal + full = np.concatenate([tail, ref]) + echo = np.convolve(full, room, mode="valid") # echo the mic picks up + tail = ref[-(len(room) - 1):] + out = ec.process(_i16(echo), _i16(ref)).astype(np.float64) / 32768.0 + if measure: + mic_energy += float(np.sum(echo ** 2)) + out_energy += float(np.sum(out ** 2)) + + mic_energy = out_energy = 0.0 + for _ in range(80): # warm up the adaptive filter + step(measure=False) + for _ in range(40): # measure steady-state residual + step(measure=True) + # once adapted, residual echo should be << the raw mic echo (~>6 dB reduction) + assert out_energy < 0.25 * mic_energy, (out_energy, mic_energy) + + +def test_make_aec_backends_fall_back_gracefully(): + assert AEC.make_aec("none").process(_i16(np.zeros(A.FRAME)), _i16(np.zeros(A.FRAME))) is not None + # os / webrtc have no native build here → software fallback, still functional + assert "nlms" in AEC.make_aec("os").backend + assert "nlms" in AEC.make_aec("webrtc").backend + + +def test_full_duplex_barge_in_fires_on_wake_during_speech(): + events = [] + wake = {"v": False} + + class _R: + def chat(self, text, **kw): + return {"reply": "ok", "media": None} + + svc = A.AudioService( + router=_R(), publish=events.append, + wake_fn=lambda f: wake["v"], stt_fn=lambda s: "hi", + synth_fn=lambda t: (np.zeros(2, dtype=np.float32), 16000), + play_fn=lambda s, sr: None, + aec=AEC.make_aec("none"), ref_provider=lambda: np.zeros(A.FRAME, dtype=np.int16), + full_duplex=True, + ) + svc.state = "speaking" # pretend ATLAS is mid-sentence + wake["v"] = True + barged = svc._maybe_barge(np.zeros(A.FRAME, dtype=np.int16)) + assert barged is True + assert svc._abort.is_set() and svc._barge is True # abort + queued to listen + assert {"type": "wake"} in events + + +def test_full_duplex_no_barge_without_wake(): + svc = A.AudioService( + router=object(), publish=lambda e: None, + wake_fn=lambda f: False, stt_fn=lambda s: "", + synth_fn=lambda t: None, play_fn=lambda s, sr: None, + aec=AEC.make_aec("none"), ref_provider=lambda: None, full_duplex=True, + ) + svc.state = "speaking" + assert svc._maybe_barge(np.zeros(A.FRAME, dtype=np.int16)) is False + assert not svc._abort.is_set() diff --git a/atlas/tests/test_audio_service.py b/atlas/tests/test_audio_service.py index c11b303..c3321fc 100644 --- a/atlas/tests/test_audio_service.py +++ b/atlas/tests/test_audio_service.py @@ -29,6 +29,7 @@ def _svc(**over): synth_fn=lambda text: (np.zeros(4, dtype=np.float32), 16000), play_fn=lambda samples, sr: played.append((len(samples), sr)), vad_silence_ms=160, # 2 frames of silence ends the utterance + async_turn=False, # run the turn inline so tests are deterministic ) defaults.update(over) svc = A.AudioService(**defaults) From 1e4f6e8c3b578f4625e363a1651f3693d9cc3f33 Mon Sep 17 00:00:00 2001 From: nirhemo Date: Tue, 14 Jul 2026 13:55:37 +0300 Subject: [PATCH 03/13] =?UTF-8?q?feat(app):=20Phase=200=20=E2=80=94=20Taur?= =?UTF-8?q?i=20native=20shell=20(sidecar=20+=20tray)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the existing HUD in a native desktop app (macOS now, Windows-ready): - Sidecar contract: `python -m atlas.server --port 0` picks a free port, binds 127.0.0.1, and prints "ATLAS_READY " once serving (server/__main__.py). - deploy/atlas-core.spec + atlas_core_entry.py: PyInstaller freeze of the FastAPI core into a standalone binary (audio ML libs excluded — optional local install). VERIFIED: the frozen binary serves the API + HUD with no Python installed. - src-tauri/: Tauri 2 shell — spawns the sidecar, reads ATLAS_READY, loads the HUD webview; single-instance, tray show/hide + Quit, autostart, hide-on-close, clean sidecar kill on exit. Entitlements (mic + hardened runtime) for notarization. - deploy/build-app.sh: freeze → place sidecar with target-triple → `cargo tauri build`. - VERSION.json bumped to 0.6.0 (packaged-app fallback when there's no .git). The Rust compile + GUI launch + icon generation are the owner's on-device build step (`./deploy/build-app.sh`); the sidecar packaging is verified here. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 6 ++ atlas/VERSION.json | 10 +-- atlas/server/__main__.py | 52 ++++++++++++-- deploy/atlas-core.spec | 50 +++++++++++++ deploy/atlas_core_entry.py | 7 ++ deploy/build-app.sh | 32 +++++++++ src-tauri/.gitignore | 5 ++ src-tauri/Cargo.toml | 29 ++++++++ src-tauri/Entitlements.plist | 22 ++++++ src-tauri/build.rs | 3 + src-tauri/capabilities/default.json | 19 +++++ src-tauri/src/main.rs | 106 ++++++++++++++++++++++++++++ src-tauri/tauri.conf.json | 35 +++++++++ 13 files changed, 366 insertions(+), 10 deletions(-) create mode 100644 deploy/atlas-core.spec create mode 100644 deploy/atlas_core_entry.py create mode 100755 deploy/build-app.sh create mode 100644 src-tauri/.gitignore create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/Entitlements.plist create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/capabilities/default.json create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/tauri.conf.json diff --git a/.gitignore b/.gitignore index 14cf090..0658c5d 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,9 @@ atlas/memory/vault/people/*.md # L8 Scheduler runtime state (last_run / next_run per job) atlas/scheduler/state.json + +# Tauri app build artifacts (PyInstaller freeze + Tauri bundle) +/build/ +/dist/ +src-tauri/target/ +src-tauri/binaries/ diff --git a/atlas/VERSION.json b/atlas/VERSION.json index d77e63c..62b0190 100644 --- a/atlas/VERSION.json +++ b/atlas/VERSION.json @@ -1,6 +1,6 @@ { - "version": "0.2.0", - "codename": "ATLAS — Automated Task & Logic Assistant System", + "version": "0.6.0", + "codename": "ATLAS \u2014 Automated Task & Logic Assistant System", "cycle": 1, "phase": 1, "layers": { @@ -14,7 +14,7 @@ "L8_scheduler": "1.0.0" }, "model_backend": "claude-sonnet-4-6", - "model_backend_notes": "Phase 1 — Claude API is the brain. Sonnet for routine turns; escalates to Opus for heavy reasoning per atlas/orchestration/config.json. Local wake word, STT, TTS, memory, and UI run on-device.", + "model_backend_notes": "Phase 1 \u2014 Claude API is the brain. Sonnet for routine turns; escalates to Opus for heavy reasoning per atlas/orchestration/config.json. Local wake word, STT, TTS, memory, and UI run on-device.", "last_upgrade": "2026-06-13T08:55:00Z", "next_scheduled_cycle": "2026-06-14T03:00:00Z", "rollback_point": "git tag v0.1.0 (commit aed71b6)", @@ -22,7 +22,7 @@ "status": "L8_scheduler_added", "owner_approval_pending": [ "L1 identity personality sign-off", - "Identity canaries — replace placeholder answers with real Owner facts", - "L7 first 3 connectors — confirm calendar/email/web and authorize install" + "Identity canaries \u2014 replace placeholder answers with real Owner facts", + "L7 first 3 connectors \u2014 confirm calendar/email/web and authorize install" ] } diff --git a/atlas/server/__main__.py b/atlas/server/__main__.py index c0cb4ae..9d4597b 100644 --- a/atlas/server/__main__.py +++ b/atlas/server/__main__.py @@ -1,20 +1,62 @@ -"""Entry point: `python -m atlas.server`.""" +"""Entry point: `python -m atlas.server` (and the frozen Tauri sidecar). + +Sidecar contract (Phase 0): accepts `--port N` (or `--port 0`/none → pick a free +port), binds 127.0.0.1 only, and prints `ATLAS_READY ` on stdout once uvicorn +is serving. The Tauri shell reads that line, then points its webview at the port. +""" from __future__ import annotations +import argparse import os +import socket +import threading import uvicorn from .. import settings as cfg +def _free_port() -> int: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(("127.0.0.1", 0)) + port = s.getsockname()[1] + s.close() + return port + + +def _announce_when_ready(server: "uvicorn.Server", port: int) -> None: + """Print the readiness line once the server is actually accepting requests, so + a supervisor (Tauri) can gate the webview on it instead of racing startup.""" + import time + for _ in range(600): # up to ~60s + if getattr(server, "started", False): + print(f"ATLAS_READY {port}", flush=True) + return + time.sleep(0.1) + + def main() -> None: - port = int(cfg.settings().get("ui", {}).get("port", 8765)) - # Cheap mode check — the actual Router is built once inside the app module. + ap = argparse.ArgumentParser(prog="atlas.server", add_help=True) + ap.add_argument("--port", type=int, default=None, + help="port to bind (0 or omitted → a free port, printed on stdout)") + ap.add_argument("--host", default="127.0.0.1") + args, _ = ap.parse_known_args() + + if args.port is None: # omitted → settings port (or a free one) + port = int(cfg.settings().get("ui", {}).get("port", 8765) or 0) or _free_port() + elif args.port == 0: # explicit 0 → pick a free port + port = _free_port() + else: + port = args.port + key_env = cfg.settings()["model"].get("api_key_env", "ANTHROPIC_API_KEY") mode = "claude" if os.environ.get(key_env) else "offline/degraded" - print(f"ATLAS core -> http://localhost:{port} (mode: {mode})") - uvicorn.run("atlas.server.app:app", host="127.0.0.1", port=port, reload=False) + print(f"ATLAS core -> http://{args.host}:{port} (mode: {mode})", flush=True) + + config = uvicorn.Config("atlas.server.app:app", host=args.host, port=port, reload=False) + server = uvicorn.Server(config) + threading.Thread(target=_announce_when_ready, args=(server, port), daemon=True).start() + server.run() if __name__ == "__main__": diff --git a/deploy/atlas-core.spec b/deploy/atlas-core.spec new file mode 100644 index 0000000..3554d6a --- /dev/null +++ b/deploy/atlas-core.spec @@ -0,0 +1,50 @@ +# PyInstaller spec — freeze the ATLAS FastAPI core into a single binary that the +# Tauri shell runs as a sidecar (so end users need no Python). Build: +# pyinstaller --clean -y deploy/atlas-core.spec +# The heavy optional native-audio deps (onnxruntime/whisper/sounddevice) are NOT +# bundled here — the shell ships with chat + browser voice; native audio is an +# opt-in local install, mirroring how Kokoro is optional. +import os +from PyInstaller.utils.hooks import collect_submodules, collect_data_files + +ROOT = os.path.dirname(SPECPATH) # repo root (spec lives in deploy/) + + +def _p(rel): + return os.path.join(ROOT, rel) + + +datas = [] +# Bundled, non-gitignored assets the server serves / reads on a fresh run: +datas += [(_p("atlas/interface/web"), "atlas/interface/web")] +datas += [(_p("atlas/config/settings.example.json"), "atlas/config")] +datas += [(_p("atlas/connectors/registry.example.json"), "atlas/connectors")] +datas += [(_p("atlas/VERSION.json"), "atlas")] +datas += [(_p("atlas/core"), "atlas/core")] +datas += [(_p("atlas/memory/vault"), "atlas/memory/vault")] +datas += [(_p("atlas/orchestration/config.json"), "atlas/orchestration")] +datas += collect_data_files("anthropic") + +hiddenimports = [] +hiddenimports += collect_submodules("uvicorn") +hiddenimports += collect_submodules("anthropic") +hiddenimports += ["atlas.server.app", "atlas.server.__main__"] + +a = Analysis( + [_p("deploy/atlas_core_entry.py")], # absolute-import launcher (keeps package context) + pathex=[ROOT], + binaries=[], + datas=datas, + hiddenimports=hiddenimports, + excludes=["tkinter", "matplotlib", "onnxruntime", "pywhispercpp", "openwakeword"], + noarchive=False, +) +pyz = PYZ(a.pure) +exe = EXE( + pyz, a.scripts, a.binaries, a.datas, [], + name="atlas-core", + console=True, # stdout carries the ATLAS_READY line the shell reads + strip=False, + upx=False, + target_arch=None, +) diff --git a/deploy/atlas_core_entry.py b/deploy/atlas_core_entry.py new file mode 100644 index 0000000..7b865f9 --- /dev/null +++ b/deploy/atlas_core_entry.py @@ -0,0 +1,7 @@ +"""PyInstaller entry point for the frozen sidecar. Uses an ABSOLUTE import so the +`atlas` package context is intact (running atlas/server/__main__.py directly would +break its `from .. import ...` relative imports).""" +from atlas.server.__main__ import main + +if __name__ == "__main__": + main() diff --git a/deploy/build-app.sh b/deploy/build-app.sh new file mode 100755 index 0000000..8168f26 --- /dev/null +++ b/deploy/build-app.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Build the native ATLAS desktop app (Tauri shell + frozen FastAPI sidecar). +# +# ./deploy/build-app.sh +# +# Produces src-tauri/target/release/bundle/ (.app + .dmg on macOS). +# Prereqs (installed once): Rust (rustup), Tauri CLI (`cargo install tauri-cli --version ^2`), +# PyInstaller (`pip install pyinstaller`). Signing/notarization is CI's job (see +# .github/workflows/build-app.yml) and needs the owner's Apple Developer cert. +set -euo pipefail +cd "$(dirname "$0")/.." + +if [ ! -f src-tauri/icons/icon.icns ]; then + echo "✗ App icons missing. Generate them once from a square PNG logo:" + echo " ( cd src-tauri && cargo tauri icon ../path/to/atlas-logo.png )" + exit 1 +fi + +echo "==> 1/3 Freezing the FastAPI core with PyInstaller" +pyinstaller --clean -y deploy/atlas-core.spec + +echo "==> 2/3 Placing the sidecar with its target-triple suffix" +TRIPLE="$(rustc -vV | awk -F': ' '/host/{print $2}')" +mkdir -p src-tauri/binaries +cp "dist/atlas-core" "src-tauri/binaries/atlas-core-${TRIPLE}" +chmod +x "src-tauri/binaries/atlas-core-${TRIPLE}" +echo " → src-tauri/binaries/atlas-core-${TRIPLE}" + +echo "==> 3/3 Building the Tauri app" +( cd src-tauri && cargo tauri build ) + +echo "✓ Done. Bundle in src-tauri/target/release/bundle/" diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 0000000..83fe972 --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,5 @@ +# Tauri / Rust build artifacts and the frozen sidecar (all rebuilt by build-app.sh) +/target +/gen +/binaries +Cargo.lock diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..f5cf3cf --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "atlas-app" +version = "0.6.0" +description = "ATLAS — voice-first local assistant (native shell)" +edition = "2021" +rust-version = "1.77" + +[lib] +name = "atlas_app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-shell = "2" +tauri-plugin-single-instance = "2" +tauri-plugin-autostart = "2" +tauri-plugin-positioner = { version = "2", features = ["tray-icon"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +panic = "abort" +strip = true diff --git a/src-tauri/Entitlements.plist b/src-tauri/Entitlements.plist new file mode 100644 index 0000000..255615f --- /dev/null +++ b/src-tauri/Entitlements.plist @@ -0,0 +1,22 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.device.audio-input + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..771913a --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,19 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "ATLAS shell: spawn the atlas-core sidecar, tray, autostart, positioner.", + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:allow-show", + "core:window:allow-hide", + "core:window:allow-set-focus", + "shell:allow-execute", + { + "identifier": "shell:allow-spawn", + "allow": [{ "name": "binaries/atlas-core", "sidecar": true, "args": true }] + }, + "positioner:default", + "autostart:default" + ] +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..3365726 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,106 @@ +// ATLAS native shell (Tauri 2). Thin host: runs the existing FastAPI core as a +// sidecar, waits for "ATLAS_READY ", then loads the current HUD in a webview. +// Single-instance, tray show/hide, autostart, and a clean sidecar shutdown on exit. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::sync::Mutex; +use tauri::menu::{Menu, MenuItem}; +use tauri::tray::TrayIconBuilder; +use tauri::{Manager, RunEvent, WebviewUrl, WebviewWindowBuilder, WindowEvent}; +use tauri_plugin_shell::process::{CommandChild, CommandEvent}; +use tauri_plugin_shell::ShellExt; + +#[derive(Default)] +struct SidecarState(Mutex>); + +fn show_main(app: &tauri::AppHandle) { + if let Some(w) = app.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } +} + +fn main() { + tauri::Builder::default() + // single-instance MUST be registered first + .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + show_main(app); + })) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_positioner::init()) + .plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + None, + )) + .manage(SidecarState::default()) + .setup(|app| { + let handle = app.handle().clone(); + + // Spawn the FastAPI core; it prints "ATLAS_READY " once serving. + let sidecar = app + .shell() + .sidecar("atlas-core") + .expect("atlas-core sidecar missing") + .args(["--port", "0"]); + let (mut rx, child) = sidecar.spawn().expect("failed to start atlas-core"); + app.state::().0.lock().unwrap().replace(child); + + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + if let CommandEvent::Stdout(line) = event { + let text = String::from_utf8_lossy(&line); + if let Some(rest) = text.trim().strip_prefix("ATLAS_READY ") { + if let Ok(port) = rest.trim().parse::() { + let url = format!("http://127.0.0.1:{port}"); + if handle.get_webview_window("main").is_none() { + let _ = WebviewWindowBuilder::new( + &handle, + "main", + WebviewUrl::External(url.parse().unwrap()), + ) + .title("ATLAS") + .inner_size(1280.0, 860.0) + .build(); + } + } + } + } + } + }); + + // Tray: click to show, right-click menu to quit. + let quit = MenuItem::with_id(app, "quit", "Quit ATLAS", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&quit])?; + let _tray = TrayIconBuilder::new() + .icon(app.default_window_icon().unwrap().clone()) + .icon_as_template(true) + .menu(&menu) + .show_menu_on_left_click(false) + .on_menu_event(|app, event| { + if event.id.as_ref() == "quit" { + app.exit(0); + } + }) + .on_tray_icon_event(|tray, _event| show_main(tray.app_handle())) + .build(app)?; + + Ok(()) + }) + // Closing the window hides it (keeps ATLAS resident); Quit via tray exits. + .on_window_event(|window, event| { + if let WindowEvent::CloseRequested { api, .. } = event { + api.prevent_close(); + let _ = window.hide(); + } + }) + .build(tauri::generate_context!()) + .expect("error building ATLAS") + .run(|app, event| { + if let RunEvent::ExitRequested { .. } = event { + // Kill the sidecar's process tree so uvicorn never orphans. + if let Some(child) = app.state::().0.lock().unwrap().take() { + let _ = child.kill(); + } + } + }); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..2d352bd --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "ATLAS", + "version": "0.6.0", + "identifier": "ai.atlas.assistant", + "build": { + "frontendDist": "../atlas/interface/web" + }, + "app": { + "withGlobalTauri": true, + "windows": [], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": ["app", "dmg"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "externalBin": ["binaries/atlas-core"], + "macOS": { + "minimumSystemVersion": "12.0", + "entitlements": "Entitlements.plist" + }, + "windows": { + "webviewInstallMode": { "type": "downloadBootstrapper" } + } + } +} From a2e855b2bf23c70a9edb95ba1b4e6d98cb14b457 Mon Sep 17 00:00:00 2001 From: nirhemo Date: Tue, 14 Jul 2026 13:58:12 +0300 Subject: [PATCH 04/13] =?UTF-8?q?feat(app):=20Phase=204/5=20=E2=80=94=20re?= =?UTF-8?q?lease=20CI=20+=20Windows=20readiness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .github/workflows/build-app.yml: macOS + Windows build matrix (tauri-action) on a v* tag — freezes the sidecar, builds the app; macOS signing + notarization activate when the Apple secrets are present (unsigned builds still succeed). - src-tauri/Info.plist: NSMicrophoneUsageDescription for the native voice mic prompt. - keychain_secret() is now cross-platform: macOS `security` CLI primary, OS keyring (Windows Credential Manager / Linux Secret Service) fallback. - deploy/NATIVE_APP.md: build steps, what's verified vs. needs-your-machine, and the Windows follow-ons (Azure signing, WASAPI-loopback AEC reference, CUDA whisper). Signing needs the owner's Apple Developer cert; the Windows build/test needs a Windows runner — both wired, both gated on their inputs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-app.yml | 73 +++++++++++++++++++++++++++++++++ atlas/orchestration/router.py | 17 +++++--- deploy/NATIVE_APP.md | 41 ++++++++++++++++++ requirements.txt | 4 ++ src-tauri/Info.plist | 13 ++++++ 5 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/build-app.yml create mode 100644 deploy/NATIVE_APP.md create mode 100644 src-tauri/Info.plist diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml new file mode 100644 index 0000000..eac64e7 --- /dev/null +++ b/.github/workflows/build-app.yml @@ -0,0 +1,73 @@ +name: Build app + +# Builds the native ATLAS desktop app (Tauri shell + frozen FastAPI sidecar) for +# macOS and Windows. Runs on a version tag (release) or manual dispatch. Signing + +# notarization activate only when the corresponding secrets are present, so forks +# and unsigned test builds still succeed. +# +# Required secrets for signed macOS builds (Settings → Secrets → Actions): +# APPLE_CERTIFICATE (base64 .p12), APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, +# APPLE_ID, APPLE_PASSWORD (app-specific), APPLE_TEAM_ID +# Windows code-signing (beat SmartScreen) is added later via Azure Key Vault. + +on: + push: + tags: ["v*"] + workflow_dispatch: + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: macos-14 # Apple Silicon + triple: aarch64-apple-darwin + - os: windows-latest + triple: x86_64-pc-windows-msvc + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.triple }} + + - name: Python deps + PyInstaller + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt pyinstaller + + - name: Freeze the FastAPI core (sidecar) + run: pyinstaller --clean -y deploy/atlas-core.spec + + - name: Name the sidecar with its target triple + shell: bash + run: | + mkdir -p src-tauri/binaries + if [ "${{ runner.os }}" = "Windows" ]; then + cp dist/atlas-core.exe "src-tauri/binaries/atlas-core-${{ matrix.triple }}.exe" + else + cp dist/atlas-core "src-tauri/binaries/atlas-core-${{ matrix.triple }}" + chmod +x "src-tauri/binaries/atlas-core-${{ matrix.triple }}" + fi + + - name: Build the app (tauri-action; signs when secrets present) + uses: tauri-apps/tauri-action@v0 + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + with: + projectPath: src-tauri + tagName: ${{ github.ref_type == 'tag' && github.ref_name || '' }} + releaseName: "ATLAS ${{ github.ref_name }}" + releaseDraft: true diff --git a/atlas/orchestration/router.py b/atlas/orchestration/router.py index 403e9fd..ecfab9b 100644 --- a/atlas/orchestration/router.py +++ b/atlas/orchestration/router.py @@ -29,10 +29,10 @@ def _identity_prompt() -> str: def keychain_secret(ref: str | None) -> str | None: - """Read a secret from the macOS Keychain given a 'keychain:' ref. - Lets the Owner store the API key securely (never in settings.json or git) - and have ATLAS pick it up without an env var. Returns None off-macOS or - if the item is absent.""" + """Read a secret given a 'keychain:' ref. macOS Keychain via the + `security` CLI (primary); on Windows/Linux it falls back to the OS keyring + (Credential Manager / Secret Service) via the optional `keyring` package. So + the key stays out of settings.json/git on every platform. None if absent.""" if not ref or not ref.startswith("keychain:"): return None name = ref.split(":", 1)[1] @@ -41,8 +41,15 @@ def keychain_secret(ref: str | None) -> str | None: ["security", "find-generic-password", "-s", name, "-w"], capture_output=True, text=True, timeout=5, ) - return out.stdout.strip() or None + val = out.stdout.strip() + if val: + return val except (OSError, subprocess.SubprocessError): + pass + try: # cross-platform fallback (Windows/Linux) + import keyring + return keyring.get_password("atlas", name) or None + except Exception: return None diff --git a/deploy/NATIVE_APP.md b/deploy/NATIVE_APP.md new file mode 100644 index 0000000..8552340 --- /dev/null +++ b/deploy/NATIVE_APP.md @@ -0,0 +1,41 @@ +# ATLAS native app (Tauri) — build & status + +ATLAS ships as a native desktop app that wraps the existing FastAPI HUD. The Python +core is unchanged: it runs as a **sidecar** (a PyInstaller-frozen binary) and the +Tauri shell points a webview at it. The localhost web HUD still works exactly as +before — Tauri is just a better front door. + +## Build (macOS) +Prereqs (once): Rust (`rustup`), Tauri CLI (`cargo install tauri-cli --version ^2`), +PyInstaller (`pip install pyinstaller`), and app icons +(`cd src-tauri && cargo tauri icon ../path/to/atlas-logo.png`). + +```bash +./deploy/build-app.sh # freeze sidecar → build .app + .dmg +``` + +Output: `src-tauri/target/release/bundle/`. + +## Signing / release +`.github/workflows/build-app.yml` builds macOS + Windows on a `v*` tag. macOS +signing + notarization activate when these repo secrets are set (never commit them): +`APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_SIGNING_IDENTITY`, +`APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID`. Entitlements (mic + hardened runtime) +are in `src-tauri/Entitlements.plist`; the mic prompt string is in `src-tauri/Info.plist`. + +## What's verified vs. needs your machine +- ✅ **Verified here:** the sidecar contract (`--port 0` → free port + `ATLAS_READY`), + and the **frozen binary serving the API + HUD with no Python installed**. +- 🖥️ **Your on-device step:** `cargo tauri build` (long Rust compile), launching the + GUI, granting the mic permission, and the interactive voice test. + +## Windows follow-ons (Phase 5) +The shell, `/voice/ws` contract, HUD, settings, Router, and Kokoro are all +platform-agnostic and reused verbatim. Windows-specific work, staged: +- **Signing:** Azure Key Vault (HSM cert) to beat SmartScreen — add to the CI job. +- **AEC reference:** feed the WebRTC canceller a **WASAPI loopback** capture of the + render endpoint (never RAW capture — it strips AEC). Hook in `aec.py` / + `audio_service.py` (`aec_backend="webrtc"`). +- **Secrets:** `keychain_secret` already falls back to the OS keyring (Windows + Credential Manager) when the macOS `security` CLI isn't present. +- **STT:** whisper.cpp on CUDA (NVIDIA) or CPU instead of Metal. diff --git a/requirements.txt b/requirements.txt index ebfe086..6a56bf1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,3 +28,7 @@ pytest>=8.0 # pip install sounddevice numpy openwakeword onnxruntime pywhispercpp # Models auto-download on first run: openWakeWord (hey_jarvis + Silero VAD) and # whisper.cpp (base.en). Not installed in CI (no mic); tests inject fakes. +# Cross-platform secret store (optional): on Windows/Linux the API-key store falls +# back to the OS keyring (Credential Manager / Secret Service) when the macOS +# `security` CLI isn't present. pip install keyring +# Native app packaging (build-time only): pip install pyinstaller (+ Rust/Tauri CLI). diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist new file mode 100644 index 0000000..06bce76 --- /dev/null +++ b/src-tauri/Info.plist @@ -0,0 +1,13 @@ + + + + + + NSMicrophoneUsageDescription + ATLAS listens for the wake word and your voice commands, on-device. + LSUIElement + + + From 0a4fcea5e138b1b49b351759c0341d185c2166e6 Mon Sep 17 00:00:00 2001 From: nirhemo Date: Tue, 14 Jul 2026 15:33:44 +0300 Subject: [PATCH 05/13] fix(voice): wait for speech after wake + conversational follow-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From live testing on the native app: - VAD no longer ends the utterance on the initial silence after the wake word — it waits for the owner to actually START speaking, then detects the end-pause. If nobody speaks within the onset window (5s), it drops back to idle instead of "thinking" at silence. This fixes "it goes straight to thinking without waiting." - Conversational: after ATLAS answers (or a barge-in), it keeps listening for a follow-up with NO wake word needed; an explicit "stop" goes quiet instead. 62 tests pass offline (+3: wait-for-speech/onset-timeout, follow-up-without-wake; updated full-turn/empty/abort for the new flow). Co-Authored-By: Claude Opus 4.8 (1M context) --- atlas/interface/voice/audio_service.py | 35 +++++++++++++++++------ atlas/tests/test_audio_service.py | 39 +++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/atlas/interface/voice/audio_service.py b/atlas/interface/voice/audio_service.py index 6e62ff7..c8d7d2e 100644 --- a/atlas/interface/voice/audio_service.py +++ b/atlas/interface/voice/audio_service.py @@ -158,6 +158,7 @@ def __init__( self.state = "idle" self._utter: list = [] self._silence_frames = 0 + self._heard = False # has the owner actually started speaking yet? self._abort = threading.Event() self._barge = False self._turn = 0 @@ -176,6 +177,10 @@ def _rms(self, frame) -> float: def _silence_limit_frames(self) -> int: return max(1, int(self.vad_silence_ms / 80)) # 80 ms per frame + @property + def _onset_limit_frames(self) -> int: + return int(5.0 * SAMPLE_RATE / FRAME) # wait up to 5s for speech to start + # ----- per-frame state machine (the testable core) -------------------- # def feed(self, frame) -> None: """Process one 80 ms int16 frame. During 'speaking', half-duplex ignores @@ -192,18 +197,26 @@ def feed(self, frame) -> None: self.publish({"type": "wake"}) self._utter = [] self._silence_frames = 0 + self._heard = False self._set_state("listening") except Exception: pass return if self.state == "listening": self._utter.append(frame) - if self._rms(frame) < 0.012: - self._silence_frames += 1 - else: + speaking = self._rms(frame) >= 0.02 # is the owner talking now? + if speaking: + self._heard = True self._silence_frames = 0 - too_long = len(self._utter) > int(_MAX_UTTERANCE_S * SAMPLE_RATE / FRAME) - if self._silence_frames >= self._silence_limit_frames or too_long: + elif self._heard: + self._silence_frames += 1 # only count end-pause AFTER speech starts + n = len(self._utter) + if not self._heard and n >= self._onset_limit_frames: + self._utter = [] # nobody spoke → drop it, back to idle + self._set_state("idle") + return + too_long = n > int(_MAX_UTTERANCE_S * SAMPLE_RATE / FRAME) + if (self._heard and self._silence_frames >= self._silence_limit_frames) or too_long: self._start_turn() def _maybe_barge(self, frame) -> bool: @@ -276,14 +289,17 @@ def _speak(self, reply: str) -> None: except Exception: pass self.publish({"type": "tts", "state": "stop"}) - # barge-in → go straight to listening for the interrupting command; else idle. - if self._barge: + if self._abort.is_set() and not self._barge: + self._set_state("idle") # explicit "stop" → go quiet + else: + # Conversational: after answering (or a barge-in), keep listening for a + # follow-up with NO wake word needed. If you don't reply, the onset + # timeout drops back to idle (wake-word listening). self._barge = False self._utter = [] self._silence_frames = 0 + self._heard = False self._set_state("listening") - else: - self._set_state("idle") # ----- external commands (from /voice/ws) ----------------------------- # def command(self, cmd: str) -> None: @@ -297,6 +313,7 @@ def command(self, cmd: str) -> None: elif cmd == "start" and self.state == "idle": self._utter = [] self._silence_frames = 0 + self._heard = False self._set_state("listening") diff --git a/atlas/tests/test_audio_service.py b/atlas/tests/test_audio_service.py index c3321fc..3c785c2 100644 --- a/atlas/tests/test_audio_service.py +++ b/atlas/tests/test_audio_service.py @@ -69,9 +69,9 @@ def test_full_turn_records_transcribes_responds_and_speaks(): wake["v"] = True svc.feed(_loud()) # wake → listening wake["v"] = False - svc.feed(_loud()) # speech frame + svc.feed(_loud()) # speech frame (owner starts talking) svc.feed(_silence()); svc.feed(_silence()) # 2 silent frames → end utterance - assert svc.state == "idle" # returned to idle after speaking + assert svc.state == "listening" # conversational: awaits a follow-up types = [e.get("type") for e in events] assert "final" in types and "reply" in types # tts start → stop bracket the speech, and Kokoro played at least one sentence @@ -88,12 +88,42 @@ def test_empty_transcript_returns_to_idle_without_responding(): svc, events, _, wake = _svc(router=router, stt_fn=lambda s: " ") wake["v"] = True svc.feed(_loud()); wake["v"] = False + svc.feed(_loud()) # owner spoke, but STT returns blank svc.feed(_silence()); svc.feed(_silence()) - assert svc.state == "idle" + assert svc.state == "idle" # blank transcript → idle, no follow-up assert router.calls == [] # nothing heard → no Router call assert "reply" not in [e.get("type") for e in events] +def test_waits_for_speech_after_wake_then_times_out_to_idle(): + # THE bug fix: after wake, pure silence must NOT trigger a turn — it waits for + # the owner to actually speak, then times out to idle if they never do. + router = _Router() + svc, _, _, wake = _svc(router=router) + wake["v"] = True + svc.feed(_loud()); wake["v"] = False # wake → listening + for _ in range(3): # a few silent frames… + svc.feed(_silence()) + assert svc.state == "listening" # still waiting for you to speak + assert router.calls == [] + for _ in range(svc._onset_limit_frames + 1): # …no speech for the whole window + svc.feed(_silence()) + assert svc.state == "idle" # gives up gracefully, no response + assert router.calls == [] + + +def test_follow_up_after_answer_needs_no_wake_word(): + # Conversational: after ATLAS answers, it listens again with no wake word. + svc, events, _, wake = _svc() + wake["v"] = True + svc.feed(_loud()); wake["v"] = False + svc.feed(_loud()); svc.feed(_silence()); svc.feed(_silence()) # first turn + assert svc.state == "listening" + # a follow-up utterance — WITHOUT saying the wake word again + svc.feed(_loud()); svc.feed(_silence()); svc.feed(_silence()) + assert len(svc.router.calls) == 2 # second turn handled directly + + def test_abort_stops_speaking(): # A synth that aborts mid-way: after the first sentence, flip the abort flag. svc, events, played, wake = _svc() @@ -105,8 +135,9 @@ def synth(text): svc.synth_fn = synth wake["v"] = True svc.feed(_loud()); wake["v"] = False + svc.feed(_loud()) # speech → utterance svc.feed(_silence()); svc.feed(_silence()) - assert svc.state == "idle" + assert svc.state == "idle" # explicit stop → quiet, not follow-up assert {"type": "tts", "state": "stop"} in events # always brackets to stop From 77f01275e2560b90eb902650b8777a4199ce6e1a Mon Sep 17 00:00:00 2001 From: nirhemo Date: Tue, 14 Jul 2026 16:53:20 +0300 Subject: [PATCH 06/13] fix(search): repair photos/search in native voice mode (code review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified fixes from an end-to-end review of the photos/search feature: - parseArticleCommand() extracted — handleFrameVoice() called it but it never existed, so saying "open article N" with a frame open threw ReferenceError. - Native-voice reply now ALWAYS calls renderSearchMedia(media) (was guarded on !== undefined), so stale floating photos/article clear on a non-search reply — matching the text-chat path. - ask(speak) and speakReply() no longer trigger browser TTS in native mode (the native service owns the voice) — kills the dual-voice conflict from the TLDR button / text turns. - web_search.rich() only fetches images when the text search produced results, so a text-search failure can't leave orphaned photos with no article. Backend + rendering were already sound; these fix the native-mode paths. 62 tests pass; headless-verified (no crash, media clears, single voice). Co-Authored-By: Claude Opus 4.8 (1M context) --- atlas/connectors/web_search.py | 6 ++++-- atlas/interface/web/app.js | 32 +++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/atlas/connectors/web_search.py b/atlas/connectors/web_search.py index 3f9f2f0..1f38858 100644 --- a/atlas/connectors/web_search.py +++ b/atlas/connectors/web_search.py @@ -133,9 +133,11 @@ def rich(query: str, *, max_results: int | None = None) -> dict[str, Any]: try: rows = _rows(query, n, web) text = _format(rows, query) + # Only fetch images when the text search produced results — otherwise the + # HUD would show orphaned photos with no article to open. + images = _images(query, max(n + 1, 6)) if rows else [] except Exception as exc: - rows, text = [], f"Web search failed ({type(exc).__name__})." - images = _images(query, max(n + 1, 6)) + rows, text, images = [], f"Web search failed ({type(exc).__name__}).", [] # Numbered article results (the Owner can say "open article 2"): each real # web result gets a thumbnail (best-effort, zipped by index) for the HUD. results = [] diff --git a/atlas/interface/web/app.js b/atlas/interface/web/app.js index 4cb2caa..c9dec5d 100644 --- a/atlas/interface/web/app.js +++ b/atlas/interface/web/app.js @@ -255,7 +255,7 @@ async function ask(text, { speak = false } = {}) { addMsg("atlas", reply); setTranscript(`ATLAS: ${reply}`); renderSearchMedia(data.media); - if (speak) speakReply(reply); else setState("idle"); + if (speak && voiceMode !== "native") speakReply(reply); else setState("idle"); } catch { thinking.remove(); const msg = "Core isn’t running — start it with `python -m atlas.server` to chat for real."; @@ -475,6 +475,7 @@ function speechClean(s) { /* Primary voice: clean on-device neural TTS (Kokoro) served by /api/tts. Falls back to the OS voice if the local model isn't installed. */ async function speakReply(text) { + if (voiceMode === "native") { setState("idle"); return; } // native service owns the voice const spoken = speechClean(text); if (!spoken) { setState("idle"); resumeWake(); return; } stopSpeaking(); @@ -1268,19 +1269,28 @@ function initArticleFrame() { /* "open article 2" / "close article" — resolved in-HUD (voice or chat) */ const _NUMWORD = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5, sixth: 6, one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 }; -function handleArticleCommand(text, speak) { +/* Parse an "open article N" / "close" command → {action:"open",n} | {action:"close"} | null. + Shared by the chat/voice entry (handleArticleCommand) and the article-frame + voice handler (handleFrameVoice). */ +function parseArticleCommand(text) { const t = (text || "").toLowerCase().trim(); - if (t === "close" || /\b(close|dismiss|hide)\b.*\b(article|reader|frame|page|window)\b/.test(t)) { + if (t === "close" || /\b(close|dismiss|hide)\b.*\b(article|reader|frame|page|window)\b/.test(t)) + return { action: "close" }; + const m = t.match(/\b(?:open|show|read|pull up|bring up|display|go to)\b\s+(?:the\s+)?(?:(?:article|result|number|link|photo|image|story|item|#|no\.?)\s+)?(?:the\s+)?(\d{1,2}|first|second|third|fourth|fifth|sixth|one|two|three|four|five|six)\b/); + if (!m) return null; + const n = /^\d+$/.test(m[1]) ? +m[1] : _NUMWORD[m[1]]; + return n ? { action: "open", n } : null; +} +function handleArticleCommand(text, speak) { + const cmd = parseArticleCommand(text); + if (!cmd) return false; + if (cmd.action === "close") { if (!$("#articleFrame").hidden) { closeFrame(); return true; } return false; } - const m = t.match(/\b(?:open|show|read|pull up|bring up|display|go to)\b\s+(?:the\s+)?(?:(?:article|result|number|link|photo|image|story|item|#|no\.?)\s+)?(?:the\s+)?(\d{1,2}|first|second|third|fourth|fifth|sixth|one|two|three|four|five|six)\b/); - if (!m) return false; - const n = /^\d+$/.test(m[1]) ? +m[1] : _NUMWORD[m[1]]; - if (!n) return false; - const r = searchResults[n - 1]; - if (!r) { toast(`No article ${n} yet — search first`, "err"); return true; } - addMsg("owner", `open article ${n}`); + const r = searchResults[cmd.n - 1]; + if (!r) { toast(`No article ${cmd.n} yet — search first`, "err"); return true; } + addMsg("owner", `open article ${cmd.n}`); addMsg("atlas", `Opening “${r.title || r.url}”.`); openArticleInFrame(r); if (speak) resumeWake(); @@ -1330,7 +1340,7 @@ const NativeVoice = { break; case "reply": // ATLAS's answer (spoken natively by the service) if (m.text) { addMsg("atlas", m.text); setTranscript(`ATLAS: ${m.text}`); } - if (m.media !== undefined) renderSearchMedia(m.media); + renderSearchMedia(m.media); // always (renders on search, clears otherwise) break; // "wake" and "tts" states are reflected via the "state" events above. } From ae4b0caffd85d8b21f839a2a8927cd138a7bc9b3 Mon Sep 17 00:00:00 2001 From: nirhemo Date: Wed, 15 Jul 2026 10:28:29 +0300 Subject: [PATCH 07/13] feat(voice): full-duplex barge-in + native voice commands (open/close/stop) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes "stop doesn't work while speaking" and "open article N does nothing": - Full-duplex: while ATLAS speaks, the mic stays live (AEC-cleaned). Barge-in on EITHER the wake word (echo-robust) OR sustained talk-over energy (~400ms), so you can cut in mid-sentence — it stops and listens for your command. - Native voice commands: "open article N" / "close" / "stop" are now recognized and routed to the HUD (open the reader / close / go quiet) instead of being sent to the model, which couldn't act on them. local_command() parses them; the audio service publishes a {command} event the HUD handles. - settings: aec_backend "os" (→ software AEC fallback) enables full-duplex. 64 tests pass offline (+2: open-routes-to-HUD, stop-goes-quiet). Co-Authored-By: Claude Opus 4.8 (1M context) --- atlas/interface/voice/audio_service.py | 60 ++++++++++++++++++++++---- atlas/interface/web/app.js | 7 +++ atlas/tests/test_audio_service.py | 24 +++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/atlas/interface/voice/audio_service.py b/atlas/interface/voice/audio_service.py index c8d7d2e..c8e59ca 100644 --- a/atlas/interface/voice/audio_service.py +++ b/atlas/interface/voice/audio_service.py @@ -118,6 +118,30 @@ def split_sentences(text: str) -> list[str]: return [p.strip() for p in parts if p.strip()] +_NUMWORD = {"first": 1, "second": 2, "third": 3, "fourth": 4, "fifth": 5, "sixth": 6, + "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6} + + +def local_command(text: str): + """Recognize an in-HUD command spoken by voice → (action, n) or (None, 0). + These are handled by the frontend (open the article frame / stop), NOT sent to + the model. 'stop' just goes quiet; 'open article N' / 'close' drive the reader.""" + t = (text or "").lower().strip().rstrip(".!?") + if re.fullmatch(r"(stop|quiet|enough|cancel|never ?mind|be quiet|shush|halt)", t): + return ("stop", 0) + if t in ("close", "close it", "go back", "dismiss") or \ + re.search(r"\b(close|dismiss|hide)\b.*\b(article|reader|it|that|page|window)\b", t): + return ("close", 0) + m = re.search(r"\b(?:open|show|read|pull up|bring up)\b\s+(?:the\s+)?" + r"(?:(?:article|result|number|link|story|item|#|no\.?)\s+)?(?:the\s+)?" + r"(\d{1,2}|first|second|third|fourth|fifth|sixth|one|two|three|four|five|six)\b", t) + if m: + n = int(m.group(1)) if m.group(1).isdigit() else _NUMWORD.get(m.group(1), 0) + if n: + return ("open", n) + return (None, 0) + + # --------------------------------------------------------------------------- # # The service (state machine is injectable → unit-testable without hardware) # --------------------------------------------------------------------------- # @@ -161,6 +185,7 @@ def __init__( self._heard = False # has the owner actually started speaking yet? self._abort = threading.Event() self._barge = False + self._barge_frames = 0 # sustained talk-over frames (for energy barge) self._turn = 0 # ----- helpers -------------------------------------------------------- # @@ -220,20 +245,31 @@ def feed(self, frame) -> None: self._start_turn() def _maybe_barge(self, frame) -> bool: - """AEC-clean the mic against the far-end (TTS) frame, then run wake on the - residual — so 'Hey Jarvis' interrupts mid-sentence without self-triggering. - Sets the abort + barge flags; the turn thread then switches to listening.""" + """Listen while ATLAS speaks: AEC-clean the mic against the far-end (TTS) + frame, then interrupt on EITHER the wake word (echo-robust) OR sustained + talk-over energy (~400 ms of speech above the residual echo). On interrupt, + abort playback + queue to listen so the owner's command is captured.""" + barge = False try: ref = self.ref_provider() if self.ref_provider else None cleaned = self.aec.process(frame, ref) if (self.aec and ref is not None) else frame if self.wake_fn(cleaned): - self._barge = True - self._abort.set() - _stop_playback() - self.publish({"type": "wake"}) - return True + barge = True # "Hey Jarvis" over the top + elif self._rms(cleaned) >= 0.06: # loud talk-over above residual echo + self._barge_frames += 1 + if self._barge_frames >= 5: # ~400 ms sustained → the owner + barge = True + else: + self._barge_frames = 0 except Exception: pass + if barge: + self._barge_frames = 0 + self._barge = True + self._abort.set() + _stop_playback() + self.publish({"type": "wake"}) + return True return False def _start_turn(self) -> None: @@ -259,6 +295,14 @@ def _run_turn(self, audio) -> None: self._set_state("idle") return self.publish({"type": "final", "text": text}) + # In-HUD voice commands (open article N / close / stop) are handled by the + # frontend, not the model — so "open article 1" actually opens the reader. + action, n = local_command(text) + if action: + if action != "stop": + self.publish({"type": "command", "action": action, "n": n}) + self._set_state("idle") + return self._abort.clear() self._barge = False self._turn += 1 diff --git a/atlas/interface/web/app.js b/atlas/interface/web/app.js index c9dec5d..f7ceb36 100644 --- a/atlas/interface/web/app.js +++ b/atlas/interface/web/app.js @@ -1342,6 +1342,13 @@ const NativeVoice = { if (m.text) { addMsg("atlas", m.text); setTranscript(`ATLAS: ${m.text}`); } renderSearchMedia(m.media); // always (renders on search, clears otherwise) break; + case "command": // in-HUD voice command (open article N / close) + if (m.action === "open") { + const r = searchResults[m.n - 1]; + if (r) { addMsg("owner", `open article ${m.n}`); openArticleInFrame(r); } + else toast(`No article ${m.n} — search first`, "err"); + } else if (m.action === "close") { closeFrame(); } + break; // "wake" and "tts" states are reflected via the "state" events above. } }, diff --git a/atlas/tests/test_audio_service.py b/atlas/tests/test_audio_service.py index 3c785c2..163cd49 100644 --- a/atlas/tests/test_audio_service.py +++ b/atlas/tests/test_audio_service.py @@ -141,6 +141,30 @@ def synth(text): assert {"type": "tts", "state": "stop"} in events # always brackets to stop +def test_local_command_open_routes_to_hud_not_model(): + # "open article 2" by voice must drive the HUD, not the LLM. + router = _Router() + svc, events, _, wake = _svc(router=router, stt_fn=lambda s: "open article 2") + wake["v"] = True + svc.feed(_loud()); wake["v"] = False + svc.feed(_loud()); svc.feed(_silence()); svc.feed(_silence()) + assert router.calls == [] # NOT sent to the model + cmds = [e for e in events if e.get("type") == "command"] + assert cmds and cmds[0]["action"] == "open" and cmds[0]["n"] == 2 + assert svc.state == "idle" + + +def test_local_command_stop_goes_quiet(): + router = _Router() + svc, events, _, wake = _svc(router=router, stt_fn=lambda s: "stop") + wake["v"] = True + svc.feed(_loud()); wake["v"] = False + svc.feed(_loud()); svc.feed(_silence()); svc.feed(_silence()) + assert router.calls == [] + assert not [e for e in events if e.get("type") == "command"] # stop → no HUD command + assert svc.state == "idle" + + def test_hub_publish_without_loop_is_safe(): hub = A.VoiceHub() hub.publish({"type": "state", "value": "idle"}) # no loop bound → no crash From 1e130b277f96af8a05baa6dd0398e6a9d7e0a4fd Mon Sep 17 00:00:00 2001 From: nirhemo Date: Wed, 15 Jul 2026 10:50:58 +0300 Subject: [PATCH 08/13] fix(voice+ui): keep listening after a voice command + bigger photos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - After an "open article N" / "close" voice command, the native pipeline now stays in listening (no wake word) so "close" / "next" work while you're reading the article — previously it dropped to idle and "close" alone wasn't heard. - Photos enlarged (~2× target) with fit-to-viewport scaling so the grid never overflows the window (390px on a 1280px window for a 6-photo grid; scales up on bigger windows / fewer results). Centred, staggered fly-in unchanged. 64 tests pass; headless-verified (bigger photos, 0 off-screen, no errors). Co-Authored-By: Claude Opus 4.8 (1M context) --- atlas/interface/voice/audio_service.py | 13 +++++++++---- atlas/interface/web/app.js | 15 ++++++++++----- atlas/tests/test_audio_service.py | 2 +- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/atlas/interface/voice/audio_service.py b/atlas/interface/voice/audio_service.py index c8e59ca..9dfa5bb 100644 --- a/atlas/interface/voice/audio_service.py +++ b/atlas/interface/voice/audio_service.py @@ -298,10 +298,15 @@ def _run_turn(self, audio) -> None: # In-HUD voice commands (open article N / close / stop) are handled by the # frontend, not the model — so "open article 1" actually opens the reader. action, n = local_command(text) - if action: - if action != "stop": - self.publish({"type": "command", "action": action, "n": n}) - self._set_state("idle") + if action == "stop": + self._set_state("idle") # explicit stop → go quiet + return + if action: # open / close → drive the HUD… + self.publish({"type": "command", "action": action, "n": n}) + self._utter = [] # …then keep listening (no wake word) so + self._silence_frames = 0 # "close" / "next" work while reading + self._heard = False + self._set_state("listening") return self._abort.clear() self._barge = False diff --git a/atlas/interface/web/app.js b/atlas/interface/web/app.js index f7ceb36..b53d433 100644 --- a/atlas/interface/web/app.js +++ b/atlas/interface/web/app.js @@ -1148,15 +1148,20 @@ function renderSearchMedia(media) { const shown = results.slice(0, 6); const vw = window.innerWidth, vh = window.innerHeight; const compact = vw < 900; - const iw = compact ? 176 : 248, ih = compact ? 124 : 176; // photo size (~2×) - const capH = 40, ch = ih + capH; - const gapX = 22, gapY = 20; + // Target ~2× the previous size, then scale the whole grid down to fit the window. + let iw = compact ? 360 : 496, ih = compact ? 256 : 352; + let capH = 44, gapX = 22, gapY = 20; const cols = Math.min(shown.length, 3); const rows = Math.ceil(shown.length / cols); + let gW = cols * iw + (cols - 1) * gapX, gH = rows * (ih + capH) + (rows - 1) * gapY; + const scale = Math.min(1, (vw * 0.94) / gW, (vh * 0.9) / gH); + iw = Math.round(iw * scale); ih = Math.round(ih * scale); capH = Math.round(capH * scale); + gapX = Math.round(gapX * scale); gapY = Math.round(gapY * scale); + const ch = ih + capH; const gridW = cols * iw + (cols - 1) * gapX; const gridH = rows * ch + (rows - 1) * gapY; - const startX = Math.max(12, (vw - gridW) / 2); // centred horizontally - const startY = Math.max(64, (vh - gridH) / 2); // …and vertically + const startX = Math.max(8, (vw - gridW) / 2); // centred horizontally + const startY = Math.max(48, (vh - gridH) / 2); // …and vertically const cx = vw / 2 - iw / 2, cy = vh / 2 - ch / 2; // screen centre (pop origin) shown.forEach((r, i) => { const col = i % cols, row = Math.floor(i / cols); diff --git a/atlas/tests/test_audio_service.py b/atlas/tests/test_audio_service.py index 163cd49..4ad8327 100644 --- a/atlas/tests/test_audio_service.py +++ b/atlas/tests/test_audio_service.py @@ -151,7 +151,7 @@ def test_local_command_open_routes_to_hud_not_model(): assert router.calls == [] # NOT sent to the model cmds = [e for e in events if e.get("type") == "command"] assert cmds and cmds[0]["action"] == "open" and cmds[0]["n"] == 2 - assert svc.state == "idle" + assert svc.state == "listening" # keeps listening for "close"/"next" def test_local_command_stop_goes_quiet(): From 55c7352ebf43ef600eae0862a8452d1b62655b9b Mon Sep 17 00:00:00 2001 From: nirhemo Date: Wed, 15 Jul 2026 11:04:21 +0300 Subject: [PATCH 09/13] fix(voice+app): forgiving article-command parser + close-quits behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - local_command() now handles the many ways whisper phrases it ("open article one", "open number 3", "open article number one", "show me article 2", "pull up the third article"…) so voice "open article N" reliably drives the reader instead of falling through to the model. Logs each voice turn's transcript + parsed command (voice_turn event) for diagnosis. - Tauri: closing the window now QUITS ATLAS (stops the backend + voice) instead of silently hiding to the tray and continuing to speak. Quit also on the tray menu. 64 tests pass offline. Co-Authored-By: Claude Opus 4.8 (1M context) --- atlas/interface/voice/audio_service.py | 38 +++++++++---- src-tauri/src/main.rs | 76 ++++++++++++++------------ 2 files changed, 68 insertions(+), 46 deletions(-) diff --git a/atlas/interface/voice/audio_service.py b/atlas/interface/voice/audio_service.py index 9dfa5bb..7b246a0 100644 --- a/atlas/interface/voice/audio_service.py +++ b/atlas/interface/voice/audio_service.py @@ -119,24 +119,35 @@ def split_sentences(text: str) -> list[str]: _NUMWORD = {"first": 1, "second": 2, "third": 3, "fourth": 4, "fifth": 5, "sixth": 6, - "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6} + "seventh": 7, "eighth": 8, "ninth": 9, "tenth": 10, + "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, + "seven": 7, "eight": 8, "nine": 9, "ten": 10} +_NUMRE = "|".join(_NUMWORD) +# open/show/read/pull up + any filler words (the, article, number, up…) + a number. +_OPEN_RE = re.compile( + r"\b(?:open|show|read|pull\s*up|bring\s*up|go\s*to|display)\b\s+" + r"(?:(?:the|up|to|me|us|article|articles|result|results|number|link|story|item|photo|image|one|no)\.?\s+){0,4}" + r"(\d{1,2}|" + _NUMRE + r")\b", re.I) +_STOP_RE = re.compile(r"^\s*(stop|stop it|stop talking|be quiet|quiet|shut up|shush|silence|" + r"enough|that'?s enough|cancel|never\s*mind|hold on|halt)\s*[.!?]*\s*$", re.I) +_CLOSE_RE = re.compile(r"\b(close|dismiss|hide|exit)\b", re.I) def local_command(text: str): """Recognize an in-HUD command spoken by voice → (action, n) or (None, 0). - These are handled by the frontend (open the article frame / stop), NOT sent to - the model. 'stop' just goes quiet; 'open article N' / 'close' drive the reader.""" - t = (text or "").lower().strip().rstrip(".!?") - if re.fullmatch(r"(stop|quiet|enough|cancel|never ?mind|be quiet|shush|halt)", t): + Forgiving of how whisper phrases things ('open article one', 'open the second + one', 'open number 3'…). Handled by the frontend (reader / stop), not the model.""" + t = (text or "").lower().strip() + if _STOP_RE.match(t): return ("stop", 0) - if t in ("close", "close it", "go back", "dismiss") or \ - re.search(r"\b(close|dismiss|hide)\b.*\b(article|reader|it|that|page|window)\b", t): + if _CLOSE_RE.search(t) and re.search(r"\b(article|reader|it|that|this|page|window|frame)\b", t): return ("close", 0) - m = re.search(r"\b(?:open|show|read|pull up|bring up)\b\s+(?:the\s+)?" - r"(?:(?:article|result|number|link|story|item|#|no\.?)\s+)?(?:the\s+)?" - r"(\d{1,2}|first|second|third|fourth|fifth|sixth|one|two|three|four|five|six)\b", t) + if t.rstrip(".!?") in ("close", "go back", "dismiss", "exit"): + return ("close", 0) + m = _OPEN_RE.search(t) if m: - n = int(m.group(1)) if m.group(1).isdigit() else _NUMWORD.get(m.group(1), 0) + g = m.group(1) + n = int(g) if g.isdigit() else _NUMWORD.get(g, 0) if n: return ("open", n) return (None, 0) @@ -298,6 +309,11 @@ def _run_turn(self, audio) -> None: # In-HUD voice commands (open article N / close / stop) are handled by the # frontend, not the model — so "open article 1" actually opens the reader. action, n = local_command(text) + try: + from ...evaluation.logger import log_event + log_event("voice_turn", {"transcript": text, "command": action, "n": n}) + except Exception: + pass if action == "stop": self._set_state("idle") # explicit stop → go quiet return diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3365726..a883bfb 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,17 +1,21 @@ -// ATLAS native shell (Tauri 2). Thin host: runs the existing FastAPI core as a -// sidecar, waits for "ATLAS_READY ", then loads the current HUD in a webview. -// Single-instance, tray show/hide, autostart, and a clean sidecar shutdown on exit. +// ATLAS native shell (Tauri 2). Personal build: runs the owner's real ATLAS +// install (venv + repo data + full voice stack) as the backend, so the app is the +// complete experience — real memory, settings, and on-device voice (Kokoro + +// wake/STT) — in a native window. (A self-contained frozen-sidecar build for +// distribution is a separate path; see deploy/build-app.sh + atlas-core.spec.) #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use std::io::{BufRead, BufReader}; +use std::process::{Child, Command, Stdio}; use std::sync::Mutex; use tauri::menu::{Menu, MenuItem}; use tauri::tray::TrayIconBuilder; use tauri::{Manager, RunEvent, WebviewUrl, WebviewWindowBuilder, WindowEvent}; -use tauri_plugin_shell::process::{CommandChild, CommandEvent}; -use tauri_plugin_shell::ShellExt; -#[derive(Default)] -struct SidecarState(Mutex>); +// The owner's ATLAS checkout (this build targets this machine). +const ATLAS_HOME: &str = "/Users/atlas/ATLAS"; + +struct Backend(Mutex>); fn show_main(app: &tauri::AppHandle) { if let Some(w) = app.get_webview_window("main") { @@ -22,39 +26,42 @@ fn show_main(app: &tauri::AppHandle) { fn main() { tauri::Builder::default() - // single-instance MUST be registered first .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { show_main(app); })) - .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_positioner::init()) .plugin(tauri_plugin_autostart::init( tauri_plugin_autostart::MacosLauncher::LaunchAgent, None, )) - .manage(SidecarState::default()) + .manage(Backend(Mutex::new(None))) .setup(|app| { let handle = app.handle().clone(); - // Spawn the FastAPI core; it prints "ATLAS_READY " once serving. - let sidecar = app - .shell() - .sidecar("atlas-core") - .expect("atlas-core sidecar missing") - .args(["--port", "0"]); - let (mut rx, child) = sidecar.spawn().expect("failed to start atlas-core"); - app.state::().0.lock().unwrap().replace(child); + // Start the real ATLAS core (venv python) on a free port; it prints + // "ATLAS_READY " once serving. + let py = format!("{ATLAS_HOME}/.venv/bin/python"); + let mut child = Command::new(&py) + .args(["-u", "-m", "atlas.server", "--port", "0"]) + .current_dir(ATLAS_HOME) + .env("ATLAS_SUPERVISED", "1") + .stdout(Stdio::piped()) + .spawn() + .expect("failed to start ATLAS core (venv python)"); + let stdout = child.stdout.take().expect("no stdout"); + app.state::().0.lock().unwrap().replace(child); - tauri::async_runtime::spawn(async move { - while let Some(event) = rx.recv().await { - if let CommandEvent::Stdout(line) = event { - let text = String::from_utf8_lossy(&line); - if let Some(rest) = text.trim().strip_prefix("ATLAS_READY ") { - if let Ok(port) = rest.trim().parse::() { - let url = format!("http://127.0.0.1:{port}"); - if handle.get_webview_window("main").is_none() { + std::thread::spawn(move || { + let reader = BufReader::new(stdout); + for line in reader.lines().map_while(Result::ok) { + if let Some(rest) = line.trim().strip_prefix("ATLAS_READY ") { + if let Ok(port) = rest.trim().parse::() { + let h = handle.clone(); + let url = format!("http://127.0.0.1:{port}"); + let _ = handle.run_on_main_thread(move || { + if h.get_webview_window("main").is_none() { let _ = WebviewWindowBuilder::new( - &handle, + &h, "main", WebviewUrl::External(url.parse().unwrap()), ) @@ -62,13 +69,13 @@ fn main() { .inner_size(1280.0, 860.0) .build(); } - } + }); + break; } } } }); - // Tray: click to show, right-click menu to quit. let quit = MenuItem::with_id(app, "quit", "Quit ATLAS", true, None::<&str>)?; let menu = Menu::with_items(app, &[&quit])?; let _tray = TrayIconBuilder::new() @@ -86,19 +93,18 @@ fn main() { Ok(()) }) - // Closing the window hides it (keeps ATLAS resident); Quit via tray exits. + // Closing the window QUITS ATLAS (stops the backend + voice) — no silent + // hide-to-tray that keeps talking. Quit is also on the tray menu. .on_window_event(|window, event| { - if let WindowEvent::CloseRequested { api, .. } = event { - api.prevent_close(); - let _ = window.hide(); + if let WindowEvent::CloseRequested { .. } = event { + window.app_handle().exit(0); } }) .build(tauri::generate_context!()) .expect("error building ATLAS") .run(|app, event| { if let RunEvent::ExitRequested { .. } = event { - // Kill the sidecar's process tree so uvicorn never orphans. - if let Some(child) = app.state::().0.lock().unwrap().take() { + if let Some(mut child) = app.state::().0.lock().unwrap().take() { let _ = child.kill(); } } From afcfe86467f40939a791f80d7f06fbcad258c213 Mon Sep 17 00:00:00 2001 From: nirhemo Date: Wed, 15 Jul 2026 11:12:24 +0300 Subject: [PATCH 10/13] =?UTF-8?q?perf(voice):=20load=20audio=20models=20in?= =?UTF-8?q?=20background=20=E2=80=94=20instant=20app=20startup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native pipeline loaded whisper + openWakeWord synchronously in the FastAPI lifespan, blocking the server (and the app window) for ~15s. Now: - NativeAudioRunner.start() returns immediately and builds the engines + opens the mic on a background thread; the server serves in <1s. - When the pipeline is live it publishes an updated {capabilities}/{ready} over the hub; /voice/ws always subscribes, so a HUD that connected during load switches to native the moment it's ready. - capabilities.audio reflects runner.ready ("native audio loading…" until then). Measured: ATLAS_READY in ~0.9s (was ~15s); native comes up in the background. 64 tests pass offline. Co-Authored-By: Claude Opus 4.8 (1M context) --- atlas/interface/voice/audio_service.py | 29 ++++++++++++++++++++------ atlas/server/app.py | 20 +++++++----------- atlas/tests/test_server.py | 2 ++ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/atlas/interface/voice/audio_service.py b/atlas/interface/voice/audio_service.py index 7b246a0..a991743 100644 --- a/atlas/interface/voice/audio_service.py +++ b/atlas/interface/voice/audio_service.py @@ -396,12 +396,25 @@ def __init__(self, router: Any, hub: VoiceHub) -> None: self._stream = None self._thread = None self._running = False + self._ready = False self.service: AudioService | None = None + @property + def ready(self) -> bool: + return self._ready + def start(self) -> bool: + """Return immediately; load the models + open the mic on a background thread + so the server (and the app window) come up instantly instead of blocking + ~15s while whisper/openWakeWord load. Announces readiness over the hub.""" if self._running or not available(): return False - from .. import voice # noqa: F401 + self._running = True + self._thread = threading.Thread(target=self._build_and_run, daemon=True) + self._thread.start() + return True + + def _build_and_run(self) -> None: from ... import settings as cfg import numpy as np import sounddevice as sd @@ -467,11 +480,15 @@ def _cb(indata, frames, time_info, status): self._stream.start() except Exception: self._stream = None - return False - self._running = True - self._thread = threading.Thread(target=self._loop, daemon=True) - self._thread.start() - return True + self._running = False + return + # Native voice is live — tell any connected HUD to hand over the mic. + self._ready = True + self.hub.publish({"type": "capabilities", "v": 1, "audio": True, "wake": True, + "stt": True, "aec": bool(self.service.full_duplex), + "tts_native": True, "reason": "native audio service running"}) + self.hub.publish({"type": "ready", "audio": True}) + self._loop() def _loop(self) -> None: while self._running: diff --git a/atlas/server/app.py b/atlas/server/app.py index 345c717..11c04c9 100644 --- a/atlas/server/app.py +++ b/atlas/server/app.py @@ -656,7 +656,7 @@ def _voice_capabilities() -> dict[str, Any]: """What the native audio pipeline can do right now. audio=True only when the NativeAudioRunner actually started (mic + wake + STT live); then the HUD hands it mic ownership. Otherwise the browser Web Speech fallback stays active.""" - on = audio_runner is not None + on = audio_runner is not None and audio_runner.ready if on: aec = ((cfg.settings().get("voice") or {}).get("aec_backend") not in (None, "none")) return {"v": 1, "audio": True, "wake": True, "stt": True, "aec": bool(aec), @@ -664,7 +664,8 @@ def _voice_capabilities() -> dict[str, Any]: from ..interface.voice.audio_service import available as _av reason = ("native audio off (enable voice.native_audio)" if not _native_audio_enabled() else - ("native audio deps/models not installed" if not _av() else "native audio not started")) + ("native audio deps/models not installed" if not _av() else + ("native audio loading…" if audio_runner is not None else "native audio not started"))) return {"v": 1, "audio": False, "wake": False, "stt": False, "aec": False, "tts_native": False, "reason": reason} @@ -675,25 +676,18 @@ async def voice_ws(sock: WebSocket) -> None: NativeVoiceProvider connects here, handshakes, and — when capabilities report audio=True — takes over the mic. Broadcasts the audio service's events (state/wake/final/reply/tts) and accepts start/stop/abort. Loopback-only.""" + import asyncio await sock.accept() caps = _voice_capabilities() await sock.send_json({"type": "hello", "v": 1, "version": __version__}) await sock.send_json({"type": "capabilities", **caps}) await sock.send_json({"type": "ready", "audio": caps["audio"]}) await sock.send_json({"type": "state", "value": "idle"}) - if not caps["audio"]: - # No native pipeline — stay connected for handshake/ping only (HUD uses Web Speech). - try: - while True: - msg = await sock.receive_json() - if msg.get("type") == "ping": - await sock.send_json({"type": "pong"}) - except WebSocketDisconnect: - return - return + # ALWAYS subscribe — the native pipeline loads models in the background and + # publishes an updated {capabilities}/{ready} over the hub when it comes live, + # so a client that connected before it was ready switches to native then. q = voice_hub.subscribe() - import asyncio async def _pump() -> None: while True: diff --git a/atlas/tests/test_server.py b/atlas/tests/test_server.py index 609ad80..ef58cd4 100644 --- a/atlas/tests/test_server.py +++ b/atlas/tests/test_server.py @@ -113,6 +113,8 @@ def test_voice_ws_native_path_forwards_commands(monkeypatch): from atlas.server import app as appmod class FakeRunner: + ready = True + def __init__(self): self.cmds = [] From c049a86518cd266045d9a70e3c32923b454b69e6 Mon Sep 17 00:00:00 2001 From: nirhemo Date: Sat, 25 Jul 2026 12:41:28 +0300 Subject: [PATCH 11/13] wip(voice): barge listen-gaps, persistent playback, mic-health readout Inter-sentence listen gaps let the mic (ducked to near-silence during playback on the DGM20/macOS) recover so 'Hey Atlas' or talk-over can interrupt; one persistent output stream replaces per-sentence sd.play churn; raw-mic wake barge with a separate reset-per-utterance model; clip in the mic callback to stop int16 overflow; extend the listen window while an article reader is open; report aec honestly. Adds a /api/voice/native mic-level readout + an echotest endpoint used to diagnose the output-ducks-the-mic behaviour. Barge path is unverified pending mic hardware. Co-Authored-By: Claude Opus 4.8 (1M context) --- atlas/interface/voice/audio_service.py | 324 +++++++++++++++++++------ atlas/server/app.py | 36 ++- 2 files changed, 279 insertions(+), 81 deletions(-) diff --git a/atlas/interface/voice/audio_service.py b/atlas/interface/voice/audio_service.py index a991743..1aa6ae5 100644 --- a/atlas/interface/voice/audio_service.py +++ b/atlas/interface/voice/audio_service.py @@ -23,13 +23,87 @@ _MAX_UTTERANCE_S = 15 # hard cap so a stuck VAD can't record forever +class _Playback: + """One persistent output stream reused across sentences. + + Per-sentence ``sd.play()/sd.wait()`` opened and tore down a fresh OutputStream + for every sentence; that churn (plus the synthesis gap between sentences) made + ATLAS's speech choppy and glitchy. A single long-lived stream written + sentence-by-sentence is smooth. ``stop()`` aborts immediately so barge-in / the + UI Stop cut playback without waiting for the buffer to drain. + """ + + def __init__(self) -> None: + self._stream = None + self._sr = None + self._abort = threading.Event() + self._lock = threading.Lock() + + def _ensure(self, sr: int): + import sounddevice as sd + if self._stream is not None and self._sr == sr and not self._stream.active: + try: + self._stream.start() # re-arm after a prior abort() + except Exception: + try: self._stream.close() + except Exception: pass + self._stream = None + if self._stream is None or self._sr != sr: + if self._stream is not None: + try: self._stream.stop(); self._stream.close() + except Exception: pass + self._stream = sd.OutputStream(samplerate=int(sr), channels=1, dtype="float32") + self._stream.start() + self._sr = int(sr) + return self._stream + + def play(self, samples, sr: int, gain: float = 1.0) -> None: + import numpy as np + self._abort.clear() + data = np.asarray(samples, dtype=np.float32).reshape(-1, 1) + if gain != 1.0: + data = data * float(gain) + with self._lock: + try: + stream = self._ensure(sr) + except Exception: + return + block = 2048 # ~85 ms @ 24 kHz → snappy abort checks + for i in range(0, len(data), block): + if self._abort.is_set(): + break + try: + stream.write(data[i:i + block]) + except Exception: + break + + def stop(self) -> None: + """Interrupt playback now — safe to call from another thread (barge/Stop).""" + self._abort.set() + try: + if self._stream is not None: + self._stream.abort() # drop buffered frames immediately + except Exception: + pass + + def close(self) -> None: + self._abort.set() + with self._lock: + try: + if self._stream is not None: + self._stream.stop(); self._stream.close() + except Exception: + pass + self._stream = None + self._sr = None + + +_PLAYBACK = _Playback() + + def _stop_playback() -> None: """Interrupt any in-progress native playback (barge-in / user Stop).""" - try: - import sounddevice as sd - sd.stop() - except Exception: - pass + _PLAYBACK.stop() # --------------------------------------------------------------------------- # @@ -47,6 +121,13 @@ def wake_model_path(name: str = "hey_jarvis") -> str | None: try: import glob import os + # Prefer a repo-local custom model (e.g. our "hey_atlas" model, trained + # and committed under voice/models/) so it survives openWakeWord reinstalls + # and travels with the source tree. Fall back to openWakeWord's bundled set. + local_dir = os.path.join(os.path.dirname(__file__), "models") + hits = glob.glob(os.path.join(local_dir, f"{name}*.onnx")) + if hits: + return hits[0] import openwakeword d = os.path.join(os.path.dirname(openwakeword.__file__), "resources", "models") hits = glob.glob(os.path.join(d, f"{name}*.onnx")) @@ -174,8 +255,11 @@ def __init__( session: str = "s_voice", aec: Any = None, # EchoCanceller (Phase 3) or None ref_provider: Callable[[], Any] = None, # -> current far-end (TTS) frame, or None - full_duplex: bool = False, # listen while speaking (needs aec + ref) + full_duplex: bool = False, # listen while speaking (wake-word barge) async_turn: bool = True, # run the LLM+TTS turn off the mic thread + barge_wake_fn: Callable[[Any], bool] = None, # dedicated wake model for barge + barge_gap_ms: int = 600, # inter-sentence listen window for barge-in + barge_energy: float = 0.05, # talk-over RMS that counts as an interrupt ) -> None: self.router = router self.publish = publish @@ -187,7 +271,17 @@ def __init__( self.session = session self.aec = aec self.ref_provider = ref_provider - self.full_duplex = bool(full_duplex and aec is not None) + # Barge-in only needs a wake detector, NOT working echo cancellation: the + # software FDAF cannot cancel the split USB-mic/speaker echo, and running + # the wake model on the raw mic (its phrase != ATLAS's words) is more robust. + self.barge_wake_fn = barge_wake_fn + self.full_duplex = bool(full_duplex) + # Many mics (e.g. the MAONO DGM20 with ENC) are ducked to near-silence by + # macOS whenever ANY audio is playing, so the mic is deaf mid-sentence. We + # therefore stop the output for a short gap between sentences — the mic + # recovers and can hear "Hey Atlas" or plain talk-over during that window. + self.barge_gap_ms = int(barge_gap_ms) + self.barge_energy = float(barge_energy) self.async_turn = async_turn self.state = "idle" @@ -196,7 +290,8 @@ def __init__( self._heard = False # has the owner actually started speaking yet? self._abort = threading.Event() self._barge = False - self._barge_frames = 0 # sustained talk-over frames (for energy barge) + self._barge_frames = 0 # sustained talk-over frames (energy barge) + self._reader_open = False # an in-HUD article reader is open (extends listen) self._turn = 0 # ----- helpers -------------------------------------------------------- # @@ -215,12 +310,17 @@ def _silence_limit_frames(self) -> int: @property def _onset_limit_frames(self) -> int: - return int(5.0 * SAMPLE_RATE / FRAME) # wait up to 5s for speech to start + # While an article reader is open we keep listening much longer (no wake word + # needed) so "close" / "next" / a follow-up question still work after you've + # been reading for a while — otherwise the 5 s window drops the mic to idle + # and the reader commands go dead. + secs = 120.0 if self._reader_open else 5.0 + return int(secs * SAMPLE_RATE / FRAME) # ----- per-frame state machine (the testable core) -------------------- # def feed(self, frame) -> None: - """Process one 80 ms int16 frame. During 'speaking', half-duplex ignores - the mic; full-duplex runs AEC + wake on the cleaned signal for barge-in.""" + """Process one 80 ms int16 frame. During 'speaking', full-duplex listens for + the wake word on the raw mic so you can interrupt by saying 'Hey Atlas'.""" if self.state == "speaking": if self.full_duplex: self._maybe_barge(frame) @@ -256,19 +356,19 @@ def feed(self, frame) -> None: self._start_turn() def _maybe_barge(self, frame) -> bool: - """Listen while ATLAS speaks: AEC-clean the mic against the far-end (TTS) - frame, then interrupt on EITHER the wake word (echo-robust) OR sustained - talk-over energy (~400 ms of speech above the residual echo). On interrupt, - abort playback + queue to listen so the owner's command is captured.""" + """Interrupt ATLAS while it speaks, on EITHER the wake word ('Hey Atlas') OR + plain talk-over energy. This is called every frame during 'speaking', but the + mic is ducked to near-silence while audio actually plays — so neither trigger + can fire on ATLAS itself. It's the inter-sentence listen gap (_listen_gap + stops the output) that lets the owner's voice through, where this fires.""" barge = False try: - ref = self.ref_provider() if self.ref_provider else None - cleaned = self.aec.process(frame, ref) if (self.aec and ref is not None) else frame - if self.wake_fn(cleaned): - barge = True # "Hey Jarvis" over the top - elif self._rms(cleaned) >= 0.06: # loud talk-over above residual echo + detect = self.barge_wake_fn or self.wake_fn + if detect(frame): + barge = True # "Hey Atlas" over the top + elif self._rms(frame) >= self.barge_energy: self._barge_frames += 1 - if self._barge_frames >= 5: # ~400 ms sustained → the owner + if self._barge_frames >= 3: # ~240 ms of talk-over in a gap barge = True else: self._barge_frames = 0 @@ -315,10 +415,15 @@ def _run_turn(self, audio) -> None: except Exception: pass if action == "stop": + self._reader_open = False self._set_state("idle") # explicit stop → go quiet return if action: # open / close → drive the HUD… self.publish({"type": "command", "action": action, "n": n}) + if action == "open": + self._reader_open = True # extend the listen window while reading + elif action == "close": + self._reader_open = False self._utter = [] # …then keep listening (no wake word) so self._silence_frames = 0 # "close" / "next" work while reading self._heard = False @@ -336,23 +441,58 @@ def _run_turn(self, audio) -> None: self.publish({"type": "reply", "text": reply, "media": out.get("media")}) self._speak(reply) + def _listen_gap(self) -> None: + """Close playback for barge_gap_ms so the mic (ducked while audio plays) + recovers, then let the mic thread's _maybe_barge listen for an interrupt. + Runs on the speak/worker thread; the mic _loop runs feed()->_maybe_barge in + parallel and sets _abort if it hears the owner, ending the gap early.""" + import time + _PLAYBACK.close() # release output device → mic un-ducks + self._barge_frames = 0 + deadline = time.monotonic() + self.barge_gap_ms / 1000.0 + while time.monotonic() < deadline and not self._abort.is_set(): + time.sleep(0.02) + def _speak(self, reply: str) -> None: + import concurrent.futures as _f spoken = speech_clean(reply) if not spoken: self._set_state("idle") return + # Give the barge detector a clean buffer for this utterance so residual echo + # from the previous turn can't bias 'Hey Atlas' detection. + if self.barge_wake_fn is not None and hasattr(self.barge_wake_fn, "reset"): + try: self.barge_wake_fn.reset() + except Exception: pass self._set_state("speaking") self.publish({"type": "tts", "state": "start"}) - for sentence in split_sentences(spoken): - if self._abort.is_set(): - break - self.publish({"type": "tts", "state": "sentence_start", "text": sentence}) - try: - pcm = self.synth_fn(sentence) - if pcm: - self.play_fn(pcm[0], pcm[1]) - except Exception: - pass + sentences = split_sentences(spoken) + # Pipeline: synthesize sentence k+1 while sentence k is playing, so there is + # no audible gap between sentences (a major cause of choppy playback). + with _f.ThreadPoolExecutor(max_workers=1) as ex: + nxt = ex.submit(self.synth_fn, sentences[0]) if sentences else None + for i, sentence in enumerate(sentences): + if self._abort.is_set(): + break + cur, nxt = nxt, (ex.submit(self.synth_fn, sentences[i + 1]) + if i + 1 < len(sentences) else None) + self.publish({"type": "tts", "state": "sentence_start", "text": sentence}) + try: + pcm = cur.result() if cur else None + if pcm and not self._abort.is_set(): + self.play_fn(pcm[0], pcm[1]) + except Exception: + pass + # Between sentences, stop the output briefly so the (ducked) mic can + # hear an interrupt — "Hey Atlas" or just talking over ATLAS. + if (self.full_duplex and self.barge_gap_ms > 0 + and i < len(sentences) - 1 and not self._abort.is_set()): + self._listen_gap() + if self._abort.is_set(): + break + # Release the output device now that we're done speaking; leaving it open + # keeps the mic ducked, which silently kills the next "Hey Atlas". + _PLAYBACK.close() self.publish({"type": "tts", "state": "stop"}) if self._abort.is_set() and not self._barge: self._set_state("idle") # explicit "stop" → go quiet @@ -398,6 +538,17 @@ def __init__(self, router: Any, hub: VoiceHub) -> None: self._running = False self._ready = False self.service: AudioService | None = None + # Rolling mic-health snapshot (max since last read) so a "is the mic actually + # hearing me?" check is possible without a browser — surfaced at /api/voice/native. + self._mic_dbg = {"frames": 0, "max_rms": 0.0, "max_wake": 0.0, + "max_barge": 0.0, "speak_rms": 0.0, "state": "idle"} + + def mic_health(self, reset: bool = True) -> dict: + d = dict(self._mic_dbg) + if reset: + for k in ("frames", "max_rms", "max_wake", "max_barge", "speak_rms"): + self._mic_dbg[k] = 0.0 + return d @property def ready(self) -> bool: @@ -420,57 +571,41 @@ def _build_and_run(self) -> None: import sounddevice as sd vcfg = (cfg.settings().get("voice") or {}) - wake = _build_wake(vcfg.get("wake_word_model", "hey_jarvis"), - float(vcfg.get("wake_word_sensitivity", 0.6))) + wake_name = vcfg.get("wake_word_model", "hey_jarvis") + wake_sens = float(vcfg.get("wake_word_sensitivity", 0.6)) + wake = _build_wake(wake_name, wake_sens) + # A SECOND, independent wake model for barge-in so speaking-time frames never + # pollute the idle detector's streaming buffer (and vice-versa). + barge_wake = _build_wake(wake_name, wake_sens) stt = _build_stt(vcfg.get("stt_model", "base.en")) voice_id = str(vcfg.get("tts_voice", "kokoro:bm_george")).split(":")[-1] speed = float(vcfg.get("speech_rate", 1.0)) from . import tts as tts_engine - from collections import deque - from .aec import make_aec - - # Full-duplex AEC (Phase 3): the far-end (Kokoro) frames are queued into a - # reference buffer as they play; the mic loop pops them so the canceller - # sees what's coming out of the speaker. Timing here is best-effort and - # wants on-device delay calibration; the macOS VoiceProcessingIO backend - # (aec_backend="os", native bridge) avoids this plumbing entirely. - aec_backend = str(vcfg.get("aec_backend", "os")) - aec = make_aec(aec_backend) if aec_backend not in ("none", "off") else None - ref_q: deque = deque(maxlen=64) - - def ref_provider(): - return ref_q.popleft() if ref_q else None - - def _resample_16k(samples, sr): - if sr == SAMPLE_RATE: - return samples - import numpy as np - n = int(len(samples) * SAMPLE_RATE / sr) - return np.interp(np.linspace(0, len(samples), n, endpoint=False), - np.arange(len(samples)), samples).astype(np.float32) def synth_fn(text: str): return tts_engine.synth_pcm(text, voice=voice_id, speed=speed) def play_fn(samples, sr): - # Queue the far-end frames for the AEC reference, then play. - r = _resample_16k(np.asarray(samples, dtype=np.float32), sr) - pcm16 = np.clip(r * 32768.0, -32768, 32767).astype(np.int16) - for i in range(0, len(pcm16), FRAME): - ref_q.append(pcm16[i:i + FRAME]) - sd.play(samples, sr); sd.wait() + # One persistent output stream, written sentence-by-sentence — no + # per-sentence stream open/close churn, no dual-clock re-latching. + _PLAYBACK.play(samples, sr) self.service = AudioService( router=self.router, publish=self.hub.publish, wake_fn=wake, stt_fn=stt, synth_fn=synth_fn, play_fn=play_fn, vad_silence_ms=int(vcfg.get("vad_silence_ms", 700)), - aec=aec, ref_provider=ref_provider, full_duplex=(aec is not None), + barge_wake_fn=barge_wake, full_duplex=True, + barge_gap_ms=int(vcfg.get("barge_gap_ms", 600)), + barge_energy=float(vcfg.get("barge_energy", 0.05)), ) def _cb(indata, frames, time_info, status): try: - self._q.put_nowait((indata[:, 0] * 32768.0).astype(np.int16)) + # clip before int16 cast: +1.0 * 32768 overflows to -32768 and + # garbles loud audio (e.g. a close-mic "Hey Atlas") for the detector. + pcm = np.clip(indata[:, 0] * 32768.0, -32768, 32767).astype(np.int16) + self._q.put_nowait(pcm) except queue.Full: pass @@ -484,20 +619,37 @@ def _cb(indata, frames, time_info, status): return # Native voice is live — tell any connected HUD to hand over the mic. self._ready = True + # aec=False is honest: there is no working echo cancellation; barge-in is + # wake-word based (say "Hey Atlas" over the top), which the HUD needn't gate on. self.hub.publish({"type": "capabilities", "v": 1, "audio": True, "wake": True, - "stt": True, "aec": bool(self.service.full_duplex), + "stt": True, "aec": False, "barge": True, "tts_native": True, "reason": "native audio service running"}) self.hub.publish({"type": "ready", "audio": True}) self._loop() def _loop(self) -> None: + import numpy as np while self._running: try: frame = self._q.get(timeout=0.5) except queue.Empty: continue if self.service: + st = self.service.state self.service.feed(frame) + d = self._mic_dbg + d["frames"] += 1 + try: + rms = float(np.sqrt(np.mean((frame.astype(np.float32) / 32768.0) ** 2))) + d["max_rms"] = max(d["max_rms"], rms) + d["max_wake"] = max(d["max_wake"], getattr(self.service.wake_fn, "last_score", 0.0)) + if st == "speaking": # measure barge detection + echo + d["max_barge"] = max(d.get("max_barge", 0.0), + getattr(self.service.barge_wake_fn, "last_score", 0.0)) + d["speak_rms"] = max(d.get("speak_rms", 0.0), rms) + except Exception: + pass + d["state"] = self.service.state def command(self, cmd: str) -> None: if self.service: @@ -511,22 +663,38 @@ def stop(self) -> None: except Exception: pass self._stream = None + _PLAYBACK.close() + + +class _WakeDetector: + """A callable openWakeWord detector with a resettable streaming buffer. reset() + clears the model's rolling feature window — used when entering 'speaking' so the + barge detector isn't primed on the previous turn's audio.""" + + def __init__(self, name: str, threshold: float) -> None: + from openwakeword.model import Model + path = wake_model_path(name) or wake_model_path("hey_jarvis") + self._model = Model(wakeword_models=[path], inference_framework="onnx") + self._threshold = float(threshold) + self._key = None + self.last_score = 0.0 + + def __call__(self, frame) -> bool: + scores = self._model.predict(frame) + if self._key is None: + self._key = next(iter(scores), None) + self.last_score = float(scores.get(self._key, 0.0)) if self._key else 0.0 + return self.last_score >= self._threshold + + def reset(self) -> None: + try: + self._model.reset() + except Exception: + pass -def _build_wake(name: str, threshold: float) -> Callable[[Any], bool]: - from openwakeword.model import Model - path = wake_model_path(name) or wake_model_path("hey_jarvis") - model = Model(wakeword_models=[path], inference_framework="onnx") - key = None - - def wake_fn(frame) -> bool: - nonlocal key - scores = model.predict(frame) - if key is None: - key = next(iter(scores), None) - return key is not None and scores.get(key, 0.0) >= threshold - - return wake_fn +def _build_wake(name: str, threshold: float) -> "_WakeDetector": + return _WakeDetector(name, threshold) def _build_stt(model_name: str) -> Callable[[Any], str]: diff --git a/atlas/server/app.py b/atlas/server/app.py index 11c04c9..068aa06 100644 --- a/atlas/server/app.py +++ b/atlas/server/app.py @@ -447,10 +447,39 @@ def voice_native() -> dict[str, Any]: "wake_model": bool(wake_model_path()), "available": available(), "running": audio_runner is not None, + "ready": bool(audio_runner is not None and audio_runner.ready), + "mic": (audio_runner.mic_health() if (audio_runner is not None + and hasattr(audio_runner, "mic_health")) else None), "capabilities": _voice_capabilities(), } +@app.post("/api/voice/echotest") +def voice_echotest(secs: float = 8.0, gain: float = 1.0) -> dict[str, Any]: + """DEBUG: play TTS through the native output while the mic loop keeps running in + idle, so we can measure whether output playback attenuates mic input (the barge-in + 'can't hear me while speaking' symptom). Poll /api/voice/native mic.max_rms during. + gain lets us test whether quieter playback un-ducks the mic (level-based ducking).""" + if audio_runner is None or not getattr(audio_runner, "ready", False): + return {"ok": False, "reason": "native audio not running"} + import threading, time + from ..interface.voice import tts as tts_engine + from ..interface.voice.audio_service import _PLAYBACK + + def _play() -> None: + pcm = tts_engine.synth_pcm( + "Testing one two three four five six seven eight nine ten eleven twelve.", + voice="bm_lewis", speed=1.0) + if not pcm: + return + t0 = time.time() + while time.time() - t0 < secs: + _PLAYBACK.play(pcm[0], pcm[1], gain=float(gain)) + + threading.Thread(target=_play, daemon=True).start() + return {"ok": True, "playing_secs": secs, "gain": gain} + + @app.get("/api/read") def read_page(url: str) -> dict[str, Any]: """Extract an article's readable text so the HUD reader panel can show it @@ -658,9 +687,10 @@ def _voice_capabilities() -> dict[str, Any]: it mic ownership. Otherwise the browser Web Speech fallback stays active.""" on = audio_runner is not None and audio_runner.ready if on: - aec = ((cfg.settings().get("voice") or {}).get("aec_backend") not in (None, "none")) - return {"v": 1, "audio": True, "wake": True, "stt": True, "aec": bool(aec), - "tts_native": True, "reason": "native audio service running"} + # No working echo cancellation (the software AEC can't cancel the real + # speaker→mic echo); barge-in is wake-word based instead. Report honestly. + return {"v": 1, "audio": True, "wake": True, "stt": True, "aec": False, + "barge": True, "tts_native": True, "reason": "native audio service running"} from ..interface.voice.audio_service import available as _av reason = ("native audio off (enable voice.native_audio)" if not _native_audio_enabled() else From 80c7b57e6df3abe9d81943b34faa2b47d591ca85 Mon Sep 17 00:00:00 2001 From: nirhemo Date: Sat, 25 Jul 2026 12:41:28 +0300 Subject: [PATCH 12/13] chore(tauri): app icons + config cleanup Add the teal-delta app icons; drop the unused [lib] target and the externalBin sidecar (the shell now runs the repo venv python directly); mark run.sh executable. Co-Authored-By: Claude Opus 4.8 (1M context) --- atlas/run.sh | 0 src-tauri/Cargo.toml | 4 ---- src-tauri/icons/128x128.png | Bin 0 -> 5969 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 15657 bytes src-tauri/icons/32x32.png | Bin 0 -> 1172 bytes src-tauri/icons/icon.icns | Bin 0 -> 167214 bytes src-tauri/icons/icon.ico | Bin 0 -> 28020 bytes src-tauri/icons/icon.png | Bin 0 -> 36895 bytes src-tauri/tauri.conf.json | 1 - 9 files changed, 5 deletions(-) mode change 100644 => 100755 atlas/run.sh create mode 100644 src-tauri/icons/128x128.png create mode 100644 src-tauri/icons/128x128@2x.png create mode 100644 src-tauri/icons/32x32.png create mode 100644 src-tauri/icons/icon.icns create mode 100644 src-tauri/icons/icon.ico create mode 100644 src-tauri/icons/icon.png diff --git a/atlas/run.sh b/atlas/run.sh old mode 100644 new mode 100755 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f5cf3cf..664c00b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -5,10 +5,6 @@ description = "ATLAS — voice-first local assistant (native shell)" edition = "2021" rust-version = "1.77" -[lib] -name = "atlas_app_lib" -crate-type = ["staticlib", "cdylib", "rlib"] - [build-dependencies] tauri-build = { version = "2", features = [] } diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..24eb6634557b4a110a308f4db7a0adf4ab63f432 GIT binary patch literal 5969 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d3fdc088ccc9c160be5263dcc02ac9e01504f057 GIT binary patch literal 15657 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 m=Sj}^Kz$R={dsTWRsJ_d2zwdb!_%<<0000f2TP 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fe1b94d05285902e36b7da96cad7e5cb80c8d433 GIT binary patch literal 28020 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 Date: Sat, 25 Jul 2026 12:41:28 +0300 Subject: [PATCH 13/13] docs: design for ATLAS develop + create/organize-folders capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Research-backed design (Hermes Agent + OpenClaw) for giving ATLAS filesystem + code-execution tools: a confined ~/ATLAS-workspace, path jail + Apple Seatbelt OS sandbox, READ/WRITE/DESTRUCTIVE gating, git-backed undo, plan/apply folder organizer, and a SKILL.md learning loop, with a phased plan. Proposal for review — no implementation yet. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/DEV_CAPABILITY_DESIGN.md | 199 ++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 docs/DEV_CAPABILITY_DESIGN.md diff --git a/docs/DEV_CAPABILITY_DESIGN.md b/docs/DEV_CAPABILITY_DESIGN.md new file mode 100644 index 0000000..7e78673 --- /dev/null +++ b/docs/DEV_CAPABILITY_DESIGN.md @@ -0,0 +1,199 @@ +# ATLAS Develop + Create/Organize Folders — Design Doc + +Status: **proposal for owner review** (no code written yet). +Informed by research on **Hermes Agent** (NousResearch) and **OpenClaw** (formerly Clawbot). + +--- + +## 1. Goal + +Let ATLAS autonomously **develop** (write + run code) and **create/organize folders**, safely, on the Mac Mini — usable by voice, working with both cloud and local LLMs. This is a **purely additive** capability: it reuses ATLAS's existing L6 tool layer (`orchestration/tools.py`) and L7 permission gate (`connectors/loader.py`) with **no architecture change**. + +## 2. Decisions (locked with owner) + +| Decision | Choice | +|---|---| +| Workspace location | **`~/ATLAS-workspace`** — its own git repo, outside the ATLAS source and MemVault | +| Enable model | **Native, always on** — no connector switch; risky tools still confirm each time | +| Network during `run` | **Allowed** — enables `pip`/`npm`/`git fetch`; exfil defenses hardened to compensate | +| Rollout | **Full design first** (this doc), then phased build with per-phase owner approval | + +## 3. Principles taken from Hermes & OpenClaw + +- **Typed FILE tools, separate from a SHELL/exec tool** (Hermes). Weaker/local models are far more reliable with `read_file`/`write_file`/`edit_file` returning diffs than heredoc-ing into a shell. +- **Two independent gates**: permission (human-in-the-loop) **and** an OS sandbox. ATLAS has the first; we add the second. +- **Un-bypassable hardline blocklist** + **secrets denylist** (Hermes) — some things are refused even when "confirmed". +- **git + markdown workspace, plan-then-apply, move-never-delete** (OpenClaw) — undo is one command; destructive ops are always a two-step "speak the plan → say yes". +- **SKILL.md procedural memory with progressive disclosure** (both) — learned procedures cost almost no context until needed. +- **Short, hand-curated context files** — bloated auto-written `AGENTS.md` measurably *lowers* success; keep them ~15 lines. + +## 4. Workspace model + +One config-driven root, default **`~/ATLAS-workspace/`** (created on first use, its **own git repo**, separate from both the ATLAS product repo and the MemVault repo). + +``` +~/ATLAS-workspace/ + .git/ # git-backed undo (reuse VaultStore.init_git / _git_commit) + AGENTS.md # SHORT, hand-curated workspace rules (setup/test cmds, do-not-touch) + projects// # one folder per project (slug = lowercase-hyphen-ascii, deduped) + README.md AGENTS.md .gitignore LICENSE(AGPL-3.0) + src|pkg/ tests/ Makefile + .venv/ # per-project virtualenv (pip installs land here) + scratch/ # throwaway experiments + .trash/ # soft-deletes land here; ATLAS never `rm`s + skills/ # SKILL.md folders (procedural memory) + .pending/ # self-authored skills await owner review before activation +``` + +- **Git-backed undo**: every WRITE/DESTRUCTIVE fs op **auto-commits the current state first** (aider pattern — you never lose work), enabling a voice-friendly `workspace_undo`. +- **Per-project git init + checkpoint** at creation, so per-project undo works too. +- Templates ship at `atlas/orchestration/templates/{python,node,static,blank}/` (committed, no secrets). +- Boundary discipline: the workspace holds only owner project code + markdown. ATLAS's own config/credentials/registry stay in the ATLAS repo and are **unreachable from the jail** (see §6). + +## 5. Tool set (~14 new tools) + +All are `_t_(args) -> str` handlers on `ToolBox`, dispatched and gated **exactly as today**. Every file/dev handler calls `fsjail.resolve()` on each path first. Risk tag drives approval: **READ** auto-runs · **WRITE** runs then confirms-after (diff shown first) · **DESTRUCTIVE** blocked until you say "yes". + +### FILE (Hermes-style) +| Tool | Risk | Args | Behavior | +|---|---|---|---| +| `list_dir` | READ | `{path?}` | Tree/ls of a workspace dir | +| `read_file` | READ | `{path, offset?, limit?}` | Text with line numbers + pagination | +| `search_files` | READ | `{query, target?:content\|files, regex?}` | ripgrep-backed grep / find | +| `write_file` | WRITE | `{path, content}` | Writes/overwrites whole file, makes parent dirs. **New file** = plain WRITE; **overwrite** returns a unified diff and needs confirmation | +| `edit_file` | WRITE | `{path, old_string, new_string}` | Targeted replace, returns a diff. **Refuses if `old_string` isn't unique** (guards the Hermes patch-corruption failure mode) | +| `create_dir` | WRITE | `{path}` | `mkdir -p` inside the jail | + +### DEV / SCAFFOLD +| Tool | Risk | Args | Behavior | +|---|---|---|---| +| `create_project` | WRITE | `{name, kind?:python\|node\|static\|blank, description?}` | Scaffolds `projects//` from a template, `git init` + checkpoint | +| `run` | DESTRUCTIVE | `{command, cwd?, timeout?, background?}` | Runs a shell/build/test command, cwd pinned in the workspace, wrapped in Seatbelt, captured stdout/stderr (byte-capped) + exit code. The single develop+run primitive (covers `git`, `npm`, `pytest`, `pip install`, …) | +| `run_python` | DESTRUCTIVE | `{script, timeout?}` | Writes a temp script inside the jail, runs it under Seatbelt with a scrubbed env | + +### ORGANIZE (plan/apply — voice-first) +| Tool | Risk | Args | Behavior | +|---|---|---|---| +| `organize_plan` | READ | `{path}` | Classifies files, returns a spoken-friendly manifest (`file → category/ · reason`), stashes it on `ToolBox.last_organize` (mirrors `last_media`). ATLAS **speaks** the plan | +| `organize_apply` | DESTRUCTIVE | `{}` | Consumes the stashed manifest; **move-only** into a sibling `-organized/` dir (source untouched), dedupes, writes `manifest.json` + `.undo-moves.sh`, git checkpoint. **Never deletes** | + +### UNDO / SKILLS +| Tool | Risk | Args | Behavior | +|---|---|---|---| +| `workspace_undo` | WRITE | `{}` | `git revert`/`reset --hard` to the pre-action checkpoint — one-command voice undo | +| `skill_view` | READ | `{name, path?}` | Loads a full `SKILL.md` (or a reference file) on demand | +| `skill_manage` | WRITE | `{action:create\|edit\|write_file, ...}` | Authors/edits a SKILL.md; self-authored skills stage to `skills/.pending/` | + +## 6. Safety model — TWO independent gates + +ATLAS today has **Gate 1** only. This adds **Gate 2** plus a path jail. + +### Gate 1 — permission (reuse `registry.gate` verbatim) +- New tools get `risk` tags in `config.json`; `DESTRUCTIVE`-without-`confirmed` is already refused (`loader.py:59`). +- **Hardline blocklist** (checked inside destructive handlers; `confirmed=true` can NEVER bypass): `rm -rf /`, fork bombs, `dd` to `/dev/*`, `mkfs`, `curl|sh`/`wget|sh` pipe-to-shell. +- **Inline-eval always-confirm**: `python -c`, `node -e`, `bash -c`, `osascript -e` always require confirmation even if the interpreter is allowed. +- **Secrets write-denylist**: refuse writes to any `.env`/`.env.*`/`.envrc`, `~/.ssh`, `~/.aws`, dotfiles, and ATLAS's own `settings.json`/`registry.json`/`owner.md`. +- **User deny-glob list** from `settings.json`. Invariant: *approvals can only tighten, never loosen* (effective = stricter of config + gate). + +### Path jail — `atlas/orchestration/fsjail.py` (new) +- `resolve(p)` = `Path(p).resolve()` (realpath, follows symlinks) then require `.is_relative_to(WORKSPACE_ROOT)`. +- Re-run on **every** call **and after any write** (TOCTOU: an agent that can write can plant an escaping symlink, so a lexical check alone fails). +- Absolute paths outside the root are refused, not silently allowed. + +### Gate 2 — OS sandbox — `atlas/orchestration/sandbox.py` (new) +- `run`/`run_python` execute under **Apple Seatbelt** via `/usr/bin/sandbox-exec -p ` (verified present on this M4 — same approach as Claude Code/Goose). +- Behind a **`SandboxRunner` abstraction** (`run(cmd, cwd, allow_net, timeout)`) so a Docker tier or a post-deprecation replacement drops in. `sandbox-exec` is officially deprecated (still present) — never couple handlers directly to it. +- Backends: `SeatbeltBackend` (default), `DockerBackend` (stub/Phase 5), `NoneBackend` (explicit owner opt-out, never default). + +### Network is ALLOWED — so exfil defenses are the load-bearing protection +Because `run` can reach the network (for `pip`/`npm`), the sandbox can't rely on network isolation. Instead it **prevents secrets from being *read* in the first place**, plus scrubs the environment: +- **Seatbelt denies reads** of `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config`, `~/Library/Keychains`, `~/.netrc`, `~/.docker`, the ATLAS repo's `atlas/config/`, and the MemVault dir — even though network is open. +- **Env scrub**: drop any var matching `*KEY*`/`*TOKEN*`/`*SECRET*`/`*PASSWORD*`/`*CREDENTIAL*`/`*AUTH*` before exec. +- **Writes** confined to the workspace + a temp dir. +- Future hardening (not v1): route outbound through a local proxy for a **domain allowlist** (Seatbelt can't filter by domain, only allow/deny sockets). + +### Illustrative Seatbelt profile (finalized + tested in Phase 2) +```scheme +(version 1) +(deny default) +(allow process-fork process-exec) +(allow signal (target self)) +(allow sysctl-read mach-lookup) +;; reads: system + workspace + temp +(allow file-read* + (subpath "/usr") (subpath "/System") (subpath "/Library") + (subpath "/bin") (subpath "/sbin") (subpath "/opt") + (subpath "/private/var") (subpath "/private/tmp") + (subpath "$WORKSPACE_ROOT") (subpath "$TMPDIR")) +;; …but NEVER these (last-match-wins → secrets denied even with network open) +(deny file-read* + (subpath "$HOME/.ssh") (subpath "$HOME/.aws") (subpath "$HOME/.gnupg") + (subpath "$HOME/.config") (subpath "$HOME/Library/Keychains") + (literal "$HOME/.netrc") (subpath "$HOME/.docker") + (subpath "$ATLAS_REPO/atlas/config") (subpath "$MEMVAULT")) +;; writes: workspace + temp only +(allow file-write* (subpath "$WORKSPACE_ROOT") (subpath "$TMPDIR") (subpath "/private/tmp")) +;; network: ALLOWED per owner decision +(allow network*) +``` + +### Resource limits + audit +- Per-call **timeout** (default 120s, SIGTERM→SIGKILL), max stdout/stderr **byte caps**. +- **Per-task tool-call budget** in `router.py` (the two hardcoded `for _ in range(5)` loops at lines 274/355 become a config-driven budget, default 30, with a near-cap warning). +- **Fail-closed**: an unanswered/again-unconfirmed destructive call is DENIED, not run. +- **Audit line** per fs op / execution via the existing `log_event()` (tool, resolved path, bytes, exit code, `confirmed` flag). + +## 7. Skills & learning loop + +SKILL.md-as-folders with **progressive disclosure**, mapped onto MemVault so it costs the local model almost no context: +- `skills_list` — a ~2–3KB index (name + description) injected into `_system_prompt()` alongside the existing vault `context_block()`. Directly helps weaker local models (prose how-to instead of more JSON). +- `skill_view(name)` / `skill_view(name, path)` — load a full skill only when needed. +- Skills are **gated by `requires.bins/env/os`** reusing the existing `openai_tools()` omission mechanism (a skill whose deps don't match is simply not listed). +- **Authoring**: after a complex task (5+ tool calls / error-recovery / novel workflow), ATLAS may write a SKILL.md via `skill_manage`. **Self-authored skills stage to `skills/.pending/` and require owner review before activation** (never auto-activate — 26% of community skills studied had a vulnerability). +- **Promotion** hooks into the L8 nightly `consolidate` job (only with prior owner opt-in). +- "Memory = small durable facts always in context (MemVault); skills = longer procedures loaded only when relevant." + +## 8. Phased plan (each phase shippable + testable; local-test → owner approval per dev-flow) + +- **Phase 0 — Jail + workspace + READ tools** (nothing executes). `fsjail.py` (+ unit tests for `../` and symlink escape), `workspace` settings block, `list_dir`/`read_file`/`search_files`, create `~/ATLAS-workspace` + `git init`. +- **Phase 1 — Write + scaffold + git undo** (WRITE tier, still no shell). `write_file`/`edit_file`/`create_dir`/`create_project` + templates + `workspace_undo`; diff-before-write; auto-commit-first. +- **Phase 2 — SandboxRunner + `run`/`run_python`** (the real dev capability). `sandbox.py` (Seatbelt SBPL, **network allowed**, secrets read-deny, env scrub, timeout, byte caps); hardline blocklist + inline-eval confirm + secrets write-deny; tool-call budget; audit line. +- **Phase 3 — Organize pipeline** (voice payoff). `organize_plan` (READ, stash manifest) + `organize_apply` (DESTRUCTIVE, move-only, `.trash/`, `.undo-moves.sh`, checkpoint). Persist org taxonomy to MemVault. +- **Phase 4 — Skills + learning loop.** `skills_list` in the system prompt, `skill_view` + `skill_manage`, requires-gating, `.pending/` review, nightly promotion hook. +- **Phase 5 (defer)** — Docker tier, Hermes-style `execute_code` RPC, background `process` poll/kill, subagent delegation, outbound domain-allowlist proxy. + +## 9. Exact ATLAS wiring (verified integration points) + +1. `atlas/orchestration/config.json` — add ~14 `tool_schema[]` entries `{name, risk, description, input_schema}`. No `connector` key (native, per decision). +2. `atlas/orchestration/tools.py` — add `_t_` handlers on `ToolBox`; add `self.last_organize` stash (mirror `self.last_media`, line 53). No change to `dispatch()`. +3. **NEW** `atlas/orchestration/fsjail.py` — path jail + hardline blocklist + secrets/inline-eval matchers (pure, unit-tested). +4. **NEW** `atlas/orchestration/sandbox.py` — `SandboxRunner` + `SeatbeltBackend`/`NoneBackend`/`DockerBackend(stub)`. +5. **NEW** `atlas/orchestration/templates/{python,node,static,blank}/` — committed scaffolds (no secrets). +6. `atlas/config/settings.json` + `settings.example.json` — add a `workspace` block: `{"root":"~/ATLAS-workspace","sandbox_backend":"seatbelt","allow_network":true,"run_timeout_s":120,"max_output_bytes":65536,"deny_globs":[],"tool_call_budget":30}`. (`settings.json` stays gitignored; the example carries defaults.) +7. `atlas/settings.py` — add `workspace_root()` accessor (expand `~`, honor absolute), matching `vault_dir()`. +8. `atlas/orchestration/router.py` — replace the two `for _ in range(5)` tool loops (lines 274, 355) with the config-driven budget + near-cap warning. `confirmed` already flows router→dispatch→gate. +9. `atlas/evaluation/logger.py` — audit line per fs op / execution via `log_event()`. +10. `atlas/scheduler/jobs.py` — extend the `consolidate` handler (JOBS line 154) to review `.pending/` skills nightly (plan-only unless pre-approved). +11. Git-undo helper — reuse the `VaultStore.init_git()` / `_git_commit()` pattern (`memory/store.py:510/529`) in a small workspace-repo helper. + +## 10. Risks & mitigations + +| Risk | Mitigation | +|---|---| +| `sandbox-exec` deprecated | `SandboxRunner` abstraction; Docker/None tiers; settings switch | +| TOCTOU / symlink escape | `Path.resolve` re-run every call + after writes; Seatbelt backstop | +| **Network on → exfil channel** | Seatbelt **denies reads** of secrets; env scrub; writes confined; future domain-allowlist proxy | +| `edit_file` ambiguous-match corruption | refuse when `old_string` count ≠ 1; prefer `write_file`+diff | +| Weak local models misuse `run` | optional per-model omission / force-confirm; `create_project`/`read_file` safe everywhere | +| Prompt-injection via a fetched/scaffolded file | secrets read-deny + env scrub even with net on; treat context files as untrusted | +| Runaway loops | per-call timeout + byte caps + per-task tool budget + fork-bomb blocklist | +| Scheduled autonomy writing/running code | scheduled jobs use the SAME jail+Seatbelt+git-undo path; default plan-only | +| Public-repo secret leak | workspace/deny_globs/timeouts live in gitignored `settings.json`, not code; audit every diff | + +## 11. Remaining open questions + +1. **Package installs**: `pip install`/`npm install` run as `run` (DESTRUCTIVE, confirmed) into a per-project `.venv`/`node_modules`. OK, or do you want an explicit `install` tool with its own confirmation copy? +2. **Local-model bar**: which local models (if any) should have `run`/`run_python` hidden or force-always-confirm? +3. **Self-authored skill promotion**: keep OFF by default (you review `.pending/` manually)? Recommended yes. +4. **Tool-call budget**: 30 per task (up from 5)? Warn aloud near the cap on voice turns? +5. **`organize_apply` outside the workspace** (e.g. real `~/Downloads`): confined-to-workspace in v1, with a separate approved-paths list later? Recommended yes.