Spaces:
Running
Running
Package Reachy app with default Mac mini audio bridge
Browse files- .env.example +3 -2
- README.md +18 -31
- src/hermesbody.egg-info/PKG-INFO +25 -1
- src/hermesbody/config.py +2 -2
- src/hermesbody/gemini_live.py +12 -4
- src/hermesbody/main.py +10 -8
- tests/test_cli.py +2 -2
- tests/test_gemini_live.py +15 -0
.env.example
CHANGED
|
@@ -13,10 +13,11 @@ HERMESBODY_GEMINI_OUTPUT_SAMPLE_RATE=24000
|
|
| 13 |
# Reachy <-> Mac mini PCM16 audio websocket.
|
| 14 |
# Bind the Mac mini server to loopback for local smoke tests or to a private
|
| 15 |
# LAN/Tailscale address when Reachy is the client. Use a real local token in .env.
|
| 16 |
-
HERMESBODY_AUDIO_BACKEND=
|
|
|
|
| 17 |
HERMESBODY_AUDIO_WS_HOST=127.0.0.1
|
| 18 |
HERMESBODY_AUDIO_WS_PORT=8766
|
| 19 |
-
HERMESBODY_AUDIO_WS_URL=ws://
|
| 20 |
|
| 21 |
# Required brain bridge selection.
|
| 22 |
# Supported: hermes_cli, openclaw_cli, openclaw_loopback
|
|
|
|
| 13 |
# Reachy <-> Mac mini PCM16 audio websocket.
|
| 14 |
# Bind the Mac mini server to loopback for local smoke tests or to a private
|
| 15 |
# LAN/Tailscale address when Reachy is the client. Use a real local token in .env.
|
| 16 |
+
HERMESBODY_AUDIO_BACKEND=gemini_ws
|
| 17 |
+
HERMESBODY_AUDIO_WS_ALLOW_UNAUTHENTICATED=true
|
| 18 |
HERMESBODY_AUDIO_WS_HOST=127.0.0.1
|
| 19 |
HERMESBODY_AUDIO_WS_PORT=8766
|
| 20 |
+
HERMESBODY_AUDIO_WS_URL=ws://10.0.0.192:8877
|
| 21 |
|
| 22 |
# Required brain bridge selection.
|
| 23 |
# Supported: hermes_cli, openclaw_cli, openclaw_loopback
|
README.md
CHANGED
|
@@ -82,53 +82,40 @@ The alternate **Reachy-local** mode is documented for simple audio wiring only.
|
|
| 82 |
|
| 83 |
## Audio WebSocket Transport
|
| 84 |
|
| 85 |
-
HermesBody includes a
|
| 86 |
|
| 87 |
-
|
| 88 |
|
| 89 |
```bash
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
export HERMESBODY_GEMINI_LIVE_ENABLED=true
|
| 93 |
-
export HERMESBODY_AUDIO_WS_HOST=127.0.0.1
|
| 94 |
-
export HERMESBODY_AUDIO_WS_PORT=8766
|
| 95 |
-
hermesbody gemini-server --host "$HERMESBODY_AUDIO_WS_HOST" --port "$HERMESBODY_AUDIO_WS_PORT"
|
| 96 |
```
|
| 97 |
|
| 98 |
-
|
| 99 |
|
| 100 |
-
|
| 101 |
|
| 102 |
```bash
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
```
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
- `hermesbody reachy-audio-client` is a transport smoke client that does not require robot hardware.
|
| 110 |
-
- The Reachy Dashboard/runtime app can use the same Mac mini bridge by setting:
|
| 111 |
|
| 112 |
-
``
|
| 113 |
-
export HERMESBODY_AUDIO_BACKEND=gemini_ws
|
| 114 |
-
export HERMESBODY_AUDIO_WS_URL=ws://MAC_MINI_PRIVATE_IP:8877
|
| 115 |
-
export HERMESBODY_SAFE_BRIDGE_TOKEN=replace-with-local-token
|
| 116 |
-
```
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
When running as an installed Reachy Dashboard app, HermesBody loads config from `~/.config/hermesbody/.env` in addition to source-tree `.env`, so robot-side app config can be installed with:
|
| 121 |
|
| 122 |
```bash
|
| 123 |
-
|
| 124 |
-
cat > ~/.config/hermesbody/.env <<'EOF'
|
| 125 |
-
HERMESBODY_AUDIO_BACKEND=gemini_ws
|
| 126 |
-
HERMESBODY_AUDIO_WS_URL=ws://MAC_MINI_PRIVATE_IP:8877
|
| 127 |
-
HERMESBODY_SAFE_BRIDGE_TOKEN=replace-with-local-token
|
| 128 |
-
EOF
|
| 129 |
```
|
| 130 |
|
| 131 |
-
|
| 132 |
|
| 133 |
## Safe Bridge API
|
| 134 |
|
|
|
|
| 82 |
|
| 83 |
## Audio WebSocket Transport
|
| 84 |
|
| 85 |
+
HermesBody includes a Reachy <-> Mac mini audio bridge. Messages are JSON with base64 PCM and the fields `type`, `audio`, `sample_rate`, `channels`, `sequence`, and `source`. Supported message types are `audio_input`, `audio_output`, `event`, `ping`, `pong`, and `error`.
|
| 86 |
|
| 87 |
+
For Karl's packaged deployment, the Reachy Dashboard app defaults to:
|
| 88 |
|
| 89 |
```bash
|
| 90 |
+
HERMESBODY_AUDIO_BACKEND=gemini_ws
|
| 91 |
+
HERMESBODY_AUDIO_WS_URL=ws://10.0.0.192:8877
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
```
|
| 93 |
|
| 94 |
+
That means the Dashboard install should work without editing files on Reachy: download the HF app, start it, and it connects to the Mac mini bridge at `ws://10.0.0.192:8877`.
|
| 95 |
|
| 96 |
+
Mac mini setup:
|
| 97 |
|
| 98 |
```bash
|
| 99 |
+
cp .env.example .env
|
| 100 |
+
# Set GEMINI_API_KEY in .env. Keep OpenClaw/Hermes loopback-only on the Mac.
|
| 101 |
+
export HERMESBODY_GEMINI_LIVE_ENABLED=true
|
| 102 |
+
export HERMESBODY_AUDIO_WS_ALLOW_UNAUTHENTICATED=true
|
| 103 |
+
export HERMESBODY_AUDIO_WS_HOST=10.0.0.192
|
| 104 |
+
export HERMESBODY_AUDIO_WS_PORT=8877
|
| 105 |
+
hermesbody gemini-server --host "$HERMESBODY_AUDIO_WS_HOST" --port "$HERMESBODY_AUDIO_WS_PORT"
|
| 106 |
```
|
| 107 |
|
| 108 |
+
`HERMESBODY_AUDIO_WS_ALLOW_UNAUTHENTICATED=true` is intentionally scoped only to this narrow audio bridge so the packaged Reachy app does not need a secret token. OpenClaw/Hermes itself remains loopback-only and is not exposed to LAN. If deploying outside Karl's private LAN/Tailscale setup, use `HERMESBODY_SAFE_BRIDGE_TOKEN` via bearer/header/query token or an initial `auth` message instead.
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
+
The app keeps the ClawBody-like body layer (motors, camera worker, movement, mic/speaker loops), skips direct OpenClaw and OpenAI Realtime on Reachy, sends mic frames to the Mac mini `gemini-server`, and pushes received Gemini PCM frames to Reachy's speaker.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
Optional smoke client without robot hardware:
|
|
|
|
|
|
|
| 113 |
|
| 114 |
```bash
|
| 115 |
+
hermesbody reachy-audio-client --server ws://10.0.0.192:8877
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
```
|
| 117 |
|
| 118 |
+
If defaults ever need to be overridden, an installed Reachy Dashboard app also loads `~/.config/hermesbody/.env`, but Karl's normal path should not require this.
|
| 119 |
|
| 120 |
## Safe Bridge API
|
| 121 |
|
src/hermesbody.egg-info/PKG-INFO
CHANGED
|
@@ -159,7 +159,31 @@ export HERMESBODY_SAFE_BRIDGE_TOKEN=replace-with-local-token
|
|
| 159 |
hermesbody reachy-audio-client --server ws://MAC_MINI_PRIVATE_IP:8766 --token "$HERMESBODY_SAFE_BRIDGE_TOKEN"
|
| 160 |
```
|
| 161 |
|
| 162 |
-
The Reachy client path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
## Safe Bridge API
|
| 165 |
|
|
|
|
| 159 |
hermesbody reachy-audio-client --server ws://MAC_MINI_PRIVATE_IP:8766 --token "$HERMESBODY_SAFE_BRIDGE_TOKEN"
|
| 160 |
```
|
| 161 |
|
| 162 |
+
The Reachy client path now has two modes:
|
| 163 |
+
|
| 164 |
+
- `hermesbody reachy-audio-client` is a transport smoke client that does not require robot hardware.
|
| 165 |
+
- The Reachy Dashboard/runtime app can use the same Mac mini bridge by setting:
|
| 166 |
+
|
| 167 |
+
```bash
|
| 168 |
+
export HERMESBODY_AUDIO_BACKEND=gemini_ws
|
| 169 |
+
export HERMESBODY_AUDIO_WS_URL=ws://MAC_MINI_PRIVATE_IP:8877
|
| 170 |
+
export HERMESBODY_SAFE_BRIDGE_TOKEN=replace-with-local-token
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
With those variables, the app keeps the ClawBody-like body layer (motors, camera worker, movement, mic/speaker loops) but skips direct OpenClaw and OpenAI Realtime on Reachy. Mic frames are sent to the Mac mini `gemini-server`; received Gemini PCM frames are pushed to Reachy's speaker. Without `HERMESBODY_AUDIO_WS_URL`, `HERMESBODY_AUDIO_BACKEND=auto` preserves the legacy OpenAI Realtime path.
|
| 174 |
+
|
| 175 |
+
When running as an installed Reachy Dashboard app, HermesBody loads config from `~/.config/hermesbody/.env` in addition to source-tree `.env`, so robot-side app config can be installed with:
|
| 176 |
+
|
| 177 |
+
```bash
|
| 178 |
+
mkdir -p ~/.config/hermesbody
|
| 179 |
+
cat > ~/.config/hermesbody/.env <<'EOF'
|
| 180 |
+
HERMESBODY_AUDIO_BACKEND=gemini_ws
|
| 181 |
+
HERMESBODY_AUDIO_WS_URL=ws://MAC_MINI_PRIVATE_IP:8877
|
| 182 |
+
HERMESBODY_SAFE_BRIDGE_TOKEN=replace-with-local-token
|
| 183 |
+
EOF
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
Actual robot mic/speaker adapters sit behind explicit hardware hooks that convert Reachy media samples to `AudioFrame` and play received `audio_output` frames; the websocket layer does not require the Reachy SDK in tests.
|
| 187 |
|
| 188 |
## Safe Bridge API
|
| 189 |
|
src/hermesbody/config.py
CHANGED
|
@@ -48,8 +48,8 @@ class Config:
|
|
| 48 |
HERMESBODY_GEMINI_OUTPUT_SAMPLE_RATE: int = field(
|
| 49 |
default_factory=lambda: int(os.getenv("HERMESBODY_GEMINI_OUTPUT_SAMPLE_RATE", "24000"))
|
| 50 |
)
|
| 51 |
-
HERMESBODY_AUDIO_BACKEND: str = field(default_factory=lambda: os.getenv("HERMESBODY_AUDIO_BACKEND", "
|
| 52 |
-
HERMESBODY_AUDIO_WS_URL: str = field(default_factory=lambda: os.getenv("HERMESBODY_AUDIO_WS_URL", ""))
|
| 53 |
|
| 54 |
# Brain bridge configuration. OpenClaw must remain loopback-only/private.
|
| 55 |
HERMESBODY_BRAIN_BACKEND: str = field(default_factory=lambda: os.getenv("HERMESBODY_BRAIN_BACKEND", "hermes_cli"))
|
|
|
|
| 48 |
HERMESBODY_GEMINI_OUTPUT_SAMPLE_RATE: int = field(
|
| 49 |
default_factory=lambda: int(os.getenv("HERMESBODY_GEMINI_OUTPUT_SAMPLE_RATE", "24000"))
|
| 50 |
)
|
| 51 |
+
HERMESBODY_AUDIO_BACKEND: str = field(default_factory=lambda: os.getenv("HERMESBODY_AUDIO_BACKEND", "gemini_ws"))
|
| 52 |
+
HERMESBODY_AUDIO_WS_URL: str = field(default_factory=lambda: os.getenv("HERMESBODY_AUDIO_WS_URL", "ws://10.0.0.192:8877"))
|
| 53 |
|
| 54 |
# Brain bridge configuration. OpenClaw must remain loopback-only/private.
|
| 55 |
HERMESBODY_BRAIN_BACKEND: str = field(default_factory=lambda: os.getenv("HERMESBODY_BRAIN_BACKEND", "hermes_cli"))
|
src/hermesbody/gemini_live.py
CHANGED
|
@@ -28,6 +28,7 @@ DEFAULT_OUTPUT_SAMPLE_RATE = 24000
|
|
| 28 |
VALID_AUDIO_PLACEMENTS = {"mac_mini_controller", "reachy_local"}
|
| 29 |
VALID_AUDIO_MESSAGE_TYPES = {"audio_input", "audio_output"}
|
| 30 |
VALID_TRANSPORT_MESSAGE_TYPES = VALID_AUDIO_MESSAGE_TYPES | {"event", "ping", "pong", "error", "auth"}
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
ASK_BRAIN_TOOL_SCHEMA: dict[str, Any] = {
|
|
@@ -303,6 +304,7 @@ class MacMiniGeminiAudioServer:
|
|
| 303 |
self.host = host
|
| 304 |
self.port = port
|
| 305 |
self.token = token or os.getenv("HERMESBODY_SAFE_BRIDGE_TOKEN")
|
|
|
|
| 306 |
self._event_cursor = 0
|
| 307 |
|
| 308 |
async def serve_forever(self) -> None:
|
|
@@ -310,8 +312,11 @@ class MacMiniGeminiAudioServer:
|
|
| 310 |
import websockets
|
| 311 |
except ImportError as exc:
|
| 312 |
raise RuntimeError("websockets package is required for HermesBody audio websocket server.") from exc
|
| 313 |
-
if not self.token:
|
| 314 |
-
raise RuntimeError(
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
async with websockets.serve(self.handler, self.host, self.port):
|
| 317 |
await asyncio.Future()
|
|
@@ -333,6 +338,8 @@ class MacMiniGeminiAudioServer:
|
|
| 333 |
pass
|
| 334 |
|
| 335 |
async def _authenticate(self, websocket: Any, path: str | None) -> str | Mapping[str, Any] | None:
|
|
|
|
|
|
|
| 336 |
# websockets <=10 passes (websocket, path) and exposes request_headers;
|
| 337 |
# websockets >=15 passes a ServerConnection with request.headers/path.
|
| 338 |
request = getattr(websocket, "request", None)
|
|
@@ -419,12 +426,13 @@ class ReachyAudioWebSocketClient:
|
|
| 419 |
self._websocket: Any | None = None
|
| 420 |
|
| 421 |
async def connect(self) -> None:
|
| 422 |
-
if not self.token:
|
| 423 |
-
raise RuntimeError("HERMESBODY_SAFE_BRIDGE_TOKEN or --token is required for the audio websocket client.")
|
| 424 |
try:
|
| 425 |
import websockets
|
| 426 |
except ImportError as exc:
|
| 427 |
raise RuntimeError("websockets package is required for HermesBody audio websocket client.") from exc
|
|
|
|
|
|
|
|
|
|
| 428 |
headers = {"Authorization": f"Bearer {self.token}"}
|
| 429 |
try:
|
| 430 |
self._websocket = await websockets.connect(self.server_url, extra_headers=headers)
|
|
|
|
| 28 |
VALID_AUDIO_PLACEMENTS = {"mac_mini_controller", "reachy_local"}
|
| 29 |
VALID_AUDIO_MESSAGE_TYPES = {"audio_input", "audio_output"}
|
| 30 |
VALID_TRANSPORT_MESSAGE_TYPES = VALID_AUDIO_MESSAGE_TYPES | {"event", "ping", "pong", "error", "auth"}
|
| 31 |
+
DEFAULT_MAC_MINI_AUDIO_WS_URL = "ws://10.0.0.192:8877"
|
| 32 |
|
| 33 |
|
| 34 |
ASK_BRAIN_TOOL_SCHEMA: dict[str, Any] = {
|
|
|
|
| 304 |
self.host = host
|
| 305 |
self.port = port
|
| 306 |
self.token = token or os.getenv("HERMESBODY_SAFE_BRIDGE_TOKEN")
|
| 307 |
+
self.allow_unauthenticated = os.getenv("HERMESBODY_AUDIO_WS_ALLOW_UNAUTHENTICATED", "false").lower() == "true"
|
| 308 |
self._event_cursor = 0
|
| 309 |
|
| 310 |
async def serve_forever(self) -> None:
|
|
|
|
| 312 |
import websockets
|
| 313 |
except ImportError as exc:
|
| 314 |
raise RuntimeError("websockets package is required for HermesBody audio websocket server.") from exc
|
| 315 |
+
if not self.token and not self.allow_unauthenticated:
|
| 316 |
+
raise RuntimeError(
|
| 317 |
+
"HERMESBODY_SAFE_BRIDGE_TOKEN/--token is required unless "
|
| 318 |
+
"HERMESBODY_AUDIO_WS_ALLOW_UNAUTHENTICATED=true."
|
| 319 |
+
)
|
| 320 |
|
| 321 |
async with websockets.serve(self.handler, self.host, self.port):
|
| 322 |
await asyncio.Future()
|
|
|
|
| 338 |
pass
|
| 339 |
|
| 340 |
async def _authenticate(self, websocket: Any, path: str | None) -> str | Mapping[str, Any] | None:
|
| 341 |
+
if self.allow_unauthenticated:
|
| 342 |
+
return None
|
| 343 |
# websockets <=10 passes (websocket, path) and exposes request_headers;
|
| 344 |
# websockets >=15 passes a ServerConnection with request.headers/path.
|
| 345 |
request = getattr(websocket, "request", None)
|
|
|
|
| 426 |
self._websocket: Any | None = None
|
| 427 |
|
| 428 |
async def connect(self) -> None:
|
|
|
|
|
|
|
| 429 |
try:
|
| 430 |
import websockets
|
| 431 |
except ImportError as exc:
|
| 432 |
raise RuntimeError("websockets package is required for HermesBody audio websocket client.") from exc
|
| 433 |
+
if not self.token:
|
| 434 |
+
self._websocket = await websockets.connect(self.server_url)
|
| 435 |
+
return
|
| 436 |
headers = {"Authorization": f"Bearer {self.token}"}
|
| 437 |
try:
|
| 438 |
self._websocket = await websockets.connect(self.server_url, extra_headers=headers)
|
src/hermesbody/main.py
CHANGED
|
@@ -28,6 +28,8 @@ from typing import Any, Optional
|
|
| 28 |
|
| 29 |
import numpy as np
|
| 30 |
|
|
|
|
|
|
|
| 31 |
try:
|
| 32 |
from dotenv import load_dotenv
|
| 33 |
except ImportError:
|
|
@@ -112,7 +114,7 @@ Examples:
|
|
| 112 |
)
|
| 113 |
reachy_client.add_argument(
|
| 114 |
"--server",
|
| 115 |
-
default=os.getenv("HERMESBODY_AUDIO_WS_URL",
|
| 116 |
help="Mac mini audio websocket URL",
|
| 117 |
)
|
| 118 |
reachy_client.add_argument("--token", default=os.getenv("HERMESBODY_SAFE_BRIDGE_TOKEN"))
|
|
@@ -173,12 +175,12 @@ Examples:
|
|
| 173 |
parser.add_argument(
|
| 174 |
"--audio-backend",
|
| 175 |
choices=["auto", "openai_realtime", "gemini_ws"],
|
| 176 |
-
default=os.getenv("HERMESBODY_AUDIO_BACKEND", "
|
| 177 |
help="Audio runtime: legacy OpenAI Realtime or Reachy->Mac mini Gemini WebSocket bridge",
|
| 178 |
)
|
| 179 |
parser.add_argument(
|
| 180 |
"--audio-ws-url",
|
| 181 |
-
default=os.getenv("HERMESBODY_AUDIO_WS_URL",
|
| 182 |
help="Mac mini Gemini audio WebSocket URL for --audio-backend gemini_ws",
|
| 183 |
)
|
| 184 |
parser.add_argument(
|
|
@@ -204,10 +206,10 @@ def resolve_audio_backend(audio_backend: str | None = None, audio_ws_url: str |
|
|
| 204 |
Gemini by setting only HERMESBODY_AUDIO_WS_URL/HERMESBODY_AUDIO_BACKEND.
|
| 205 |
"""
|
| 206 |
|
| 207 |
-
requested = (audio_backend or os.getenv("HERMESBODY_AUDIO_BACKEND", "
|
| 208 |
if requested not in {"auto", "openai_realtime", "gemini_ws"}:
|
| 209 |
requested = "auto"
|
| 210 |
-
ws_url = audio_ws_url or os.getenv("HERMESBODY_AUDIO_WS_URL") or
|
| 211 |
if requested == "auto":
|
| 212 |
return "gemini_ws" if ws_url else "openai_realtime"
|
| 213 |
return requested
|
|
@@ -320,7 +322,7 @@ class HermesBodyCore:
|
|
| 320 |
self.gateway_url = ensure_loopback_url(gateway_url)
|
| 321 |
self._external_stop_event = external_stop_event
|
| 322 |
self._owns_robot = robot is None
|
| 323 |
-
self.audio_ws_url = audio_ws_url or os.getenv("HERMESBODY_AUDIO_WS_URL") or
|
| 324 |
self.audio_token = audio_token or os.getenv("HERMESBODY_SAFE_BRIDGE_TOKEN")
|
| 325 |
self.audio_backend = resolve_audio_backend(audio_backend, self.audio_ws_url)
|
| 326 |
self.audio_client = None
|
|
@@ -756,8 +758,8 @@ class HermesBodyApp:
|
|
| 756 |
gateway_url=gateway_url,
|
| 757 |
robot=reachy_mini,
|
| 758 |
external_stop_event=stop_event,
|
| 759 |
-
audio_backend=os.getenv("HERMESBODY_AUDIO_BACKEND", "
|
| 760 |
-
audio_ws_url=os.getenv("HERMESBODY_AUDIO_WS_URL",
|
| 761 |
audio_token=os.getenv("HERMESBODY_SAFE_BRIDGE_TOKEN"),
|
| 762 |
)
|
| 763 |
|
|
|
|
| 28 |
|
| 29 |
import numpy as np
|
| 30 |
|
| 31 |
+
from hermesbody.gemini_live import DEFAULT_MAC_MINI_AUDIO_WS_URL
|
| 32 |
+
|
| 33 |
try:
|
| 34 |
from dotenv import load_dotenv
|
| 35 |
except ImportError:
|
|
|
|
| 114 |
)
|
| 115 |
reachy_client.add_argument(
|
| 116 |
"--server",
|
| 117 |
+
default=os.getenv("HERMESBODY_AUDIO_WS_URL", DEFAULT_MAC_MINI_AUDIO_WS_URL),
|
| 118 |
help="Mac mini audio websocket URL",
|
| 119 |
)
|
| 120 |
reachy_client.add_argument("--token", default=os.getenv("HERMESBODY_SAFE_BRIDGE_TOKEN"))
|
|
|
|
| 175 |
parser.add_argument(
|
| 176 |
"--audio-backend",
|
| 177 |
choices=["auto", "openai_realtime", "gemini_ws"],
|
| 178 |
+
default=os.getenv("HERMESBODY_AUDIO_BACKEND", "gemini_ws"),
|
| 179 |
help="Audio runtime: legacy OpenAI Realtime or Reachy->Mac mini Gemini WebSocket bridge",
|
| 180 |
)
|
| 181 |
parser.add_argument(
|
| 182 |
"--audio-ws-url",
|
| 183 |
+
default=os.getenv("HERMESBODY_AUDIO_WS_URL", DEFAULT_MAC_MINI_AUDIO_WS_URL),
|
| 184 |
help="Mac mini Gemini audio WebSocket URL for --audio-backend gemini_ws",
|
| 185 |
)
|
| 186 |
parser.add_argument(
|
|
|
|
| 206 |
Gemini by setting only HERMESBODY_AUDIO_WS_URL/HERMESBODY_AUDIO_BACKEND.
|
| 207 |
"""
|
| 208 |
|
| 209 |
+
requested = (audio_backend or os.getenv("HERMESBODY_AUDIO_BACKEND", "gemini_ws")).strip().lower()
|
| 210 |
if requested not in {"auto", "openai_realtime", "gemini_ws"}:
|
| 211 |
requested = "auto"
|
| 212 |
+
ws_url = audio_ws_url or os.getenv("HERMESBODY_AUDIO_WS_URL") or DEFAULT_MAC_MINI_AUDIO_WS_URL
|
| 213 |
if requested == "auto":
|
| 214 |
return "gemini_ws" if ws_url else "openai_realtime"
|
| 215 |
return requested
|
|
|
|
| 322 |
self.gateway_url = ensure_loopback_url(gateway_url)
|
| 323 |
self._external_stop_event = external_stop_event
|
| 324 |
self._owns_robot = robot is None
|
| 325 |
+
self.audio_ws_url = audio_ws_url or os.getenv("HERMESBODY_AUDIO_WS_URL") or DEFAULT_MAC_MINI_AUDIO_WS_URL
|
| 326 |
self.audio_token = audio_token or os.getenv("HERMESBODY_SAFE_BRIDGE_TOKEN")
|
| 327 |
self.audio_backend = resolve_audio_backend(audio_backend, self.audio_ws_url)
|
| 328 |
self.audio_client = None
|
|
|
|
| 758 |
gateway_url=gateway_url,
|
| 759 |
robot=reachy_mini,
|
| 760 |
external_stop_event=stop_event,
|
| 761 |
+
audio_backend=os.getenv("HERMESBODY_AUDIO_BACKEND", "gemini_ws"),
|
| 762 |
+
audio_ws_url=os.getenv("HERMESBODY_AUDIO_WS_URL", DEFAULT_MAC_MINI_AUDIO_WS_URL),
|
| 763 |
audio_token=os.getenv("HERMESBODY_SAFE_BRIDGE_TOKEN"),
|
| 764 |
)
|
| 765 |
|
tests/test_cli.py
CHANGED
|
@@ -19,9 +19,9 @@ def test_cli_parser_exposes_audio_transport_subcommands_without_optional_imports
|
|
| 19 |
assert client.server == "ws://127.0.0.1:8766"
|
| 20 |
|
| 21 |
|
| 22 |
-
def
|
| 23 |
monkeypatch.delenv("HERMESBODY_AUDIO_WS_URL", raising=False)
|
| 24 |
-
assert resolve_audio_backend("auto", "") == "
|
| 25 |
assert resolve_audio_backend("auto", "ws://10.0.0.192:8877") == "gemini_ws"
|
| 26 |
assert resolve_audio_backend("gemini_ws", "") == "gemini_ws"
|
| 27 |
|
|
|
|
| 19 |
assert client.server == "ws://127.0.0.1:8766"
|
| 20 |
|
| 21 |
|
| 22 |
+
def test_audio_backend_auto_defaults_to_packaged_mac_mini_bridge(monkeypatch):
|
| 23 |
monkeypatch.delenv("HERMESBODY_AUDIO_WS_URL", raising=False)
|
| 24 |
+
assert resolve_audio_backend("auto", "") == "gemini_ws"
|
| 25 |
assert resolve_audio_backend("auto", "ws://10.0.0.192:8877") == "gemini_ws"
|
| 26 |
assert resolve_audio_backend("gemini_ws", "") == "gemini_ws"
|
| 27 |
|
tests/test_gemini_live.py
CHANGED
|
@@ -281,6 +281,21 @@ async def _audio_server_auth_rejects_missing_or_wrong_token():
|
|
| 281 |
assert "auth_failed" in wrong.sent[0]
|
| 282 |
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
def test_audio_server_processes_one_audio_input_and_returns_output_and_event():
|
| 285 |
asyncio.run(_audio_server_processes_one_audio_input_and_returns_output_and_event())
|
| 286 |
|
|
|
|
| 281 |
assert "auth_failed" in wrong.sent[0]
|
| 282 |
|
| 283 |
|
| 284 |
+
def test_audio_server_can_allow_unauthenticated_packaged_reachy_client(monkeypatch):
|
| 285 |
+
asyncio.run(_audio_server_can_allow_unauthenticated_packaged_reachy_client(monkeypatch))
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
async def _audio_server_can_allow_unauthenticated_packaged_reachy_client(monkeypatch):
|
| 289 |
+
monkeypatch.setenv("HERMESBODY_AUDIO_WS_ALLOW_UNAUTHENTICATED", "true")
|
| 290 |
+
server = MacMiniGeminiAudioServer(FakeAudioController(), token="good-token")
|
| 291 |
+
socket = FakeSocket()
|
| 292 |
+
|
| 293 |
+
pending = await server._authenticate(socket, socket.path)
|
| 294 |
+
|
| 295 |
+
assert pending is None
|
| 296 |
+
assert socket.sent == []
|
| 297 |
+
|
| 298 |
+
|
| 299 |
def test_audio_server_processes_one_audio_input_and_returns_output_and_event():
|
| 300 |
asyncio.run(_audio_server_processes_one_audio_input_and_returns_output_and_event())
|
| 301 |
|