invokerx commited on
Commit
fcdb282
·
verified ·
1 Parent(s): 41ce769

Package Reachy app with default Mac mini audio bridge

Browse files
.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=auto
 
17
  HERMESBODY_AUDIO_WS_HOST=127.0.0.1
18
  HERMESBODY_AUDIO_WS_PORT=8766
19
- HERMESBODY_AUDIO_WS_URL=ws://127.0.0.1:8766
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 transport-only 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`. Authentication uses `HERMESBODY_SAFE_BRIDGE_TOKEN` via bearer/header/query token or an initial `auth` message.
86
 
87
- Mac mini setup:
88
 
89
  ```bash
90
- cp .env.example .env
91
- # Set GEMINI_API_KEY and HERMESBODY_SAFE_BRIDGE_TOKEN in .env.
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
- For Reachy over a private LAN or Tailscale address, bind the server to that private interface instead of `127.0.0.1`. Keep OpenClaw itself on loopback.
99
 
100
- Reachy setup / smoke client:
101
 
102
  ```bash
103
- export HERMESBODY_SAFE_BRIDGE_TOKEN=replace-with-local-token
104
- hermesbody reachy-audio-client --server ws://MAC_MINI_PRIVATE_IP:8766 --token "$HERMESBODY_SAFE_BRIDGE_TOKEN"
 
 
 
 
 
105
  ```
106
 
107
- The Reachy client path now has two modes:
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
- ```bash
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
- 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.
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
- mkdir -p ~/.config/hermesbody
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
- 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.
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 is intentionally transport-only today. Actual robot mic/speaker adapters should 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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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", "auto"))
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("HERMESBODY_SAFE_BRIDGE_TOKEN or --token is required for the audio websocket server.")
 
 
 
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", "ws://127.0.0.1:8766"),
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", "auto"),
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", "auto")).strip().lower()
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", "auto"),
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 test_audio_backend_auto_prefers_gemini_ws_only_when_url_is_configured(monkeypatch):
23
  monkeypatch.delenv("HERMESBODY_AUDIO_WS_URL", raising=False)
24
- assert resolve_audio_backend("auto", "") == "openai_realtime"
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