Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/build-app.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
10 changes: 5 additions & 5 deletions atlas/VERSION.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -14,15 +14,15 @@
"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)",
"changelog_ref": "atlas/CHANGELOG.md",
"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"
]
}
4 changes: 3 additions & 1 deletion atlas/config/settings.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 4 additions & 2 deletions atlas/connectors/web_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
110 changes: 110 additions & 0 deletions atlas/interface/voice/aec.py
Original file line number Diff line number Diff line change
@@ -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()
Loading
Loading