REACHY_CONV / v0.4.17
local · online
▶ Reachy Mini App

Reachy Conv

A fully local conversation stack for Reachy Mini — your robot talks to a local Ollama LLM, hears you through a local STT model, speaks with a local TTS engine, and animates itself with a state-driven emotion gesture system. No cloud, no API keys, no per-token costs.

// Stack

What runs on the robot

ComponentDefaultAlternatives
STT NVIDIA Parakeet TDT 0.6B v3 (multilingual, ~600 MB) faster-whisper (CTranslate2); Parakeet v2 (EN-only); Parakeet v3-SmoothQuant (int8, long-audio); NVIDIA Canary-1B-v2 (translation)
LLM Anything on your local Ollama server OpenAI-compatible HTTP endpoints (vLLM, LM Studio, llama.cpp --server)
TTS edge-tts (Microsoft neural voices, ~300 voices, online) Silero v3_en (offline, CPU, 119 speakers); Qwen3-TTS (opt-in extra)
VAD smolvad (silero-vad ONNX, <1 MB) with adaptive silence window silero (PyPI; pulls torch)
Motion pollen-robotics/reachy-mini-emotions-library (85 moves) — state-driven Procedural breathing + head-wobble when disabled

// Install

Two ways in

▸ From the Reachy Mini desktop app

Open the app store, search for Reachy Conv, install. The first run pulls Parakeet v3 (~600 MB) and Silero TTS (~55 MB) into ~/.cache/huggingface.

▸ For local development

Laptop + sim daemon. Two terminals: one runs the daemon, one runs the app. Dashboard opens on http://localhost:7860/.

git clone https://huggingface.co/spaces/ArtFix0/reachy_conv_app
cd reachy_conv_app
pip install -e .

# In another terminal:
reachy-mini-daemon --sim

# Back in the first:
python -m reachy_conv_app
 dashboard at http://localhost:7860/
ffmpeg is bundled (since v0.4.1)

The default TTS engine (edge-tts) returns MP3 audio and the pydub decoder shells out to ffmpeg to convert it to PCM. As of v0.4.1 the imageio-ffmpeg package is a required dependency — it bundles a self-contained ffmpeg binary for Linux, macOS, and Windows, and tts/edge_backend.py automatically points pydub at it. No system-level ffmpeg install is required on a fresh install.

If you want to use a different ffmpeg (e.g. a system install with hardware acceleration), install it on PATH and the bundled one will be shadowed: sudo apt install ffmpeg · brew install ffmpeg · winget install Gyan.FFmpeg.

!
Qwen TTS (opt-in)

Qwen3-TTS is not installed by default. The qwen-tts PyPI package hard-pins transformers==4.57.3, which transitively requires huggingface-hub>=0.34.0,<1.0. Every released reachy-mini (1.5.0 through 1.9.0) is incompatible with that constraint, so the Reachy Mini Control desktop app's shared apps_venv hard-fails the dependency resolution the moment qwen-tts is required.

Install the extra in a separate venv:

python -m venv .venv-qwen
source .venv-qwen/bin/activate
pip install -e .[qwen_tts]

When qwen-tts is not importable, the dashboard's STATUS / TTS line and the #tts-status paragraph under the engine dropdown show the fallback reason (e.g. "qwen-tts not importable — check your install") and the TTS manager falls back through edge to silero. Selecting silero in the TTS section always works.

// Configuration

config.yaml

All runtime settings live in config.yaml at the project root. The dashboard edits this file in place and the conversation loop hot-reloads it on every save — no restart needed. A documented config.example.yaml is checked in for reference.

# llm
llm:
  base_url: http://localhost:11434
  model: llama3.1:8b-instruct-q4_K_M
  api_format: ollama              # or "openai" for OpenAI-compat endpoints
  system_prompt: "You are Reachy Mini, a small expressive desk robot..."
  temperature: 0.7
  max_tokens: 600                 # reasoning models need room for thought + answer
  think: false                    # set true to allow chain-of-thought on qwen3.5/gpt-oss
  num_ctx: 4096                   # context window; raise for longer conversations
  keep_alive: 5m                  # how long Ollama keeps the model in VRAM ("-1" = never)
  timeout_s: 300.0                # HTTP timeout per chat call (cold loads can take 60-120s)

# stt
stt:
  engine: parakeet                # or "whisper"
  parakeet:
    variant: v3                   # v2 (EN-only) / v3 (multilingual) / v3sq / canary
  whisper:
    model: small                  # tiny / base / small / medium / large-v3
    device: cpu                   # or "cuda"

# tts
tts:
  engine: edge                    # edge (default) / silero / qwen (opt-in)
  voice: en-US-AriaNeural         # ~300 edge voices; silero en_0…en_118
  rate: "+0%"                     # -50%…+100% (edge-tts only)
  volume: "+0%"                   # -50%…+50%
  pitch: "+0Hz"                   # -20Hz…+20Hz
  strip_emoji: true               # strip pictographs from LLM reply before TTS

# vad
vad:
  engine: smolvad                 # "smolvad" (silero-vad ONNX) or "silero"
  threshold: 0.5
  min_speech_ms: 250
  min_silence_ms: 300             # max silence window for short utterances
  min_silence_floor_ms: 300       # floor for long utterances (adaptive mode)
  silence_curve_knee_ms: 1500     # speech length (ms) at which we reach the floor
  silence_curve: adaptive         # "adaptive" (window shrinks) or "off" (fixed)

# conversation
conversation:
  mode: ptt                       # "ptt" (antenna) or "vad" (always-on)
  ptt_antenna: right              # "right" or "left"
  max_listen_seconds: 30.0
  interrupt_on_touch: true
  wake_word: ''                   # reserved for future use

# motion
motion:
  enabled: true                   # false = breathing + head-wobble only
  reply_overrides_enabled: true   # false = no post-TTS punctuation gestures
  emotion_map:                    # state → (move_name, loop?)
    wake:      laughing2
    listening: breathing
    thinking:  laughing2
    question:  inquiring1
    affirm:    yes1
    deny:      no1
    sad:       sad1
    cheerful:  cheerful1
    sleep:     tired1

# dashboard
dashboard:
  host: 0.0.0.0
  port: 7860
  autostart_loop: false

// Dashboard

Tabs at http://<robot>:7860/

Single FastAPI server in a daemon thread on the robot. Static assets are version-stamped (/static/app.js?v=<version>) so the browser is forced to fetch the new bundle after every reinstall.

STATUS

Backend pips (robot/llm/tts/vad/stt), armed/loop state, CPU/RAM meters, ARM/DISARM, live transcript.

LLM

Model picker (queries your Ollama), system prompt, temperature, max_tokens, num_ctx, keep_alive, timeout. "TEST → Say hi in 5 words" button.

STT

Engine + per-engine knobs; upload a WAV to transcribe. Auto-populates the model dropdown on init.

TTS

Engine + voice picker (~300 edge voices grouped by language); "SPEAK" button to play a test phrase; rate/volume/pitch sliders (edge-tts only).

VAD

threshold, min-silence, adaptive curve, floor + knee; upload a WAV to see segment boundaries.

CONVERSATION

mode (PTT/VAD), antenna, max listen, ARM button, live transcript.

MOTION

Library status (loads on first click), per-state move dropdowns with Test/Set buttons, master "Use emotion library" toggle, Retry library load, Post-TTS punctuation gestures.

LOGS

Live tail of the last 200 log lines, color-coded by level, with a since watermark so reconnects don't duplicate.

ABOUT

Version (read from importlib.metadata.version()), config path.

// Internals

How the conversation loop works

  1. Mic pump — A thread in main.py reads 16 kHz PCM frames from the Reachy daemon's media manager and feeds them to LocalConversationHandler.receive().
  2. HandlerLocalConversationHandler (local_conversation.py) implements the fastrtc.AsyncStreamHandler interface, but the local loop bypasses fastrtc.Stream — mic samples go straight in.
  3. VADvad/backends.py consumes each frame, accumulates 512-sample chunks, and runs the silero-vad ONNX model. Its state machine implements an adaptive silence window: short utterances get the full min_silence_ms, long utterances get the min_silence_floor_ms. The transition is linear over silence_curve_knee_ms.
  4. STT → LLM → TTS — When the VAD fires utterance_ended=True, the handler flushes the buffered audio to the STT backend, gets the transcript, sends it to the LLM, gets the reply, sends it to the TTS backend, and pushes the resulting audio to the Reachy daemon's speaker.
  5. Self-feedback guard — While the robot is speaking, the mic is muted (_speaking_until in local_conversation.py) to prevent the robot from transcribing its own TTS output and replying to itself.
  6. Motion loop — Runs MovementManager at 100 Hz. On each conversation state transition (start, arm, VAD start, LLM call, TTS start, TTS end), GestureDispatcher looks up the move name in motion.emotion_map and asks the MovementManager to play it from the pollen-robotics/reachy-mini-emotions-library dataset. State-driven mapping is hot-reloaded on every settings save.
  7. Procedural fallback — LISTENING and IDLE use the procedural BreathingMove (5 mm z-sway at 0.1 Hz + ±15° antenna sway at 0.5 Hz) rather than a recorded move — it feels natural without committing to a heavy gesture. THINKING uses a short one-shot (default laughing2, loop forced off).

// Troubleshooting

When it doesn't work

Click any item to expand the fix.

"No VAD backend could be initialised"

The silero VAD ONNX model isn't loadable. Reinstall the package: pip install --force-reinstall reachy_conv_app (the .onnx file ships in the wheel).

"OllamaError: connection refused"

Start your local Ollama server (ollama serve) or set llm.base_url to a remote one.

TTS playback is silent

Check the Reachy daemon is running and the speaker is enabled. The dashboard's "SPEAK" test button uses the same audio path as real conversation, so if that plays something the audio chain is working.

I selected edge but the robot speaks with silero

edge-tts or one of its decoders isn't working. The dashboard's STATUS / TTS line and the #tts-status paragraph under the engine dropdown show the install hint. Two common paths:

edge-tts not importable — the wheel didn't land. Reinstall with pip install --force-reinstall edge-tts pydub.

ffmpeg not on PATHpydub shells out to ffmpeg to decode the MP3 that edge-tts returns. Install ffmpeg (apt install ffmpeg / brew install ffmpeg / winget install Gyan.FFmpeg).

I selected qwen but the robot speaks with edge or silero

qwen-tts is an optional extra, not a default dep. The dashboard's STATUS / TTS line and the #tts-status paragraph show the install hint.

You want qwen in this installqwen-tts 0.1.1 is upstream-incompatible with every released reachy-mini version because it hard-pins transformers==4.57.3, which requires huggingface-hub<1.0. The shared apps_venv resolution fails. There is no workaround on the desktop app's shared venv; install pip install .[qwen_tts] in a separate venv where reachy-mini isn't pinned, or wait for qwen-tts upstream to relax the transformers pin.

You don't have qwen-tts installed at all — the engine drops to silero with a yellow pip and a clear fallback reason; select silero in the TTS section to make the warning go away.

Motion tab says "emotion library unavailable"

The HF dataset download failed (offline, rate-limited, disk full). Click the ↻ Retry library load button. The dashboard's fallback is procedural breathing + head-wobble; the robot still works.

Motion tab "Test" button shows "play failed: …"

The toast shows the real error string (not "[object Object]"). Common causes: library not loaded yet (wait or hit Retry), or the move name isn't in the dataset (the dropdown should prevent that but a hand-edited config.yaml could break it).

Robot transcribes itself in a loop

Your silence_curve floor is too aggressive. Raise min_silence_floor_ms to 300+ (the default).

First STT/TTS call takes a long time

The model checkpoint is downloading. Watch the logs; subsequent calls are fast.

Browser shows stale dashboard after reinstall

Shouldn't happen any more (v0.4.16 added ?v=<version> cache-buster on app.js and cyberpunk.css). If it does, hard refresh (Ctrl+Shift+R) and report the version shown in the top bar.

// Changelog

Release notes

v0.4.17

Motion tab play endpoint switched to path-parameter shape (POST /api/motion/play_emotion/{move_name}?loop=…) to match the official Reachy Mini Desktop App's pollen-robotics/reachy-mini-desktop-app Expression picker pattern (the daemon's own endpoint is POST /api/move/play/recorded-move-dataset/{ds}/{move}). No Pydantic body model — the v0.4.16 "Field required" 422 is now structurally impossible.

v0.4.16

Cache-bust static assets (?v=<version> injected by index() into the <script>/<link> tags) so the browser is forced to fetch the new bundle on every reinstall. Diagnostic console.warn in playEmotionPreview logs the request body.

v0.4.15

Dynamic /api/app/version from importlib.metadata.version(). The top-bar brand and About card now show the actually-installed version (was hardcoded "v0.1.0").

v0.4.14

Defensive error-message coercion in api() and playEmotionPreview. Pydantic 422 detail lists are joined by ; instead of becoming "[object Object]".

v0.4.13

Motion tab per-row Set button (focused PUT, hot-reloads the dispatcher's mapping on save, no restart) + real error feedback on Test (the r.ok === false toast that surfaced the silent "object = object" failure).

v0.4.12

Auto-populate Ollama model dropdown on dashboard init; auto-save model on change. max_tokens raised to 600 for reasoning models.

v0.4.11

Re-fetch Motion tab on every navigation (so the library list picks up the background download without a hard refresh); ↻ Retry library load button for failed downloads.

v0.4.10

Emoji-strip toggle for TTS. Head-wobble fix on subsequent responses (was clipping when LLM replies were short).

v0.4.9

LISTENING/IDLE = procedural BreathingMove; THINKING = one-shot. Smooth blend-in for every recorded move (so the head doesn't snap to the move's first frame). Motion tab dropdowns always editable even when the library isn't loaded.

v0.4.6

State-driven emotion moves (GestureDispatcher + per-state map).

v0.4.4

max_tokens raised to 200 (from 100) for reasoning models.

v0.4.1

Bundle ffmpeg via imageio-ffmpeg (no system install required).

v0.4.0

Wire edge-tts knobs (rate/volume/pitch) through settings, dashboard, and synth call.

v0.3.2

Surface qwen→silero fallback reason; ship onnx-only silero VAD (dropped the silero-vad + torchaudio dep tree, which crashed on CPU-only robots without CUDA 13).

END · TRANSMISSION