Skip to content
mikokono
Go back

Physical AI with Reachy Mini and Microsoft Foundry

TL;DR

From a desk ornament to a voice agent

Before I got to the AI part, my girlfriend cursed me for the sounds Reachy makes when it starts and stops. I was restarting it often while developing, so she kept hearing them. Fair enough: the startup and shutdown soundtrack became part of my test loop.

When Reachy Mini arrived, I had the robot on my desk and a question: what would it take to make it more than a cute desk companion? I wanted to talk to it, give it a task, and have it answer with more than words. A nod, a head shake, a wiggle of the antennas, maybe even a little dance.

That became Reachy-Foundry, a project connecting the robot to Microsoft Foundry. It runs on a Reachy Mini Wireless, with Azure providing the speech services and the model behind the conversation. The robot is not just a character on a screen: its head and antennas can move in response to what the agent says.

This is the kind of project I mean when I say physical AI. The interesting part isn’t a newly trained model. It’s connecting perception, reasoning, and action so an AI system can respond in the physical world.

Getting my code onto Reachy

One early challenge was deployment. The Reachy Mini Windows desktop app didn’t have a way to upload an app from my local development setup. My good old friend, the coding agent, helped me work through a few approaches. After some trial and error, I ended up with a PowerShell script that deploys the app to Reachy over SSH.

The deployment script installs the app into the robot’s app environment and can restart the daemon as part of the deploy. That gave me a repeatable local edit, SSH deploy, test-on-the-robot loop. It also explains why Reachy’s startup and shutdown sounds got so many repetitions. The deployment notes cover the setup.

Reachy Mini laughs and moves while telling a joke

Reachy telling a joke, with captions. The GIF is from the Reachy-Foundry repository, where you can also watch the full video with sound.

A voice loop with a body at the end

The app and its audio loop run on Reachy Mini Wireless, whose Raspberry Pi Compute Module 4 uses ARM64. Audio capture and playback go through Reachy’s media interface. A lightweight energy endpointer closes an utterance after about 0.9 seconds of silence, then the app sends the audio to Azure Speech for transcription over REST. The Speech SDK isn’t used here: it has no ARM64 wheels for the robot, and it would compete with the Reachy daemon for the microphone and speaker.

The transcript goes to a conversation gate. While Reachy is idle, speech is still transcribed so the gate can check for a wake phrase; anything that doesn’t wake it is dropped before it reaches the chat model. On a match, the app creates a fresh Agent Framework session and forwards the request to the agent. The robot’s head rises while the model request is already in flight.

The agent uses a FoundryChatClient for the deployed model and registers the robot’s RobotTools as callable tools. That gives the model actions such as move_head, nod, and play_emotion. The model returns a tool name and arguments; it does not drive the motors directly. The Python tool handler checks and clamps those arguments, serializes physical actions, and then calls the Reachy SDK. The app logs tool calls and their duration too.

Putting a gate in front of the model

In my first attempts, I had no conversation gate. Every transcript went straight to the LLM, and Reachy tried to respond to anything I said nearby. That felt wrong for a robot that was meant to join a conversation only when I wanted it to.

The model was doing what I had asked it to do: answer the input it received. One time, while my girlfriend was complaining about the startup noise, Reachy apologized and said he was sorry for being so annoying. It was funny, but it also made the problem obvious: ordinary background speech should not automatically become another model turn.

I wanted a simple rhythm: say a start phrase, talk to Reachy, then say an end phrase to put it back in idle. The gate now keeps transcripts out of the model until it hears the wake phrase. Once the conversation is active, normal requests go to the Agent Framework. The end phrase drops that conversation’s session, so the next wake starts with a clean history.

stateDiagram-v2
  [*] --> Idle
  Idle --> Idle: Transcript without wake phrase is discarded
  Idle --> Active: Wake phrase creates a fresh session
  Active --> Active: Normal request goes to the agent
  Active --> Idle: End phrase drops session and returns to idle
  Active --> Idle: Inactivity timeout drops session and returns to idle

This gate acts on transcripts, not raw audio. While Reachy is idle, Azure Speech still transcribes what it hears so the app can check for the wake phrase; the rejected transcript is not sent to the reasoning model. Local wake and stop detection is still on my list to reduce that cloud round trip.

The joke, step by step

A joke is a small interaction, but it exercises speech recognition, the wake gate, model reasoning, tool calling, speech synthesis, and a physical action. I split the active joke turn from the goodbye path to keep both diagrams readable. The Agent Framework dispatches the model’s play_emotion("laughing1", when="after_reply") call to local RobotTools, which validates and queues the animation. After the synthesized joke has played, the app tells the robot to run it.

sequenceDiagram
  autonumber
  participant User
  participant App as Reachy app
  participant Speech as Azure Speech
  participant Model as Agent and Foundry
  participant Tools as Robot tools
  participant Robot as Reachy

  User->>App: Hey Ritchie, tell me a joke
  App->>Speech: Transcribe audio after 0.9s quiet
  Speech-->>App: Request transcript
  Note over App: Wake gate opens a fresh session
  App->>Robot: ensure_awake()
  Note over App,Robot: Head rises as reasoning starts
  App->>Model: agent.run(request, fresh session)
  Model-->>App: Joke and play_emotion tool call
  App->>Tools: play_emotion(laughing1, after_reply)
  Tools-->>App: Validate and queue animation
  App->>Speech: Synthesize the joke
  Speech-->>App: Speech audio
  App->>User: Speak the scarecrow joke
  App->>Tools: play_pending_emotion()
  Tools->>Robot: Play laughing1 on hardware
  Robot-->>User: Reachy laughs and moves

Reachy’s spoken answer is: “Why did the scarecrow win an award? Because he was outstanding in his field.”

Then the goodbye

The goodbye takes a different route. The gate recognizes the end phrase, the app drops the conversation session and any queued emotion, Reachy speaks its farewell, and the app puts the robot back to sleep. It doesn’t need another reasoning-model round trip just to stop the conversation.

sequenceDiagram
  autonumber
  participant User
  participant App as Reachy app
  participant Speech as Azure Speech
  participant Gate as Wake gate
  participant Robot as Reachy

  User->>App: Goodbye
  App->>Speech: Transcribe audio
  Speech-->>App: Transcript
  App->>Gate: Check end phrase
  Gate-->>App: End conversation
  Note over App,Gate: Drop session and clear queued emotion
  App->>Speech: Synthesize farewell
  Speech-->>App: Farewell audio
  App->>User: Say goodbye
  App->>Robot: go_to_sleep()
  Robot-->>User: Head folds down and returns to idle

Reachy’s farewell is: “Goodbye! Say my name when you need me.”

A wake phrase is not the same as a private microphone

Reachy goes idle after a conversation and waits for a wake phrase such as “Hey Reachy.” While idle, recognized text is dropped instead of being forwarded to the chat model. There is an important detail, though: the current implementation still sends audio to Azure Speech for transcription so it can check for that phrase. “Not sent to the reasoning model” does not mean “never leaves the device.”

I want that distinction to be clear. It is an architectural choice, and it matters to anyone evaluating where audio goes. The wake and goodbye notes describe the gate and its behavior in more detail.

🚀 What’s next

The current project is a voice-and-motion experiment, not a general-purpose humanoid assistant. The model can use the tools exposed by the app, and application code validates tool arguments before they reach the robot. With that basic loop in place, I have three clear next steps.

Connect the camera

The repository has camera and on-robot person-detection code, but they are not wired into the agent conversation yet. Reachy cannot answer questions about what it sees today. I want to connect that path so the agent can use visual context, while being deliberate about when images are captured and sent for reasoning.

Keep start and stop commands local

Right now, the wake and goodbye phrases are checked after Azure Speech transcribes the audio. I’m looking for a local solution for those start and stop voice commands. Recognizing them on the robot should avoid a cloud round-trip just to begin or end a conversation, which could make the interaction feel quicker. I still need to test how reliably that works with Reachy’s microphone and real room noise.

Reduce the wait for a response

The reasoning model’s response latency is another area I’d like to improve. I’ll look at faster model options and the time from the first model response to the first spoken audio, while checking that tool use still works reliably. A faster answer is only useful if Reachy can still do the right thing with it.

These are next steps, not capabilities the project already has. The camera would give Reachy more context; local start and stop detection and a faster reasoning path would make the conversation feel more immediate.

The physical part changes the question

Building this made me think less about whether a model can answer a prompt and more about what happens after it answers. A physical agent has to connect speech, state, model decisions, tools, and hardware in a way that is observable and bounded. That is a different engineering problem from putting a chat window in front of a model.

Reachy-Foundry is a small, deliberately playful place to explore that boundary. The joke is fun, but the useful idea underneath it is simple: cloud reasoning can meet a local body, as long as the application is explicit about what the model can do, what data leaves the device, and what is still only a plan.

The code and setup notes are in nampacx/Reachy-Foundry.


Share this post on:

Next Post
Trying Foundry Routines with a Stock Watch Hosted Agent