edith-pc 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edith_pc-0.3.0/LICENSE +21 -0
- edith_pc-0.3.0/PKG-INFO +75 -0
- edith_pc-0.3.0/PYPI.md +39 -0
- edith_pc-0.3.0/README.md +262 -0
- edith_pc-0.3.0/edith/__init__.py +13 -0
- edith_pc-0.3.0/edith/audio_io.py +130 -0
- edith_pc-0.3.0/edith/backup.py +260 -0
- edith_pc-0.3.0/edith/body/__init__.py +17 -0
- edith_pc-0.3.0/edith/body/__main__.py +116 -0
- edith_pc-0.3.0/edith/body/cli.py +224 -0
- edith_pc-0.3.0/edith/body/client.py +371 -0
- edith_pc-0.3.0/edith/body/desktop.py +478 -0
- edith_pc-0.3.0/edith/body/identity.py +10 -0
- edith_pc-0.3.0/edith/body/link.py +206 -0
- edith_pc-0.3.0/edith/body/pc_backup.py +347 -0
- edith_pc-0.3.0/edith/config.py +205 -0
- edith_pc-0.3.0/edith/gui.py +610 -0
- edith_pc-0.3.0/edith/gui_classic.py +352 -0
- edith_pc-0.3.0/edith/hud/__init__.py +11 -0
- edith_pc-0.3.0/edith/hud/easing.py +72 -0
- edith_pc-0.3.0/edith/hud/fonts.py +69 -0
- edith_pc-0.3.0/edith/hud/orb_renderer.py +66 -0
- edith_pc-0.3.0/edith/hud/textures.py +39 -0
- edith_pc-0.3.0/edith/legacy_migration.py +185 -0
- edith_pc-0.3.0/edith/logging_setup.py +53 -0
- edith_pc-0.3.0/edith/main.py +91 -0
- edith_pc-0.3.0/edith/pricing.py +143 -0
- edith_pc-0.3.0/edith/prompts.py +117 -0
- edith_pc-0.3.0/edith/realtime_client.py +685 -0
- edith_pc-0.3.0/edith/relay/__init__.py +12 -0
- edith_pc-0.3.0/edith/relay/__main__.py +4 -0
- edith_pc-0.3.0/edith/relay/bridge.py +333 -0
- edith_pc-0.3.0/edith/relay/crypto.py +186 -0
- edith_pc-0.3.0/edith/relay/server.py +147 -0
- edith_pc-0.3.0/edith/service/__init__.py +18 -0
- edith_pc-0.3.0/edith/service/__main__.py +130 -0
- edith_pc-0.3.0/edith/service/app.py +1180 -0
- edith_pc-0.3.0/edith/service/approvals.py +119 -0
- edith_pc-0.3.0/edith/service/brain.py +371 -0
- edith_pc-0.3.0/edith/service/devicebook.py +212 -0
- edith_pc-0.3.0/edith/service/devices.py +200 -0
- edith_pc-0.3.0/edith/service/phone_agent.py +266 -0
- edith_pc-0.3.0/edith/service/protocol.py +99 -0
- edith_pc-0.3.0/edith/service/tokens.py +32 -0
- edith_pc-0.3.0/edith/service/totp.py +61 -0
- edith_pc-0.3.0/edith/service/webauth.py +238 -0
- edith_pc-0.3.0/edith/session_manager.py +99 -0
- edith_pc-0.3.0/edith/tools/__init__.py +1 -0
- edith_pc-0.3.0/edith/tools/briefing.py +115 -0
- edith_pc-0.3.0/edith/tools/clock.py +13 -0
- edith_pc-0.3.0/edith/tools/computer_agent.py +419 -0
- edith_pc-0.3.0/edith/tools/delegate.py +99 -0
- edith_pc-0.3.0/edith/tools/documents.py +76 -0
- edith_pc-0.3.0/edith/tools/external_cost.py +36 -0
- edith_pc-0.3.0/edith/tools/file_ops.py +189 -0
- edith_pc-0.3.0/edith/tools/google_workspace.py +205 -0
- edith_pc-0.3.0/edith/tools/health_watch.py +70 -0
- edith_pc-0.3.0/edith/tools/home_assistant.py +142 -0
- edith_pc-0.3.0/edith/tools/llm.py +28 -0
- edith_pc-0.3.0/edith/tools/memory.py +205 -0
- edith_pc-0.3.0/edith/tools/news_finance.py +137 -0
- edith_pc-0.3.0/edith/tools/notes.py +50 -0
- edith_pc-0.3.0/edith/tools/pc_control.py +474 -0
- edith_pc-0.3.0/edith/tools/registry.py +942 -0
- edith_pc-0.3.0/edith/tools/reminders.py +129 -0
- edith_pc-0.3.0/edith/tools/spotify_music.py +141 -0
- edith_pc-0.3.0/edith/tools/system_control.py +197 -0
- edith_pc-0.3.0/edith/tools/system_toggles.py +261 -0
- edith_pc-0.3.0/edith/tools/task_manager.py +137 -0
- edith_pc-0.3.0/edith/tools/telegram_notify.py +36 -0
- edith_pc-0.3.0/edith/tools/vision.py +109 -0
- edith_pc-0.3.0/edith/tools/weather.py +57 -0
- edith_pc-0.3.0/edith/tools/web_search.py +65 -0
- edith_pc-0.3.0/edith/tools/whatsapp.py +97 -0
- edith_pc-0.3.0/edith/tools/window_control.py +146 -0
- edith_pc-0.3.0/edith/vault.py +395 -0
- edith_pc-0.3.0/edith/wake_word.py +159 -0
- edith_pc-0.3.0/edith_pc.egg-info/PKG-INFO +75 -0
- edith_pc-0.3.0/edith_pc.egg-info/SOURCES.txt +123 -0
- edith_pc-0.3.0/edith_pc.egg-info/dependency_links.txt +1 -0
- edith_pc-0.3.0/edith_pc.egg-info/entry_points.txt +5 -0
- edith_pc-0.3.0/edith_pc.egg-info/requires.txt +22 -0
- edith_pc-0.3.0/edith_pc.egg-info/top_level.txt +1 -0
- edith_pc-0.3.0/pyproject.toml +68 -0
- edith_pc-0.3.0/setup.cfg +4 -0
- edith_pc-0.3.0/tests/test_backup.py +178 -0
- edith_pc-0.3.0/tests/test_body.py +434 -0
- edith_pc-0.3.0/tests/test_briefing.py +63 -0
- edith_pc-0.3.0/tests/test_computer_agent.py +179 -0
- edith_pc-0.3.0/tests/test_delegate.py +67 -0
- edith_pc-0.3.0/tests/test_documents.py +55 -0
- edith_pc-0.3.0/tests/test_external_cost.py +41 -0
- edith_pc-0.3.0/tests/test_file_ops.py +94 -0
- edith_pc-0.3.0/tests/test_google.py +51 -0
- edith_pc-0.3.0/tests/test_home_assistant.py +51 -0
- edith_pc-0.3.0/tests/test_idle.py +53 -0
- edith_pc-0.3.0/tests/test_legacy_migration.py +184 -0
- edith_pc-0.3.0/tests/test_memory.py +107 -0
- edith_pc-0.3.0/tests/test_news_finance.py +61 -0
- edith_pc-0.3.0/tests/test_notes.py +29 -0
- edith_pc-0.3.0/tests/test_packaging.py +162 -0
- edith_pc-0.3.0/tests/test_pc_link.py +479 -0
- edith_pc-0.3.0/tests/test_phone_agent.py +152 -0
- edith_pc-0.3.0/tests/test_pricing.py +95 -0
- edith_pc-0.3.0/tests/test_registry.py +56 -0
- edith_pc-0.3.0/tests/test_relay.py +547 -0
- edith_pc-0.3.0/tests/test_run_command.py +73 -0
- edith_pc-0.3.0/tests/test_search.py +32 -0
- edith_pc-0.3.0/tests/test_service_app.py +177 -0
- edith_pc-0.3.0/tests/test_service_brain.py +177 -0
- edith_pc-0.3.0/tests/test_service_devices.py +94 -0
- edith_pc-0.3.0/tests/test_service_protocol.py +45 -0
- edith_pc-0.3.0/tests/test_service_transfer.py +201 -0
- edith_pc-0.3.0/tests/test_spotify.py +41 -0
- edith_pc-0.3.0/tests/test_system_toggles.py +118 -0
- edith_pc-0.3.0/tests/test_task_manager.py +94 -0
- edith_pc-0.3.0/tests/test_telegram.py +33 -0
- edith_pc-0.3.0/tests/test_vault.py +241 -0
- edith_pc-0.3.0/tests/test_voice_confirm.py +176 -0
- edith_pc-0.3.0/tests/test_voice_hud.py +262 -0
- edith_pc-0.3.0/tests/test_wake_word.py +56 -0
- edith_pc-0.3.0/tests/test_web_hud.py +221 -0
- edith_pc-0.3.0/tests/test_webauth.py +262 -0
- edith_pc-0.3.0/tests/test_whatsapp.py +36 -0
- edith_pc-0.3.0/tests/test_window_control.py +97 -0
edith_pc-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shuvo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
edith_pc-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: edith-pc
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: EDITH on this PC: a body of your EDITH server, linked with a Google Authenticator code
|
|
5
|
+
Author: Shuvo
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: assistant,voice assistant,automation,windows,openai
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
10
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
11
|
+
Classifier: Topic :: Desktop Environment
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: openai>=1.70.0
|
|
16
|
+
Requires-Dist: websockets>=15.0
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Requires-Dist: python-dotenv>=1.0
|
|
19
|
+
Requires-Dist: requests>=2.31
|
|
20
|
+
Requires-Dist: psutil>=5.9
|
|
21
|
+
Requires-Dist: APScheduler<4.0,>=3.10
|
|
22
|
+
Requires-Dist: SQLAlchemy>=2.0
|
|
23
|
+
Requires-Dist: pycaw>=20240210; sys_platform == "win32"
|
|
24
|
+
Requires-Dist: comtypes>=1.2; sys_platform == "win32"
|
|
25
|
+
Requires-Dist: pynput>=1.7
|
|
26
|
+
Requires-Dist: screen-brightness-control>=0.22
|
|
27
|
+
Requires-Dist: pyautogui>=0.9.54
|
|
28
|
+
Requires-Dist: pyperclip>=1.8
|
|
29
|
+
Requires-Dist: AppOpener>=1.7; sys_platform == "win32"
|
|
30
|
+
Requires-Dist: mss>=9.0
|
|
31
|
+
Requires-Dist: windows-toasts>=1.1; sys_platform == "win32"
|
|
32
|
+
Requires-Dist: pypdf>=4.0
|
|
33
|
+
Requires-Dist: Pillow>=10
|
|
34
|
+
Requires-Dist: customtkinter>=5.2
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# EDITH for your PC
|
|
38
|
+
|
|
39
|
+
`edith-pc` makes a Windows PC a body of your **EDITH** server: the server's brain can use this
|
|
40
|
+
PC's tools (apps, windows, files, the screen, the keyboard, the shell) when you talk to EDITH
|
|
41
|
+
from your phone or the browser, from anywhere.
|
|
42
|
+
|
|
43
|
+
You need an EDITH server (the brain) with Google Authenticator sign-in set up. This package is
|
|
44
|
+
only the PC side.
|
|
45
|
+
|
|
46
|
+
## Install and link
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
pip install edith-pc
|
|
50
|
+
edith link
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`edith link` asks for your server's address and the 6-digit code Google Authenticator shows
|
|
54
|
+
for EDITH. That's all: no token or key to copy. The PC is linked, EDITH runs in the
|
|
55
|
+
background, and it starts with Windows from then on.
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
| Command | What it does |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `edith status` | what this PC is linked to, and whether EDITH is running |
|
|
62
|
+
| `edith start` / `edith stop` | start or stop the background body |
|
|
63
|
+
| `edith app` | open the EDITH window |
|
|
64
|
+
| `edith run` | run the body in the terminal (Ctrl+C stops it) |
|
|
65
|
+
| `edith update` | install the newest version and restart |
|
|
66
|
+
| `edith unlink` | unlink this PC and stop starting with Windows |
|
|
67
|
+
|
|
68
|
+
## What the PC can and can't do
|
|
69
|
+
|
|
70
|
+
The server gives this PC a key of its own when you link it. The key only lets the PC answer
|
|
71
|
+
the brain's tool calls and send model calls through your server, so no OpenAI key lives on
|
|
72
|
+
the PC. It can't send commands, approve actions or read the server's files. Remove the PC in
|
|
73
|
+
EDITH's HUD (Bodies, PCs) and its key stops working at once.
|
|
74
|
+
|
|
75
|
+
Windows only. Python 3.10 or newer.
|
edith_pc-0.3.0/PYPI.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# EDITH for your PC
|
|
2
|
+
|
|
3
|
+
`edith-pc` makes a Windows PC a body of your **EDITH** server: the server's brain can use this
|
|
4
|
+
PC's tools (apps, windows, files, the screen, the keyboard, the shell) when you talk to EDITH
|
|
5
|
+
from your phone or the browser, from anywhere.
|
|
6
|
+
|
|
7
|
+
You need an EDITH server (the brain) with Google Authenticator sign-in set up. This package is
|
|
8
|
+
only the PC side.
|
|
9
|
+
|
|
10
|
+
## Install and link
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
pip install edith-pc
|
|
14
|
+
edith link
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`edith link` asks for your server's address and the 6-digit code Google Authenticator shows
|
|
18
|
+
for EDITH. That's all: no token or key to copy. The PC is linked, EDITH runs in the
|
|
19
|
+
background, and it starts with Windows from then on.
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
| Command | What it does |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `edith status` | what this PC is linked to, and whether EDITH is running |
|
|
26
|
+
| `edith start` / `edith stop` | start or stop the background body |
|
|
27
|
+
| `edith app` | open the EDITH window |
|
|
28
|
+
| `edith run` | run the body in the terminal (Ctrl+C stops it) |
|
|
29
|
+
| `edith update` | install the newest version and restart |
|
|
30
|
+
| `edith unlink` | unlink this PC and stop starting with Windows |
|
|
31
|
+
|
|
32
|
+
## What the PC can and can't do
|
|
33
|
+
|
|
34
|
+
The server gives this PC a key of its own when you link it. The key only lets the PC answer
|
|
35
|
+
the brain's tool calls and send model calls through your server, so no OpenAI key lives on
|
|
36
|
+
the PC. It can't send commands, approve actions or read the server's files. Remove the PC in
|
|
37
|
+
EDITH's HUD (Bodies, PCs) and its key stops working at once.
|
|
38
|
+
|
|
39
|
+
Windows only. Python 3.10 or newer.
|
edith_pc-0.3.0/README.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# 🤖 EDITH — a real-time desktop voice assistant
|
|
2
|
+
|
|
3
|
+
> Talk to your computer like Tony Stark. A Python desktop assistant built on the **OpenAI Realtime API** (`gpt-realtime-2.1-mini` by default, `gpt-realtime-2.1` for demos) — a sleek, tactical AI that wakes to a hotword, holds a natural, continuous spoken conversation, and actually **does things**: delegates hard questions to a stronger reasoning model, autonomously drives your mouse and keyboard through multi-step tasks, plays Spotify, reads your email and documents aloud, runs your smart home, and controls your PC.
|
|
4
|
+
|
|
5
|
+
<!-- TODO: record a 30–60s demo and drop it here. A GIF (<10MB) renders inline on GitHub and beats a YouTube link. -->
|
|
6
|
+
<!--  -->
|
|
7
|
+
|
|
8
|
+
[](https://github.com/sohanurislamshuvo/edith/actions/workflows/ci.yml)
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
- 🎙️ **Speech-to-speech, real time** — OpenAI Realtime API over WebSocket, sub-second responses. **Barge-in** (interrupt it mid-sentence and it stops to listen) works with headphones; on laptop speakers it runs half-duplex by default so it can't hear itself (`EDITH_HALF_DUPLEX`).
|
|
18
|
+
- 🗣️ **Wake word + continuous conversation** — keyless, fully on-device hotword (default **"Edith"**) via Vosk — `pip install vosk` then `python scripts/setup_wake_word.py`, no account or cost (see [docs/WAKE_WORD.md](docs/WAKE_WORD.md)); after each reply she keeps listening, so follow-ups need no wake word until it idle-sleeps (`EDITH_IDLE_TIMEOUT_SECONDS`). Without setup she falls back to a Press-Enter prompt.
|
|
19
|
+
- 🧠 **Hybrid brain** — the voice model handles the conversation; genuinely hard questions (research, math, debugging, planning) are **delegated to a stronger reasoning model with live web access** and the answer is spoken back (`delegate_task`).
|
|
20
|
+
- 🖱️ **Computer autonomy** — *"Open Notepad and write a shopping list"* actually happens: a bounded **see → decide → act → verify** loop screenshots the screen, plans one action at a time, and drives the mouse/keyboard — confirm-gated, cost-capped, audited, with ESC and mouse-corner kill switches.
|
|
21
|
+
- 🧰 **70 function-calling tools** — it doesn't just talk, it acts:
|
|
22
|
+
- 🌐 Live **web search**, 📰 **news headlines**, 📈 **stock quotes**, 💱 **currency conversion** (all keyless)
|
|
23
|
+
- 🌦️ **Weather** (Open-Meteo, no API key) & 🕐 time/date
|
|
24
|
+
- ⏰ **Reminders & timers** (persistent, SQLite) with desktop notifications, plus a spoken **morning briefing** on the day's first wake
|
|
25
|
+
- 🧠 **Semantic long-term memory** — "remember my wifi password"; recall works by meaning, not exact words
|
|
26
|
+
- 👁️ **Screen vision & diagnosis** — *"What's wrong on my screen?"*; EDITH captures the screen, **analyzes** it, and reads out a **step-by-step fix plan** (then it can act with the mouse/keyboard).
|
|
27
|
+
- 🎵 **Spotify** play-by-name + now-playing, 🎬 YouTube
|
|
28
|
+
- 📧 **Gmail & Google Calendar** — unread summaries, send (confirm-gated), list/create events
|
|
29
|
+
- 🏠 **Home Assistant** — lights/switches/scenes by their spoken names
|
|
30
|
+
- 📄 **Document reading** — *"summarize the PDF on my desktop"* (PDF/txt/md/code)
|
|
31
|
+
- 📱 **Telegram push-to-phone** and **WhatsApp** (message prefilled, you press send)
|
|
32
|
+
- 📝 **Voice notes** — dictate to per-topic Markdown files and read them back
|
|
33
|
+
- 💻 **PC control**: open/close apps, system stats, volume, **screen brightness**, media keys, screenshots, **type & hotkeys & mouse**, **files/folders & clipboard**, **window & power** (lock/sleep/shutdown), and **shell commands** — with spoken confirmation required for anything destructive, plus an ambient **health watchdog** (battery/disk/CPU toasts).
|
|
34
|
+
- 💸 **Cost-guarded by design** — split-prefix prompt caching, trimmed tool outputs, a live token/$ budget counter that also meters every delegated model call, idle-close, and a hard auto-disconnect timer so a forgotten session can't drain your credit.
|
|
35
|
+
- 🖥️ **Desktop HUD ("Halo Reactor")** — a frameless, glass-panelled interface built around a real Gaussian-bloomed arc-reactor orb that breathes, pulses with your voice and crossfades colour by state, with a holographic ring assembly, speech ripples, a live transcript, a status readout and a running cost meter (`python -m edith.gui`).
|
|
36
|
+
|
|
37
|
+
## 🏗️ Architecture
|
|
38
|
+
|
|
39
|
+
```mermaid
|
|
40
|
+
flowchart LR
|
|
41
|
+
A[🎤 Mic] -->|PCM16 24kHz| B[audio_io]
|
|
42
|
+
W[👂 Wake word<br/>'Edith'] -->|gate| C
|
|
43
|
+
B -->|base64 frames| C[realtime_client<br/>WebSocket]
|
|
44
|
+
C <-->|speech-to-speech| D[(OpenAI Realtime API)]
|
|
45
|
+
D -->|function_call| E[tools/registry]
|
|
46
|
+
E --> F[web · weather · news · stocks · reminders<br/>delegate_task · computer agent · memory<br/>Spotify · Gmail/Calendar · Home Assistant<br/>screen vision · documents · pc_control]
|
|
47
|
+
F -->|trimmed JSON| C
|
|
48
|
+
D -->|audio deltas| B
|
|
49
|
+
B -->|cedar voice| G[🔊 Speaker]
|
|
50
|
+
C -.usage.-> H[session_manager<br/>budget guard + hard timer]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Why it's structured this way:** the transport (`realtime_client`), audio (`audio_io`), tools (`tools/`), and cost guardrails (`session_manager`) are decoupled, so any layer — model, voice, or a new tool — can be swapped without touching the others.
|
|
54
|
+
|
|
55
|
+
## 🚀 Quickstart
|
|
56
|
+
|
|
57
|
+
```powershell
|
|
58
|
+
# 1. Clone & enter
|
|
59
|
+
git clone <your-repo-url> edith ; cd edith
|
|
60
|
+
|
|
61
|
+
# 2. Create a virtual env and install
|
|
62
|
+
python -m venv .venv ; .\.venv\Scripts\Activate.ps1
|
|
63
|
+
pip install -r requirements.txt
|
|
64
|
+
pip install -r requirements-dev.txt # optional: pytest — then `python -m pytest`
|
|
65
|
+
|
|
66
|
+
# 3. Add your keys
|
|
67
|
+
Copy-Item .env.example .env # then edit .env: add your OPENAI_API_KEY
|
|
68
|
+
|
|
69
|
+
# 4. Sanity-check your mic & speakers (records 3s, plays it back)
|
|
70
|
+
python scripts/test_audio.py
|
|
71
|
+
|
|
72
|
+
# 5. Verify the tools work with ZERO API cost (no voice yet)
|
|
73
|
+
python scripts/test_tools_text.py
|
|
74
|
+
|
|
75
|
+
# 6. Talk to EDITH — pick one:
|
|
76
|
+
python -m edith.gui # Halo Reactor HUD: bloom orb + glass transcript + cost meter
|
|
77
|
+
python -m edith.gui_classic # the original HUD (guaranteed-working fallback)
|
|
78
|
+
python -m edith.main # terminal / CLI
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Say **"Edith"** (the default hotword, keyless via Vosk — run `python scripts/setup_wake_word.py` once to enable it), wait for the greeting, then try: *"What's my CPU and battery?"*, *"Play Bohemian Rhapsody."*, *"What's Apple stock at?"*, *"What's 100 dollars in taka?"*, *"Research the best budget mechanical keyboards for me."* (delegates to the reasoning model), or *"Open Notepad and write a shopping list."* (autonomous computer task — confirm it, hands off, ESC aborts). Follow-ups need no wake word; it goes back to sleep after ~30s of silence.
|
|
82
|
+
|
|
83
|
+
**Optional integrations** (each degrades gracefully when unconfigured — see `.env.example`): Spotify (2-minute free dev app), Telegram (BotFather token), Home Assistant (long-lived token), Gmail/Calendar (`python scripts/setup_google.py` after a one-time Google Cloud setup; Testing-mode tokens re-consent weekly).
|
|
84
|
+
|
|
85
|
+
## 📱 Phone app (Android)
|
|
86
|
+
|
|
87
|
+
EDITH's second body. The Android app in [`android/`](android/) connects to the PC over Wi-Fi,
|
|
88
|
+
Tailscale or your own relay, and both directions work: control the PC from your phone, and let EDITH control the
|
|
89
|
+
phone — 47 tools in 11 access groups you can switch off one by one: apps, notifications
|
|
90
|
+
(WhatsApp/Telegram/Messenger/Gmail), the SMS inbox and call log, calls, contacts, calendar,
|
|
91
|
+
weather, location, media playback, brightness, ringer, torch, DND, files (both ways between
|
|
92
|
+
phone and PC), Home Assistant, and **any app's screen** through the accessibility service
|
|
93
|
+
(`phone_drive_screen`). She can **look** through the camera or at the screen, remembers facts
|
|
94
|
+
(synced with the PC's memory), runs your **routines**, delivers a **morning briefing** and
|
|
95
|
+
keeps an **activity log** of everything she does on the phone. Confirm-gated actions show up as
|
|
96
|
+
**Yes / No** cards. The app has its own brain (your OpenAI key, encrypted on the phone), so chat,
|
|
97
|
+
**Live** voice (Realtime API), the **"Edith" wake word** (on-device Vosk, like the PC), the
|
|
98
|
+
Quick Settings tile, widget and assistant gesture, phone tasks and **auto-reply** keep working
|
|
99
|
+
with the PC off; linking the PC adds its tools and its budget. Share anything to EDITH from any
|
|
100
|
+
app's share sheet. The app is a port of the desktop Halo Reactor HUD:
|
|
101
|
+
|
|
102
|
+
<p align="center">
|
|
103
|
+
<img src="docs/phone-hud-idle.png" width="220" alt="EDITH phone HUD standing by">
|
|
104
|
+
<img src="docs/phone-hud-working.png" width="220" alt="EDITH phone HUD working">
|
|
105
|
+
<img src="docs/phone-hud-approval.png" width="220" alt="EDITH phone HUD approval card">
|
|
106
|
+
</p>
|
|
107
|
+
|
|
108
|
+
```powershell
|
|
109
|
+
pip install -r requirements.txt # adds fastapi + uvicorn
|
|
110
|
+
python -m edith.service # prints the address + pairing token for the app
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
To have it running whenever the PC is on, register it once as a hidden logon task that
|
|
114
|
+
restarts itself after a crash (its output then goes to `~/.edith.log`; the token is in
|
|
115
|
+
`~/.edith_service_token`):
|
|
116
|
+
|
|
117
|
+
```powershell
|
|
118
|
+
powershell -ExecutionPolicy Bypass -File scripts\install_autostart.ps1 -Start # -Status / -Uninstall
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Then build/install the app (see [android/README.md](android/README.md)), enter the address
|
|
122
|
+
and token, tap Connect. Never port-forward the service. From outside your Wi-Fi, either run
|
|
123
|
+
your own relay, a small server both ends connect out to, end-to-end encrypted with the
|
|
124
|
+
pairing token ([docs/RELAY.md](docs/RELAY.md)), or use Tailscale
|
|
125
|
+
([android/README.md → Reach the PC from anywhere](android/README.md#reach-the-pc-from-anywhere-tailscale)).
|
|
126
|
+
Design notes: [docs/MOBILE.md](docs/MOBILE.md).
|
|
127
|
+
|
|
128
|
+
**Or run the brain on your own server** ([docs/SERVER.md](docs/SERVER.md)): `python -m edith.service` with
|
|
129
|
+
`EDITH_ROLE=server` on a small Linux VPS behind Caddy gives you EDITH at your own domain, a browser HUD (sign in with Google Authenticator),
|
|
130
|
+
the phone working with the PC off, and the OpenAI key living only on the server. The PC links to it as a
|
|
131
|
+
body (`python -m edith.body`), so its tools work from anywhere while it is on. Any other PC needs no code at
|
|
132
|
+
all: install **EDITH for Windows** (`EDITH-Setup.exe` from a `pc-v*` release), or with Python
|
|
133
|
+
`pip install edith-pc`, and link it with a six-digit code from
|
|
134
|
+
Google Authenticator (`edith link`).
|
|
135
|
+
|
|
136
|
+
## ⚙️ Configuration
|
|
137
|
+
|
|
138
|
+
All config lives in `.env` (see `.env.example`):
|
|
139
|
+
|
|
140
|
+
| Key | Default | What it does |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `OPENAI_API_KEY` | — | **Required.** OpenAI key. |
|
|
143
|
+
| `PICOVOICE_ACCESS_KEY` | — | Optional legacy wake-word engine (Vosk is the keyless default; Picovoice now needs a paid plan). |
|
|
144
|
+
| `EDITH_MODEL` | `gpt-realtime-2.1-mini` | `gpt-realtime-2.1` (best) / `gpt-realtime-2.1-mini` (cheap, for dev). The older `gpt-realtime-mini` shuts down 2027-01-20. |
|
|
145
|
+
| `EDITH_VOICE` | `marin` | Spoken voice (EDITH default; `cedar` for the old deep voice). |
|
|
146
|
+
| `EDITH_SESSION_MAX_SECONDS` | `300` | Hard auto-disconnect per session. |
|
|
147
|
+
| `EDITH_IDLE_TIMEOUT_SECONDS` | `30` | Continuous conversation: seconds of silence before going back to sleep (0 = off). |
|
|
148
|
+
| `EDITH_DAILY_BUDGET_USD` | `2.0` | Hard stop once a day's spend crosses this (includes delegated model calls). |
|
|
149
|
+
| `EDITH_HALF_DUPLEX` | `true` | Mutes the mic while EDITH speaks (stops it hearing itself on laptop speakers). Set `false` + use headphones for true barge-in. |
|
|
150
|
+
| `EDITH_DELEGATE_MODEL` | `gpt-6-sol` | The reasoning model behind `delegate_task` (`gpt-6-luna` is ~20x cheaper, `gpt-6-astra` the flagship). |
|
|
151
|
+
| `EDITH_VISION_MODEL` / `EDITH_DOC_MODEL` / `EDITH_SEARCH_MODEL` | `gpt-6-luna` | The cheap vision/text model behind `see_screen`, `read_document` and `web_search`. |
|
|
152
|
+
| `EDITH_AGENT_*` | see `.env.example` | Computer-autonomy model, step/time/cost caps, monitor. |
|
|
153
|
+
| `EDITH_BRIEFING*` | see `.env.example` | Morning briefing (on/off, city, news, nudge time). |
|
|
154
|
+
| `EDITH_SERVICE_*` / `EDITH_PHONE_DENYLIST` / `EDITH_INBOX_DIR` | see `.env.example` | Phone link: host/port/pairing token, extra protected phone apps, where phone uploads land (default `~/Downloads/EDITH`). |
|
|
155
|
+
| `EDITH_RELAY_URL` | — | Your own relay, e.g. `wss://relay.example.com/?key=…`: the phone reaches the PC from anywhere ([docs/RELAY.md](docs/RELAY.md)). |
|
|
156
|
+
| Integration keys | see `.env.example` | Spotify, Telegram, Home Assistant, Google, WhatsApp, health watch. |
|
|
157
|
+
|
|
158
|
+
### 🖥️ Desktop HUD options
|
|
159
|
+
|
|
160
|
+
The redesigned HUD (`python -m edith.gui`) is **frameless** by default (custom title bar, drag-to-move) and auto-scales to fit your screen. Tweak it with env vars:
|
|
161
|
+
|
|
162
|
+
| Env var | Default | What it does |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `EDITH_NATIVE_FRAME` | `0` | Set `1` to keep the standard OS window (title bar, snap, minimize) instead of the frameless HUD. |
|
|
165
|
+
| `EDITH_TOPMOST` | `0` | Set `1` to float the HUD above other windows (desktop-widget mode). |
|
|
166
|
+
|
|
167
|
+
- **Fonts:** the wordmark/labels use bundled **Orbitron** + **Rajdhani** (SIL OFL, in `assets/fonts/`), registered at runtime — no system install needed. If they're missing, the HUD falls back to **Bahnschrift** / **Consolas** and still looks intentional.
|
|
168
|
+
- **Escape hatch:** if the new HUD ever misbehaves on a machine, `python -m edith.gui_classic` runs the original UI (same brain, identical behaviour).
|
|
169
|
+
|
|
170
|
+
## 💸 Cost notes ($70 of credit goes a long way)
|
|
171
|
+
|
|
172
|
+
Realtime is billed by **audio tokens**, not flat per-minute. With prompt caching + trimmed tool outputs, expect **~$0.05–0.10/min → roughly 12–23 hours** of live conversation on $70. The built-in guardrails (`session_manager.py`) keep you there:
|
|
173
|
+
|
|
174
|
+
- The **system prompt + tool schemas are byte-stable** → prompt cache hits cut audio-input cost ~80%.
|
|
175
|
+
- Tool results are **trimmed** to only what the model needs before being sent back.
|
|
176
|
+
- A **live budget counter** reads each response's usage and hard-stops at `EDITH_DAILY_BUDGET_USD`.
|
|
177
|
+
- A **hard session timer** force-closes the socket (OpenAI does *not* auto-kill sessions).
|
|
178
|
+
- Develop against `gpt-realtime-2.1-mini`; switch to `gpt-realtime-2.1` only to record demos.
|
|
179
|
+
- Every model a tool can call has a row in `pricing.py`; an unknown id is metered at a mid-priced fallback.
|
|
180
|
+
|
|
181
|
+
> 🛡️ Backstop: also set a hard monthly spend limit in your OpenAI billing dashboard.
|
|
182
|
+
|
|
183
|
+
## 📁 Project layout
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
edith/
|
|
187
|
+
main.py entry: wake-word loop -> realtime session
|
|
188
|
+
config.py loads .env
|
|
189
|
+
legacy_migration.py one-time rename of pre-rebrand ~/.jarvis_* state files
|
|
190
|
+
prompts.py STABLE EDITH personality (kept byte-identical for caching)
|
|
191
|
+
pricing.py per-model token prices for the budget counter
|
|
192
|
+
realtime_client.py WebSocket transport, event loop, function-call dispatch
|
|
193
|
+
audio_io.py mic in / speaker out, barge-in flush, RMS for the orb
|
|
194
|
+
wake_word.py Vosk "Edith" listener (legacy Porcupine fallback)
|
|
195
|
+
session_manager.py hard timer + live token/$ budget guard
|
|
196
|
+
gui.py desktop HUD ("Halo Reactor"): bloom orb + glass UI + transcript + cost
|
|
197
|
+
gui_classic.py original HUD, kept as a guaranteed-working fallback
|
|
198
|
+
hud/ HUD render helpers: easing, PIL orb_renderer, textures, font loader
|
|
199
|
+
tools/
|
|
200
|
+
registry.py tool JSON schemas (cached prefix) + name->function dispatch
|
|
201
|
+
clock.py current time / date
|
|
202
|
+
weather.py Open-Meteo current conditions
|
|
203
|
+
web_search.py OpenAI web_search tool + DuckDuckGo fallback
|
|
204
|
+
delegate.py delegate_task: hard questions -> stronger reasoning model
|
|
205
|
+
computer_agent.py execute_computer_task: see->act->verify autonomy loop
|
|
206
|
+
memory.py semantic long-term memory (embeddings + cosine recall)
|
|
207
|
+
briefing.py morning briefing (weather + reminders + headlines)
|
|
208
|
+
news_finance.py news / stock quotes / currency (keyless APIs)
|
|
209
|
+
spotify_music.py play-by-name + now-playing
|
|
210
|
+
google_workspace.py Gmail + Calendar (one-time OAuth via scripts/setup_google.py)
|
|
211
|
+
home_assistant.py lights/switches/scenes by spoken name
|
|
212
|
+
documents.py read/summarize PDFs & text files aloud
|
|
213
|
+
notes.py dictated voice notes (per-topic Markdown)
|
|
214
|
+
telegram_notify.py push text to the user's phone
|
|
215
|
+
whatsapp.py prefilled WhatsApp messages (human presses send)
|
|
216
|
+
health_watch.py ambient battery/disk/CPU toasts
|
|
217
|
+
external_cost.py meters tool API spend into the session budget
|
|
218
|
+
reminders.py APScheduler reminders/timers + desktop notifications
|
|
219
|
+
system_control.py apps, volume, brightness, media keys, screenshots, YouTube
|
|
220
|
+
vision.py see_screen: capture + analyze + diagnose
|
|
221
|
+
pc_control.py keyboard/mouse, files/clipboard, windows, power, shell
|
|
222
|
+
window_control.py list/focus/minimize/maximize/close/snap windows by name
|
|
223
|
+
file_ops.py list, copy/move/rename, Recycle-Bin delete, zip/unzip
|
|
224
|
+
task_manager.py heaviest processes, kill by name (protected list)
|
|
225
|
+
system_toggles.py mic mute, monitors off, dark mode, recycle bin, network, wallpaper
|
|
226
|
+
llm.py shared Responses-API call helper (explicit reasoning effort)
|
|
227
|
+
service/ phone-app link (python -m edith.service): FastAPI + WebSocket,
|
|
228
|
+
text brain over ALL tools, approvals, phone_drive_screen agent
|
|
229
|
+
.github/workflows/ CI (tests on Windows + Linux, Android unit tests + debug APK), deploy of the
|
|
230
|
+
server brain on every green master, signed APK on every v* tag (docs/SERVER.md)
|
|
231
|
+
relay/ reach the PC from anywhere: the relay server (python -m edith.relay),
|
|
232
|
+
end-to-end encryption keyed by the pairing token, the PC-side bridge
|
|
233
|
+
web/static/ the browser HUD served by edith.service at / (chat, bodies, activity, approvals)
|
|
234
|
+
android/ the Android app (Kotlin): 47 phone tools, its own brain (works with the PC off),
|
|
235
|
+
live voice, wake word, accessibility agent, vision, auto-reply, PC link
|
|
236
|
+
scripts/ test_audio.py, test_tools_text.py, setup_google.py, install_autostart.ps1
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## 🗺️ Roadmap
|
|
240
|
+
|
|
241
|
+
- [x] Persistent reminders across restarts (SQLite jobstore)
|
|
242
|
+
- [x] Conversation memory / long-term notes (semantic memory + voice notes)
|
|
243
|
+
- [x] Smart-home (Home Assistant) tool
|
|
244
|
+
- [x] Hybrid reasoning (delegate to a stronger model)
|
|
245
|
+
- [x] Autonomous computer use (see → act → verify)
|
|
246
|
+
- [ ] Cross-platform (macOS/Linux) PC-control backends
|
|
247
|
+
- [ ] Telegram remote-control bridge (text commands to EDITH from your phone)
|
|
248
|
+
- [x] Android app v0.1: full phone control + PC remote, one brain two bodies ([android/](android/), [docs/MOBILE.md](docs/MOBILE.md))
|
|
249
|
+
- [x] Android app v0.2: phone-local brain (API key on the phone), auto-reply through notifications, hands-free follow-ups ([android/README.md](android/README.md#works-with-the-pc-off))
|
|
250
|
+
- [x] Android app v0.3: live voice (Realtime API), "Edith" wake word, tile / widget / assistant / headset entry points, camera and screen vision, SMS inbox and call log, media and device controls, Home Assistant, memory sync, routines, morning briefing, share sheet, file transfer, access switches and an activity log ([android/README.md](android/README.md#what-edith-can-do-on-the-phone))
|
|
251
|
+
- [x] Android app v0.4: reach the PC from anywhere through your own end-to-end-encrypted relay ([docs/RELAY.md](docs/RELAY.md))
|
|
252
|
+
- [x] Brain on your own server with a browser HUD and Google Authenticator sign-in; the PC links to it as a body, so the phone reaches PC tools from anywhere, with real approvals ([docs/SERVER.md](docs/SERVER.md))
|
|
253
|
+
- [ ] Sync of notes, reminders and routines across server, PC and phone; optional fallback key per body
|
|
254
|
+
- [ ] Phone app: GPT-Live / WebRTC transport, on-phone document reading
|
|
255
|
+
|
|
256
|
+
## 📝 License
|
|
257
|
+
|
|
258
|
+
MIT — see [LICENSE](LICENSE).
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
*Built with the OpenAI Realtime API. Not affiliated with Marvel.* 🦾
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""EDITH — a real-time desktop voice assistant built on the OpenAI Realtime API."""
|
|
2
|
+
|
|
3
|
+
import sys as _sys
|
|
4
|
+
|
|
5
|
+
# EDITH prints emoji/box-drawing status lines. On Windows those crash with
|
|
6
|
+
# UnicodeEncodeError when stdout is a non-UTF-8 pipe; force UTF-8 with replacement.
|
|
7
|
+
for _stream in (_sys.stdout, _sys.stderr):
|
|
8
|
+
try:
|
|
9
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
10
|
+
except Exception:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Microphone capture and speaker playback for the Realtime stream.
|
|
2
|
+
|
|
3
|
+
Design notes:
|
|
4
|
+
- The Realtime API speaks PCM16 / 24 kHz / mono. We capture and play in exactly
|
|
5
|
+
that format so there is no resampling.
|
|
6
|
+
- Capture runs in a sounddevice callback (a separate thread); we hand frames to the
|
|
7
|
+
asyncio loop via ``call_soon_threadsafe`` so the event loop is never blocked.
|
|
8
|
+
- Playback uses a callback that drains an internal byte buffer. ``flush_playback()``
|
|
9
|
+
clears that buffer instantly — this is how barge-in works: when the user starts
|
|
10
|
+
talking, we drop whatever EDITH was about to say.
|
|
11
|
+
- ``input_level`` / ``output_level`` (0..1 RMS) drive the GUI orb.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import threading
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import sounddevice as sd
|
|
20
|
+
|
|
21
|
+
from .config import BLOCK_FRAMES, CHANNELS, SAMPLE_RATE, SAMPLE_WIDTH
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _rms(pcm: bytes) -> float:
|
|
25
|
+
samples = np.frombuffer(pcm, dtype=np.int16)
|
|
26
|
+
if samples.size == 0:
|
|
27
|
+
return 0.0
|
|
28
|
+
return float(np.sqrt(np.mean((samples.astype(np.float32) / 32768.0) ** 2)))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _rms_arr(samples: np.ndarray) -> float:
|
|
32
|
+
if samples.size == 0:
|
|
33
|
+
return 0.0
|
|
34
|
+
return float(np.sqrt(np.mean((samples.astype(np.float32) / 32768.0) ** 2)))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AudioIO:
|
|
38
|
+
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
39
|
+
self._loop = loop
|
|
40
|
+
self.mic_queue: asyncio.Queue[bytes] = asyncio.Queue()
|
|
41
|
+
self._in_stream: sd.RawInputStream | None = None
|
|
42
|
+
self._out_stream: sd.RawOutputStream | None = None
|
|
43
|
+
self._play_buf = bytearray()
|
|
44
|
+
self._play_lock = threading.Lock()
|
|
45
|
+
self.muted = False
|
|
46
|
+
self.input_level = 0.0
|
|
47
|
+
self.output_level = 0.0
|
|
48
|
+
# latest int16 frames, kept for the GUI's live spectrum (read-only from GUI thread)
|
|
49
|
+
self._last_in: np.ndarray | None = None
|
|
50
|
+
self._last_out: np.ndarray | None = None
|
|
51
|
+
|
|
52
|
+
# --- microphone (input) ---------------------------------------------------
|
|
53
|
+
def _on_mic(self, indata, frames, time_info, status) -> None: # noqa: ANN001
|
|
54
|
+
data = bytes(indata)
|
|
55
|
+
samples = np.frombuffer(data, dtype=np.int16)
|
|
56
|
+
self._last_in = samples
|
|
57
|
+
self.input_level = _rms_arr(samples)
|
|
58
|
+
if self.muted:
|
|
59
|
+
return
|
|
60
|
+
try:
|
|
61
|
+
self._loop.call_soon_threadsafe(self.mic_queue.put_nowait, data)
|
|
62
|
+
except RuntimeError:
|
|
63
|
+
pass # event loop already closed
|
|
64
|
+
|
|
65
|
+
# --- speaker (output) -----------------------------------------------------
|
|
66
|
+
def _on_play(self, outdata, frames, time_info, status) -> None: # noqa: ANN001
|
|
67
|
+
needed = frames * CHANNELS * SAMPLE_WIDTH
|
|
68
|
+
with self._play_lock:
|
|
69
|
+
chunk = bytes(self._play_buf[:needed])
|
|
70
|
+
del self._play_buf[:needed]
|
|
71
|
+
if len(chunk) < needed: # underrun -> pad with silence
|
|
72
|
+
chunk += b"\x00" * (needed - len(chunk))
|
|
73
|
+
samples = np.frombuffer(chunk, dtype=np.int16)
|
|
74
|
+
self._last_out = samples
|
|
75
|
+
self.output_level = _rms_arr(samples)
|
|
76
|
+
outdata[:] = chunk
|
|
77
|
+
|
|
78
|
+
# --- live spectrum (for the GUI HUD) --------------------------------------
|
|
79
|
+
def latest_spectrum(self, bands: int = 24) -> np.ndarray:
|
|
80
|
+
"""Peak-normalized voice-band FFT magnitudes (0..1) of the most active stream."""
|
|
81
|
+
src = self._last_out if self.output_level >= self.input_level else self._last_in
|
|
82
|
+
if src is None or src.size == 0:
|
|
83
|
+
return np.zeros(bands, dtype=np.float32)
|
|
84
|
+
x = src.astype(np.float32) / 32768.0
|
|
85
|
+
windowed = x * np.hanning(x.size)
|
|
86
|
+
mag = np.abs(np.fft.rfft(windowed))
|
|
87
|
+
usable = mag[: max(1, mag.size // 2)] # lower half ~ voice energy
|
|
88
|
+
if usable.size < bands:
|
|
89
|
+
usable = np.pad(usable, (0, bands - usable.size))
|
|
90
|
+
grouped = np.array([float(g.mean()) for g in np.array_split(usable, bands)], dtype=np.float32)
|
|
91
|
+
peak = grouped.max()
|
|
92
|
+
return grouped / peak if peak > 0 else grouped
|
|
93
|
+
|
|
94
|
+
def play(self, pcm: bytes) -> None:
|
|
95
|
+
"""Queue assistant audio for playback."""
|
|
96
|
+
with self._play_lock:
|
|
97
|
+
self._play_buf.extend(pcm)
|
|
98
|
+
|
|
99
|
+
def flush_playback(self) -> None:
|
|
100
|
+
"""Drop all pending playback — called on barge-in to interrupt EDITH."""
|
|
101
|
+
with self._play_lock:
|
|
102
|
+
self._play_buf.clear()
|
|
103
|
+
self.output_level = 0.0
|
|
104
|
+
|
|
105
|
+
def is_speaking(self) -> bool:
|
|
106
|
+
with self._play_lock:
|
|
107
|
+
return len(self._play_buf) > 0
|
|
108
|
+
|
|
109
|
+
# --- lifecycle ------------------------------------------------------------
|
|
110
|
+
def start(self) -> None:
|
|
111
|
+
self._out_stream = sd.RawOutputStream(
|
|
112
|
+
samplerate=SAMPLE_RATE, channels=CHANNELS, dtype="int16",
|
|
113
|
+
blocksize=BLOCK_FRAMES, callback=self._on_play,
|
|
114
|
+
)
|
|
115
|
+
self._in_stream = sd.RawInputStream(
|
|
116
|
+
samplerate=SAMPLE_RATE, channels=CHANNELS, dtype="int16",
|
|
117
|
+
blocksize=BLOCK_FRAMES, callback=self._on_mic,
|
|
118
|
+
)
|
|
119
|
+
self._out_stream.start()
|
|
120
|
+
self._in_stream.start()
|
|
121
|
+
|
|
122
|
+
def stop(self) -> None:
|
|
123
|
+
for stream in (self._in_stream, self._out_stream):
|
|
124
|
+
if stream is not None:
|
|
125
|
+
try:
|
|
126
|
+
stream.stop()
|
|
127
|
+
stream.close()
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
self._in_stream = self._out_stream = None
|