Spaces:
Running
Running
Suppress Gemini mic before output audio arrives
Browse files- src/hermesbody/gemini_live.py +31 -1
- src/hermesbody/main.py +6 -0
src/hermesbody/gemini_live.py
CHANGED
|
@@ -16,6 +16,7 @@ import logging
|
|
| 16 |
import math
|
| 17 |
import os
|
| 18 |
import secrets
|
|
|
|
| 19 |
from collections.abc import AsyncIterator, Mapping
|
| 20 |
from dataclasses import dataclass, field
|
| 21 |
from pathlib import Path
|
|
@@ -347,6 +348,8 @@ class MacMiniGeminiAudioServer:
|
|
| 347 |
self._audio_bytes_received = 0
|
| 348 |
self._provider_events_sent = 0
|
| 349 |
self._output_audio_events_sent = 0
|
|
|
|
|
|
|
| 350 |
|
| 351 |
async def serve_forever(self) -> None:
|
| 352 |
try:
|
|
@@ -446,6 +449,20 @@ class MacMiniGeminiAudioServer:
|
|
| 446 |
peak,
|
| 447 |
self._audio_bytes_received,
|
| 448 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
try:
|
| 450 |
returned = await _maybe_await(self.controller.send_audio_frame(frame))
|
| 451 |
except Exception as exc:
|
|
@@ -493,13 +510,26 @@ class MacMiniGeminiAudioServer:
|
|
| 493 |
self._provider_events_sent += 1
|
| 494 |
if event.type == "output_audio" and event.frame is not None:
|
| 495 |
self._output_audio_events_sent += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
logger.info(
|
| 497 |
-
"Mac audio bridge provider event client #%s type=output_audio bytes=%d sr=%d",
|
| 498 |
client_id,
|
| 499 |
len(event.frame.pcm),
|
| 500 |
event.frame.sample_rate,
|
|
|
|
| 501 |
)
|
| 502 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
logger.info(
|
| 504 |
"Mac audio bridge provider event client #%s type=%s text=%r reason=%r data=%s",
|
| 505 |
client_id,
|
|
|
|
| 16 |
import math
|
| 17 |
import os
|
| 18 |
import secrets
|
| 19 |
+
import time
|
| 20 |
from collections.abc import AsyncIterator, Mapping
|
| 21 |
from dataclasses import dataclass, field
|
| 22 |
from pathlib import Path
|
|
|
|
| 348 |
self._audio_bytes_received = 0
|
| 349 |
self._provider_events_sent = 0
|
| 350 |
self._output_audio_events_sent = 0
|
| 351 |
+
self._suppress_audio_input_until = 0.0
|
| 352 |
+
self._suppressed_audio_input_frames = 0
|
| 353 |
|
| 354 |
async def serve_forever(self) -> None:
|
| 355 |
try:
|
|
|
|
| 449 |
peak,
|
| 450 |
self._audio_bytes_received,
|
| 451 |
)
|
| 452 |
+
now = time.monotonic()
|
| 453 |
+
if now < self._suppress_audio_input_until:
|
| 454 |
+
self._suppressed_audio_input_frames += 1
|
| 455 |
+
if self._suppressed_audio_input_frames <= 5 or self._suppressed_audio_input_frames % 100 == 0:
|
| 456 |
+
remaining_ms = int((self._suppress_audio_input_until - now) * 1000)
|
| 457 |
+
logger.info(
|
| 458 |
+
"Mac audio bridge suppressing input during Gemini output client #%s frame=%d remaining_ms=%d rms=%.5f peak=%.5f",
|
| 459 |
+
client_id,
|
| 460 |
+
self._suppressed_audio_input_frames,
|
| 461 |
+
remaining_ms,
|
| 462 |
+
rms,
|
| 463 |
+
peak,
|
| 464 |
+
)
|
| 465 |
+
return
|
| 466 |
try:
|
| 467 |
returned = await _maybe_await(self.controller.send_audio_frame(frame))
|
| 468 |
except Exception as exc:
|
|
|
|
| 510 |
self._provider_events_sent += 1
|
| 511 |
if event.type == "output_audio" and event.frame is not None:
|
| 512 |
self._output_audio_events_sent += 1
|
| 513 |
+
playback_seconds = len(event.frame.pcm) / float((event.frame.sample_rate or DEFAULT_OUTPUT_SAMPLE_RATE) * max(event.frame.channels, 1) * 2)
|
| 514 |
+
self._suppress_audio_input_until = max(
|
| 515 |
+
self._suppress_audio_input_until,
|
| 516 |
+
time.monotonic() + playback_seconds + 0.35,
|
| 517 |
+
)
|
| 518 |
logger.info(
|
| 519 |
+
"Mac audio bridge provider event client #%s type=output_audio bytes=%d sr=%d suppress_ms=%d",
|
| 520 |
client_id,
|
| 521 |
len(event.frame.pcm),
|
| 522 |
event.frame.sample_rate,
|
| 523 |
+
int((self._suppress_audio_input_until - time.monotonic()) * 1000),
|
| 524 |
)
|
| 525 |
else:
|
| 526 |
+
if event.type in {"output_text", "output_transcription"}:
|
| 527 |
+
self._suppress_audio_input_until = max(
|
| 528 |
+
self._suppress_audio_input_until,
|
| 529 |
+
time.monotonic() + 2.0,
|
| 530 |
+
)
|
| 531 |
+
if event.type in {"turn_complete"}:
|
| 532 |
+
self._suppress_audio_input_until = min(self._suppress_audio_input_until, time.monotonic() + 0.25)
|
| 533 |
logger.info(
|
| 534 |
"Mac audio bridge provider event client #%s type=%s text=%r reason=%r data=%s",
|
| 535 |
client_id,
|
src/hermesbody/main.py
CHANGED
|
@@ -639,6 +639,12 @@ class HermesBodyCore:
|
|
| 639 |
self.movement_manager.set_processing(False)
|
| 640 |
if self.head_wobbler is not None:
|
| 641 |
self.head_wobbler.reset()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
elif message.type == "brain_start":
|
| 643 |
self.movement_manager.set_processing(True)
|
| 644 |
logger.info("Gemini brain call started: %s", message.text or message.data)
|
|
|
|
| 639 |
self.movement_manager.set_processing(False)
|
| 640 |
if self.head_wobbler is not None:
|
| 641 |
self.head_wobbler.reset()
|
| 642 |
+
elif message.type in {"output_text", "output_transcription"}:
|
| 643 |
+
# Gemini can emit text/transcription slightly before audio frames arrive.
|
| 644 |
+
# Close the mic immediately so the tail of the user's utterance or local
|
| 645 |
+
# speaker output doesn't interrupt the model before TTS reaches Reachy.
|
| 646 |
+
self._suppress_mic_until = max(self._suppress_mic_until, time.monotonic() + 2.0)
|
| 647 |
+
logger.info("Gemini output started; suppressing mic briefly: %s", message.text or message.data)
|
| 648 |
elif message.type == "brain_start":
|
| 649 |
self.movement_manager.set_processing(True)
|
| 650 |
logger.info("Gemini brain call started: %s", message.text or message.data)
|