Alina Lozovskaya commited on
Commit
d112528
·
2 Parent(s): b0cb5ad eaa4ab7

Merge branch 'develop' into 49-improve-readme

Browse files
.gitignore CHANGED
@@ -79,6 +79,9 @@ coverage.xml
79
  .pytest_cache/
80
  cover/
81
 
 
 
 
82
  # Translations
83
  *.mo
84
  *.pot
 
79
  .pytest_cache/
80
  cover/
81
 
82
+ # Ruff cache
83
+ .ruff_cache/
84
+
85
  # Translations
86
  *.mo
87
  *.pot
README.md CHANGED
@@ -93,9 +93,10 @@ The app starts a Gradio UI served locally (http://127.0.0.1:7860/). When running
93
  |--------|---------|-------------|
94
  | `--head-tracker {yolo,mediapipe}` | `None` | Select a face-tracking backend when a camera is available. Requires the matching optional extra. |
95
  | `--no-camera` | `False` | Run without camera capture or face tracking. |
96
- | `--headless` | `False` | Suppress launching the Gradio UI (useful on remote machines). |
97
  | `--debug` | `False` | Enable verbose logging for troubleshooting. |
98
 
 
99
  ### Examples
100
  - Run on hardware with MediaPipe face tracking:
101
 
 
93
  |--------|---------|-------------|
94
  | `--head-tracker {yolo,mediapipe}` | `None` | Select a face-tracking backend when a camera is available. Requires the matching optional extra. |
95
  | `--no-camera` | `False` | Run without camera capture or face tracking. |
96
+ | `--gradio` | `False` | Launch the Gradio web UI. Without this flag, runs in console mode. Required when running in simulation mode. |
97
  | `--debug` | `False` | Enable verbose logging for troubleshooting. |
98
 
99
+
100
  ### Examples
101
  - Run on hardware with MediaPipe face tracking:
102
 
pyproject.toml CHANGED
@@ -16,7 +16,6 @@ dependencies = [
16
  "gradio>=5.49.0",
17
  "huggingface_hub>=0.34.4",
18
  "opencv-python>=4.12.0.88",
19
- "PyGObject>=3.42.2,<=3.46.0",
20
 
21
  #Environment variables
22
  "python-dotenv",
@@ -88,4 +87,4 @@ split-on-trailing-comma = true
88
  quote-style = "double"
89
  indent-style = "space"
90
  skip-magic-trailing-comma = false
91
- line-ending = "auto"
 
16
  "gradio>=5.49.0",
17
  "huggingface_hub>=0.34.4",
18
  "opencv-python>=4.12.0.88",
 
19
 
20
  #Environment variables
21
  "python-dotenv",
 
87
  quote-style = "double"
88
  indent-style = "space"
89
  skip-magic-trailing-comma = false
90
+ line-ending = "auto"
src/reachy_mini_conversation_demo/audio/gstreamer.py DELETED
@@ -1,214 +0,0 @@
1
- import logging
2
- from typing import Optional
3
- from threading import Thread
4
-
5
- import gi
6
-
7
-
8
- gi.require_version("Gst", "1.0")
9
- gi.require_version("GstApp", "1.0")
10
- from gi.repository import Gst, GLib # noqa: E402
11
-
12
-
13
- class GstPlayer:
14
- """Audio player using GStreamer."""
15
-
16
- def __init__(self, sample_rate: int = 24000, device_name: Optional[str] = None):
17
- """Initialize player."""
18
- self._logger = logging.getLogger(__name__)
19
- Gst.init(None)
20
- self._loop = GLib.MainLoop()
21
- self._thread_bus_calls: Optional[Thread] = None
22
-
23
- self.pipeline = Gst.Pipeline.new("audio_player")
24
-
25
- # Create elements
26
- self.appsrc = Gst.ElementFactory.make("appsrc", None)
27
- self.appsrc.set_property("format", Gst.Format.TIME)
28
- self.appsrc.set_property("is-live", True)
29
- caps = Gst.Caps.from_string(f"audio/x-raw,format=S16LE,channels=1,rate={sample_rate},layout=interleaved")
30
- self.appsrc.set_property("caps", caps)
31
- queue = Gst.ElementFactory.make("queue")
32
- audioconvert = Gst.ElementFactory.make("audioconvert")
33
- audioresample = Gst.ElementFactory.make("audioresample")
34
-
35
- # Try to pin specific output device; fallback to autoaudiosink
36
- audiosink = _create_device_element(direction="sink", name_substr=device_name) or Gst.ElementFactory.make(
37
- "autoaudiosink"
38
- )
39
-
40
- self.pipeline.add(self.appsrc)
41
- self.pipeline.add(queue)
42
- self.pipeline.add(audioconvert)
43
- self.pipeline.add(audioresample)
44
- self.pipeline.add(audiosink)
45
-
46
- self.appsrc.link(queue)
47
- queue.link(audioconvert)
48
- audioconvert.link(audioresample)
49
- audioresample.link(audiosink)
50
-
51
- def _on_bus_message(self, bus: Gst.Bus, msg: Gst.Message, loop) -> bool: # type: ignore[no-untyped-def]
52
- t = msg.type
53
- if t == Gst.MessageType.EOS:
54
- self._logger.warning("End-of-stream")
55
- return False
56
-
57
- elif t == Gst.MessageType.ERROR:
58
- err, debug = msg.parse_error()
59
- self._logger.error(f"Error: {err} {debug}")
60
- return False
61
-
62
- return True
63
-
64
- def _handle_bus_calls(self) -> None:
65
- self._logger.debug("starting bus message loop")
66
- bus = self.pipeline.get_bus()
67
- bus.add_watch(GLib.PRIORITY_DEFAULT, self._on_bus_message, self._loop)
68
- self._loop.run() # type: ignore[no-untyped-call]
69
- bus.remove_watch()
70
- self._logger.debug("bus message loop stopped")
71
-
72
- def play(self):
73
- """Start playback."""
74
- self.pipeline.set_state(Gst.State.PLAYING)
75
- self._thread_bus_calls = Thread(target=self._handle_bus_calls, daemon=True)
76
- self._thread_bus_calls.start()
77
-
78
- def push_sample(self, data: bytes):
79
- """Push audio sample (bytes) to playback pipeline."""
80
- buf = Gst.Buffer.new_wrapped(data)
81
- self.appsrc.push_buffer(buf)
82
-
83
- def stop(self):
84
- """Stop playback and clean up."""
85
- logger = logging.getLogger(__name__)
86
- self._loop.quit()
87
- self.pipeline.set_state(Gst.State.NULL)
88
- if self._thread_bus_calls is not None:
89
- self._thread_bus_calls.join()
90
- logger.info("Stopped Player")
91
-
92
-
93
- class GstRecorder:
94
- """Audio recorder using GStreamer."""
95
-
96
- def __init__(self, sample_rate: int = 24000, device_name: Optional[str] = None):
97
- """Initialize recorder."""
98
- self._logger = logging.getLogger(__name__)
99
- Gst.init(None)
100
- self._loop = GLib.MainLoop()
101
- self._thread_bus_calls: Optional[Thread] = None
102
-
103
- self.pipeline = Gst.Pipeline.new("audio_recorder")
104
-
105
- # Create elements: try specific mic; fallback to default
106
- autoaudiosrc = _create_device_element(direction="source", name_substr=device_name) or Gst.ElementFactory.make(
107
- "autoaudiosrc", None
108
- )
109
-
110
- queue = Gst.ElementFactory.make("queue", None)
111
- audioconvert = Gst.ElementFactory.make("audioconvert", None)
112
- audioresample = Gst.ElementFactory.make("audioresample", None)
113
- self.appsink = Gst.ElementFactory.make("appsink", None)
114
-
115
- if not all([autoaudiosrc, queue, audioconvert, audioresample, self.appsink]):
116
- raise RuntimeError("Failed to create GStreamer elements")
117
-
118
- # Force mono/S16LE at 24000; resample handles device SR (e.g., 16000 → 24000)
119
- caps = Gst.Caps.from_string(f"audio/x-raw,channels=1,rate={sample_rate},format=S16LE")
120
- self.appsink.set_property("caps", caps)
121
-
122
- # Build pipeline
123
- self.pipeline.add(autoaudiosrc)
124
- self.pipeline.add(queue)
125
- self.pipeline.add(audioconvert)
126
- self.pipeline.add(audioresample)
127
- self.pipeline.add(self.appsink)
128
-
129
- autoaudiosrc.link(queue)
130
- queue.link(audioconvert)
131
- audioconvert.link(audioresample)
132
- audioresample.link(self.appsink)
133
-
134
- def _on_bus_message(self, bus: Gst.Bus, msg: Gst.Message, loop) -> bool: # type: ignore[no-untyped-def]
135
- t = msg.type
136
- if t == Gst.MessageType.EOS:
137
- self._logger.warning("End-of-stream")
138
- return False
139
-
140
- elif t == Gst.MessageType.ERROR:
141
- err, debug = msg.parse_error()
142
- self._logger.error(f"Error: {err} {debug}")
143
- return False
144
-
145
- return True
146
-
147
- def _handle_bus_calls(self) -> None:
148
- self._logger.debug("starting bus message loop")
149
- bus = self.pipeline.get_bus()
150
- bus.add_watch(GLib.PRIORITY_DEFAULT, self._on_bus_message, self._loop)
151
- self._loop.run() # type: ignore[no-untyped-call]
152
- bus.remove_watch()
153
- self._logger.debug("bus message loop stopped")
154
-
155
- def record(self):
156
- """Start recording."""
157
- self.pipeline.set_state(Gst.State.PLAYING)
158
- self._thread_bus_calls = Thread(target=self._handle_bus_calls, daemon=True)
159
- self._thread_bus_calls.start()
160
-
161
- def get_sample(self):
162
- """Return next audio sample as bytes, or None if no sample available."""
163
- sample = self.appsink.pull_sample()
164
- data = None
165
- if isinstance(sample, Gst.Sample):
166
- buf = sample.get_buffer()
167
- if buf is None:
168
- self._logger.warning("Buffer is None")
169
-
170
- data = buf.extract_dup(0, buf.get_size())
171
- return data
172
-
173
- def stop(self):
174
- """Stop recording and clean up."""
175
- logger = logging.getLogger(__name__)
176
- self._loop.quit()
177
- self.pipeline.set_state(Gst.State.NULL)
178
- if self._thread_bus_calls is not None:
179
- self._thread_bus_calls.join()
180
- logger.info("Stopped Recorder")
181
-
182
-
183
- def _create_device_element(direction: str, name_substr: Optional[str]) -> Optional[Gst.Element]:
184
- """direction: 'source' or 'sink'.
185
-
186
- name_substr: case-insensitive substring matching device display name/description.
187
- """
188
- logger = logging.getLogger(__name__)
189
-
190
- if not name_substr:
191
- logger.error(f"Device select: no name_substr for {direction}; returning None")
192
- return None
193
-
194
- monitor = Gst.DeviceMonitor.new()
195
- klass = "Audio/Source" if direction == "source" else "Audio/Sink"
196
- monitor.add_filter(klass, None)
197
- monitor.start()
198
-
199
- try:
200
- for dev in monitor.get_devices() or []:
201
- disp = dev.get_display_name() or ""
202
- props = dev.get_properties()
203
- desc = props.get_string("device.description") if props and props.has_field("device.description") else ""
204
- logger.info(f"Device candidate: disp='{disp}', desc='{desc}'")
205
-
206
- if name_substr.lower() in disp.lower() or name_substr.lower() in desc.lower():
207
- elem = dev.create_element(None)
208
- factory = elem.get_factory().get_name() if elem and elem.get_factory() else "<?>"
209
- logger.info(f"Using {direction} device: '{disp or desc}' (factory='{factory}')")
210
- return elem
211
- finally:
212
- monitor.stop()
213
- logging.getLogger(__name__).warning("Requested %s '%s' not found; using auto*", direction, name_substr)
214
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/reachy_mini_conversation_demo/camera_worker.py CHANGED
@@ -92,6 +92,7 @@ class CameraWorker:
92
  self._stop_event.set()
93
  if self._thread is not None:
94
  self._thread.join()
 
95
  logger.debug("Camera worker stopped")
96
 
97
  def working_loop(self) -> None:
@@ -108,6 +109,8 @@ class CameraWorker:
108
  while not self._stop_event.is_set():
109
  try:
110
  current_time = time.time()
 
 
111
  frame = self.reachy_mini.media.get_frame()
112
 
113
  if frame is not None:
 
92
  self._stop_event.set()
93
  if self._thread is not None:
94
  self._thread.join()
95
+
96
  logger.debug("Camera worker stopped")
97
 
98
  def working_loop(self) -> None:
 
109
  while not self._stop_event.is_set():
110
  try:
111
  current_time = time.time()
112
+
113
+ # Get frame from robot
114
  frame = self.reachy_mini.media.get_frame()
115
 
116
  if frame is not None:
src/reachy_mini_conversation_demo/console.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Bidirectional local audio stream.
2
+
3
+ records mic frames to the handler and plays handler audio frames to the speaker.
4
+ """
5
+
6
+ import asyncio
7
+ import logging
8
+
9
+ import librosa
10
+ from fastrtc import AdditionalOutputs, audio_to_int16, audio_to_float32
11
+
12
+ from reachy_mini import ReachyMini
13
+ from reachy_mini_conversation_demo.openai_realtime import OpenaiRealtimeHandler
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class LocalStream:
20
+ """LocalStream using Reachy Mini's recorder/player."""
21
+
22
+ def __init__(self, handler: OpenaiRealtimeHandler, robot: ReachyMini):
23
+ """Initialize the stream with an OpenAI realtime handler and pipelines."""
24
+ self.handler = handler
25
+ self._robot = robot
26
+ self._stop_event = asyncio.Event()
27
+ self._tasks = []
28
+ # Allow the handler to flush the player queue when appropriate.
29
+ self.handler._clear_queue = self.clear_queue # type: ignore[assignment]
30
+
31
+ def launch(self) -> None:
32
+ """Start the recorder/player and run the async processing loops."""
33
+ self._stop_event.clear()
34
+ self._robot.media.start_recording()
35
+ self._robot.media.start_playing()
36
+
37
+ async def runner() -> None:
38
+ self._tasks = [
39
+ asyncio.create_task(self.handler.start_up(), name="openai-handler"),
40
+ asyncio.create_task(self.record_loop(), name="stream-record-loop"),
41
+ asyncio.create_task(self.play_loop(), name="stream-play-loop"),
42
+ ]
43
+ try:
44
+ await asyncio.gather(*self._tasks)
45
+ except asyncio.CancelledError:
46
+ logger.info("Tasks cancelled during shutdown")
47
+ finally:
48
+ # Ensure handler connection is closed
49
+ await self.handler.shutdown()
50
+
51
+ asyncio.run(runner())
52
+
53
+ def stop(self) -> None:
54
+ """Stop the stream and underlying GStreamer pipelines.
55
+
56
+ This method:
57
+ - Sets the stop event to signal async loops to terminate
58
+ - Cancels all pending async tasks (openai-handler, record-loop, play-loop)
59
+ - Stops audio recording and playback
60
+ """
61
+ logger.info("Stopping LocalStream...")
62
+ self._stop_event.set()
63
+
64
+ # Cancel all running tasks
65
+ for task in self._tasks:
66
+ if not task.done():
67
+ task.cancel()
68
+
69
+ self._robot.media.stop_recording()
70
+ self._robot.media.stop_playing()
71
+
72
+ def clear_queue(self) -> None:
73
+ """Flush the player's appsrc to drop any queued audio immediately."""
74
+ logger.info("User intervention: flushing player queue")
75
+ self.handler.output_queue = asyncio.Queue()
76
+
77
+ async def record_loop(self) -> None:
78
+ """Read mic frames from the recorder and forward them to the handler."""
79
+ logger.info("Starting receive loop")
80
+ while not self._stop_event.is_set():
81
+ data = self._robot.media.get_audio_sample()
82
+ if data is not None:
83
+ frame_mono = data.T[0] # both channels are identical
84
+ frame = audio_to_int16(frame_mono)
85
+ await self.handler.receive((16000, frame))
86
+ # await asyncio.sleep(0) # yield to event loop
87
+ else:
88
+ await asyncio.sleep(0.01) # avoid busy loop
89
+
90
+ async def play_loop(self) -> None:
91
+ """Fetch outputs from the handler: log text and play audio frames."""
92
+ while not self._stop_event.is_set():
93
+ data = await self.handler.emit()
94
+
95
+ if isinstance(data, AdditionalOutputs):
96
+ for msg in data.args:
97
+ content = msg.get("content", "")
98
+ if isinstance(content, str):
99
+ logger.info(
100
+ "role=%s content=%s",
101
+ msg.get("role"),
102
+ content if len(content) < 500 else content[:500] + "…",
103
+ )
104
+
105
+ elif isinstance(data, tuple):
106
+ sample_rate, frame = data
107
+ device_sample_rate = self._robot.media.get_audio_samplerate()
108
+ frame = audio_to_float32(frame.squeeze())
109
+ if sample_rate != device_sample_rate:
110
+ frame = librosa.resample(frame, orig_sr=sample_rate, target_sr=device_sample_rate)
111
+ self._robot.media.push_audio_sample(frame)
112
+
113
+ # else: ignore None/unknown outputs
114
+
115
+ await asyncio.sleep(0) # yield to event loop
src/reachy_mini_conversation_demo/main.py CHANGED
@@ -1,6 +1,7 @@
1
  """Entrypoint for the Reachy Mini conversation demo."""
2
 
3
  import os
 
4
 
5
  import gradio as gr
6
  from fastapi import FastAPI
@@ -14,6 +15,7 @@ from reachy_mini_conversation_demo.utils import (
14
  setup_logger,
15
  handle_vision_stuff,
16
  )
 
17
  from reachy_mini_conversation_demo.openai_realtime import OpenaiRealtimeHandler
18
  from reachy_mini_conversation_demo.audio.head_wobbler import HeadWobbler
19
 
@@ -31,8 +33,19 @@ def main():
31
  logger = setup_logger(args.debug)
32
  logger.info("Starting Reachy Mini Conversation Demo")
33
 
 
 
 
34
  robot = ReachyMini()
35
 
 
 
 
 
 
 
 
 
36
  camera_worker, _, vision_manager = handle_vision_stuff(args, robot)
37
 
38
  movement_manager = MovementManager(
@@ -62,18 +75,24 @@ def main():
62
  logger.debug(f"Chatbot avatar images: {chatbot.avatar_images}")
63
 
64
  handler = OpenaiRealtimeHandler(deps)
65
- stream = Stream(
66
- handler=handler,
67
- mode="send-receive",
68
- modality="audio",
69
- additional_inputs=[chatbot],
70
- additional_outputs=[chatbot],
71
- additional_outputs_handler=update_chatbot,
72
- ui_args={"title": "Talk with Reachy Mini"},
73
- )
74
 
75
- app = FastAPI()
76
- app = gr.mount_gradio_app(app, stream.ui, path="/")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  # Each async service → its own thread/loop
79
  movement_manager.start()
@@ -82,11 +101,14 @@ def main():
82
  camera_worker.start()
83
 
84
  try:
85
- stream.ui.launch()
86
  except KeyboardInterrupt:
87
- logger.info("Exiting...")
88
-
89
  finally:
 
 
 
 
90
  movement_manager.stop()
91
  head_wobbler.stop()
92
  if camera_worker:
@@ -94,6 +116,7 @@ def main():
94
 
95
  # prevent connection to keep alive some threads
96
  robot.client.disconnect()
 
97
 
98
 
99
  if __name__ == "__main__":
 
1
  """Entrypoint for the Reachy Mini conversation demo."""
2
 
3
  import os
4
+ import sys
5
 
6
  import gradio as gr
7
  from fastapi import FastAPI
 
15
  setup_logger,
16
  handle_vision_stuff,
17
  )
18
+ from reachy_mini_conversation_demo.console import LocalStream
19
  from reachy_mini_conversation_demo.openai_realtime import OpenaiRealtimeHandler
20
  from reachy_mini_conversation_demo.audio.head_wobbler import HeadWobbler
21
 
 
33
  logger = setup_logger(args.debug)
34
  logger.info("Starting Reachy Mini Conversation Demo")
35
 
36
+ if args.no_camera and args.head_tracker is not None:
37
+ logger.warning("Head tracking is not activated due to --no-camera.")
38
+
39
  robot = ReachyMini()
40
 
41
+ # Check if running in simulation mode without --gradio
42
+ if robot.client.get_status()["simulation_enabled"] and not args.gradio:
43
+ logger.error(
44
+ "Simulation mode requires Gradio interface. Please use --gradio flag when running in simulation mode."
45
+ )
46
+ robot.client.disconnect()
47
+ sys.exit(1)
48
+
49
  camera_worker, _, vision_manager = handle_vision_stuff(args, robot)
50
 
51
  movement_manager = MovementManager(
 
75
  logger.debug(f"Chatbot avatar images: {chatbot.avatar_images}")
76
 
77
  handler = OpenaiRealtimeHandler(deps)
 
 
 
 
 
 
 
 
 
78
 
79
+ stream_manager = None
80
+
81
+ if args.gradio:
82
+ stream = Stream(
83
+ handler=handler,
84
+ mode="send-receive",
85
+ modality="audio",
86
+ additional_inputs=[chatbot],
87
+ additional_outputs=[chatbot],
88
+ additional_outputs_handler=update_chatbot,
89
+ ui_args={"title": "Talk with Reachy Mini"},
90
+ )
91
+ stream_manager = stream.ui
92
+ app = FastAPI()
93
+ app = gr.mount_gradio_app(app, stream.ui, path="/")
94
+ else:
95
+ stream_manager = LocalStream(handler, robot)
96
 
97
  # Each async service → its own thread/loop
98
  movement_manager.start()
 
101
  camera_worker.start()
102
 
103
  try:
104
+ stream_manager.launch()
105
  except KeyboardInterrupt:
106
+ logger.info("Keyboard interruption in main thread... closing server.")
 
107
  finally:
108
+ # Stop the stream manager and its pipelines
109
+ stream_manager.close()
110
+
111
+ # Stop other services
112
  movement_manager.stop()
113
  head_wobbler.stop()
114
  if camera_worker:
 
116
 
117
  # prevent connection to keep alive some threads
118
  robot.client.disconnect()
119
+ logger.info("Shutdown complete.")
120
 
121
 
122
  if __name__ == "__main__":
src/reachy_mini_conversation_demo/moves.py CHANGED
@@ -498,7 +498,7 @@ class MovementManager:
498
  self.state.move_start_time = current_time
499
  # Any real move cancels breathing mode flag
500
  self._breathing_active = isinstance(self.state.current_move, BreathingMove)
501
- logger.info(f"Starting new move, duration: {self.state.current_move.duration}s")
502
 
503
  def _manage_breathing(self, current_time: float) -> None:
504
  """Manage automatic breathing when idle."""
@@ -525,7 +525,7 @@ class MovementManager:
525
  interpolation_duration=1.0,
526
  )
527
  self.move_queue.append(breathing_move)
528
- logger.info("Started breathing after %.1fs of inactivity", idle_for)
529
  except Exception as e:
530
  self._breathing_active = False
531
  logger.error("Failed to start breathing: %s", e)
 
498
  self.state.move_start_time = current_time
499
  # Any real move cancels breathing mode flag
500
  self._breathing_active = isinstance(self.state.current_move, BreathingMove)
501
+ logger.debug(f"Starting new move, duration: {self.state.current_move.duration}s")
502
 
503
  def _manage_breathing(self, current_time: float) -> None:
504
  """Manage automatic breathing when idle."""
 
525
  interpolation_duration=1.0,
526
  )
527
  self.move_queue.append(breathing_move)
528
+ logger.debug("Started breathing after %.1fs of inactivity", idle_for)
529
  except Exception as e:
530
  self._breathing_active = False
531
  logger.error("Failed to start breathing: %s", e)
src/reachy_mini_conversation_demo/openai_realtime.py CHANGED
@@ -27,8 +27,8 @@ class OpenaiRealtimeHandler(AsyncStreamHandler):
27
  """Initialize the handler."""
28
  super().__init__(
29
  expected_layout="mono",
30
- output_sample_rate=24000,
31
- input_sample_rate=24000,
32
  )
33
  self.deps = deps
34
 
@@ -169,7 +169,7 @@ class OpenaiRealtimeHandler(AsyncStreamHandler):
169
  )
170
  )
171
 
172
- if tool_name == "camera":
173
  b64_im = json.dumps(tool_result["b64_im"])
174
  await self.connection.conversation.item.create(
175
  item={
 
27
  """Initialize the handler."""
28
  super().__init__(
29
  expected_layout="mono",
30
+ output_sample_rate=24000, # openai outputs
31
+ input_sample_rate=16000, # respeaker output
32
  )
33
  self.deps = deps
34
 
 
169
  )
170
  )
171
 
172
+ if tool_name == "camera" and "b64_im" in tool_result:
173
  b64_im = json.dumps(tool_result["b64_im"])
174
  await self.connection.conversation.item.create(
175
  item={
src/reachy_mini_conversation_demo/utils.py CHANGED
@@ -8,15 +8,14 @@ from reachy_mini_conversation_demo.camera_worker import CameraWorker
8
  def parse_args():
9
  """Parse command line arguments."""
10
  parser = argparse.ArgumentParser("Reachy Mini Conversation Demo")
11
- parser.add_argument("--sim", action="store_true", help="Run in simulation mode")
12
  parser.add_argument(
13
  "--head-tracker",
14
  choices=["yolo", "mediapipe", None],
15
  default=None,
16
- help="Choose head tracker (default: mediapipe)",
17
  )
18
  parser.add_argument("--no-camera", default=False, action="store_true", help="Disable camera usage")
19
- parser.add_argument("--headless", default=False, action="store_true", help="Run in headless mode")
20
  parser.add_argument("--debug", default=False, action="store_true", help="Enable debug logging")
21
  return parser.parse_args()
22
 
 
8
  def parse_args():
9
  """Parse command line arguments."""
10
  parser = argparse.ArgumentParser("Reachy Mini Conversation Demo")
 
11
  parser.add_argument(
12
  "--head-tracker",
13
  choices=["yolo", "mediapipe", None],
14
  default=None,
15
+ help="Choose head tracker (default: None)",
16
  )
17
  parser.add_argument("--no-camera", default=False, action="store_true", help="Disable camera usage")
18
+ parser.add_argument("--gradio", default=False, action="store_true", help="Open gradio interface")
19
  parser.add_argument("--debug", default=False, action="store_true", help="Enable debug logging")
20
  return parser.parse_args()
21
 
uv.lock CHANGED
The diff for this file is too large to render. See raw diff