REACHY_CONV / v0.5.56
local · online
▶ A body for Hermes

Reachy Conv

A clean, empty pipe that gives Hermes a physical body — a Reachy Mini robot in the real world. Through the robot, Hermes hears with the built-in microphone, speaks through the speaker, and sees its surroundings through the camera. The pipe wires up the sensors and actuators and then steps out of the way: no injected system prompt, no hard-coded user prompt, no tool-use scaffolding, no scripted behavior. Hermes is self-learning and self-adapting — it reads the Reachy Mini documentation, invents its own skills, and decides for itself how to listen, move, and talk. This app is just the nervous system; Hermes is the mind.

// The pipe

Sensors & actuators the app wires up

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 (native /api/chat) completion_api engine — any OpenAI-compatible HTTP endpoint (vLLM, LM Studio, llama.cpp --server, OpenAI direct) via /v1/chat/completions; thinking on/off + reasoning_effort knob
TTS edge-tts (Microsoft neural voices, ~300 voices, online) Silero v3_en (offline, CPU, 119 speakers); Qwen3-TTS via qwentts.cpp (opt-in extra, GGUF quant dropdown Q4_K_M/Q8_0/BF16/F32 + live sampling knobs + in-tab preview)
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
Output EQ Off (flat) — passthrough In-app 10-band graphic EQ (31 Hz–8 kHz) + separate high-pass / low-shelf / high-shelf end filters + master gain + soft-knee brickwall limiter (clean volume boost — no tanh squashing) — RBJ biquad chain applied per chunk before the speaker (EasyEffects replacement, cross-platform). Off by default.

// 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
  engine: ollama                 # "ollama" (native /api/chat) or "completion_api" (OpenAI-compat /v1/chat/completions)
  api_format: ollama              # legacy alias kept in sync with engine
  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
  reasoning_effort: none          # only when think=false & engine=completion_api: none (LM Studio/vLLM/llama.cpp) / minimal (OpenAI direct) / low / medium / high
  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
  post_tts_silence_s: 0.7         # mic-mute tail after TTS — the self-barge-in knob
  barge_lead_in_s: 1.5            # how much preceding speech a barge-in captures
  rms_silence_threshold: 200.0   # below this RMS the captured utterance is treated as silence
  ptt_press_threshold: 0.5        # antenna angle (rad) that counts as a PTT press

# 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
  breathing_z_amplitude: 0.005    # procedural breathing depth (m)
  breathing_frequency: 0.1       # breathing rate (Hz)
  antenna_sway_amplitude_deg: 15.0
  antenna_frequency: 0.5
  blend_s: 0.7                    # gesture blend-in time
  idle_inactivity_delay_s: 30.0   # idle seconds before breathing starts
  max_head_body_yaw_delta_deg: 65.0

# audio (XVF3800 chip — hardware mic / AEC config; live re-apply from the AUDIO tab)
audio:
  agc_max_gain: 10.0
  min_ns: 0.8
  min_nn: 0.8
  gamma_e: 0.5
  gamma_etail: 0.5
  nlattenonoff: 0
  mgscale: [4.0, 1.0, 1.0]

# hermes (optional remote reasoning bridge; START/STOP from the HERMES tab)
hermes:
  enabled: false
  base_url: ""
  api_key: ""                       # auto-generated key is kept on save; empty incoming key is dropped
  model: ""
  stop_timeout_s: 5.0
  log_poll_interval_s: 0.5
  log_buffer_size: 5000

# 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

Engine picker (ollama native or completion_api OpenAI-compat), model picker (queries the right server — separate CompletionApiClient for /v1/models), system prompt, temperature, max_tokens, num_ctx, keep_alive, timeout, idle-stream watchdog. Thinking on/off toggle + reasoning_effort select (none/minimal/low/medium/high — matches the server you point at). "TEST → Say hi in 5 words" button.

STT

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

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.

AUDIO

XVF3800 hardware chip config (AGC max gain, min NS/NN, gamma E/ETAIL, non-linear attenuation, MGSCALE). Re-apply now button writes the chip live — no restart. Defaults are the tuned values; reset if audio breaks. Output voice EQ — a 10-band graphic EQ (31 Hz–8 kHz vertical faders) + separate high-pass / low-shelf / high-shelf end filters + master gain, applied to the robot speaker output (EasyEffects replacement). Off by default; changes apply live on the next spoken chunk.

CONVERSATION

mode (PTT/VAD), antenna, max listen, ARM button, live transcript. Advanced (mic gating / self-barge-in): post-TTS silence, barge lead-in, RMS silence threshold, PTT press threshold.

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. Advanced (breathing / blend / timing): z-amplitude, frequency, antenna sway, blend, idle delay, head/body yaw clamp.

HERMES

Optional remote reasoning bridge. START pushes the api_key to the live handler + opens a session; STOP clears it (new session). Advanced (stop / log poll timing): stop timeout, log poll interval, log buffer size.

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.5.56

Fix: spontaneous voice never fired + idle random moves only fired when disarmed. Two bugs from v0.5.54/v0.5.55. (1) The spontaneous-voice loop's idle guard checked self._buffer to mean "the user is talking" — but every mic frame is appended to that buffer while armed (silence included), so it was non-empty the entire armed-silent time and the guard gated the loop off forever. The robot never spoke on its own, no matter how long you waited. Fix: guard on self._vad_has_speech (True only between VAD speech-start and speech-end — the real "you're talking right now" signal). (2) The idle random moves (fidget) were gated to MotionState.IDLE only, which is emitted only on disarm — so they never fired while the robot was armed and silent on the desk (the way you actually use it). Fix: also allow MotionState.LISTENING (armed + silent) in the dispatcher gate. Fidget now fires whenever the robot isn't in a turn — armed-silent or disarmed — and stays suppressed during think / speak / speak-done and while you're actually talking (VAD). The interval clock keeps accumulating across IDLE ↔ LISTENING so it fires as long as the app is up.

v0.5.55

Motion tab — spontaneous voice interjection (the "alive on the desk" feature). New "Spontaneous voice" card: the voice counterpart of the idle random moves. Enter up to 12 text prompts (in a collapsible list — click to open/close them all), set an interval (30–3600 s, default 180 = once per 3 min), and enable. While the robot is armed and truly idle, it picks one prompt at random, feeds it to the LLM as if you spoke it, and speaks the reply out loud (asks you something, tells a fact, …). It then re-arms the mic so you can answer — but the re-arm respects the orb mute: muted → armed but deaf (you can't talk back); unmuted → you can reply. It never fires when disarmed (standby), while you're talking, or while it's speaking — and it can't overlap itself or a real turn (guarded by the existing listening/speaking signals). The injected line shows in the chat and the LOGS tab. Default off = zero behavior change. All live-apply + persist; restart-safe.

v0.5.54

Motion tab — idle random moves + breathing auto-restart toggle. New "Idle random moves" card: pick up to 8 gestures from the emotion library, set an interval (10–1800 s, default 120 = once per 2 min), and enable. While the robot is truly idle (procedural breathing in IDLE — never during listening / thinking / speaking), it pauses the breathing, plays one randomly-picked move, then resumes breathing immediately (no 30 s dead gap). The interval clock resets on every state change so a conversation doesn't eat into it. Default off = zero behavior change. Also a new "Idle breathing auto-restart" checkbox: off = breathing keeps running but stops re-creating itself on every state transition (the "why does it restart when it's already breathing" blip is gone); on = re-starts as before. Breathing is never stopped by this toggle — it only gates the re-start-when-already-working. All live-apply + persist; restart-safe.

v0.5.53

Fix: barge-in (voice interrupt) left the robot dead — stuck in listening forever. Interrupting the robot mid-speech primed the barge-in VAD but never primed the listening VAD with the lead-in speech, so it had no record of speech and could never fire utterance_ended → the buffer never flushed → the robot stopped responding. Fix: after interrupt(), prime the listening VAD with the barge-in lead-in frames so it can endpoint normally.

The loaded quant is now logged. The qwentts model: loaded ... line includes quant=Q4_K_M|Q8_0|BF16|F32 so you can see which quantization is actually running (matches the TTS-tab dropdown).

v0.5.52

Qwen TTS settings panel — quant dropdown + sampling knobs + in-tab preview. A new "Qwen TTS settings" card in the TTS tab (shown only when engine = qwentts_cpp) exposes the real audio-quality levers, styled like the faster-qwen3-tts demo but native HTML/JS — no Gradio, no second process, one engine. Quantization dropdown (Q4_K_M / Q8_0 / BF16 / F32) picks the GGUF level — higher = cleaner vocoder reconstruction, more VRAM. Switching unloads the old model first (frees VRAM before the new one loads, via the existing _do_load chokepoint), then loads the new quant. Default stays Q4_K_M (zero behavior change on existing installs — you opt into a higher quant by ear). The 7 sampling knobs (seed / max-new-tokens / top-k / temperature / top-p / repetition-penalty / do-sample) moved into the card; a one-click "Good defaults" button applies cooled sampling (temp 0.8 / top_p 0.9 / top_k 20 / rep 1.1) that kills the intermittent garble/noise. A preview player synthesizes sample text through the live engine (honors the quant + knobs just set) so you tune by ear without talking to the robot. All live-apply + persist; restart-safe.

Volume boost without the "crazy" voice — brickwall limiter replaces tanh. The old master-gain stage did x = np.tanh(x * gain), which engaged across the whole signal (even small values got compressed), so a 20 dB boost squashed everything into the cap — distorted/"crazy" voice and a loudness ceiling (20 dB wasn't actually louder than ~8 dB). Replaced with a soft-knee brickwall limiter: linear below the knee (full intonation, full gain — nothing touched), only peaks that would otherwise clip bend toward the ceiling. Net: louder + clean. No dynamics/compressor squashing (the 5 W speaker + maxed desktop-app volume now gets a real boost instead of being slammed into a tanh cap).

v0.5.51

Fix: app crashed (exit 250 / CUDA out-of-memory) on long TTS sentences. The qwentts non-streaming synth path fell back to the binding's codec_chunk_sec=24 default, so the DAC vocoder decoded an entire long sentence as one giant chunk. A ~14 s / 166-frame sentence allocated a ~1.2 GB CUDA compute buffer, committed via ggml's VMM cuMemCreate — a sub-second spike past an 11 GB card shared with the LLM, even with ~2 GB of steady-state VRAM free (nvidia-smi's ~1 s poll can't see it). The spike aborted the process: CUDA error: out of memoryFatal Python error: Aborted → exit 250. Pattern: one short reply speaks fine, then crash on the next reply containing a long run-on sentence (Hermes reasoning output is what exposed it). Fix: bound the vocoder chunk on the non-streaming path too, via the existing qwentts_codec_chunk_sec knob (default 1.0 s) — a 14 s sentence now decodes as 14 small ~86 MB chunks instead of one ~1.2 GB chunk. Audio is identical (chunks concatenate internally with 2 s left-context overlap, same as the streaming path that already did this and never crashed). The crash is the vocoder chunk size, not the gateway — any LLM (LM Studio / Hermes) that emits a long single sentence hit it; this fixes all of them. Streaming users were already safe; this makes batched playback safe too.

LLM idle-stream timeout default raised 8 s → 20 s (reasoning models). The watchdog force-closes the LLM stream when no token arrives for idle_stream_timeout_s. The old 8 s cap was too tight for reasoning models with reasoning baked in (can't be disabled — e.g. Qwen3.5-4B in LM Studio, run at "lower reasoning" via Hermes), which routinely have ~10 s+ gaps between streamed tokens while reasoning. 8 s cut the reply mid-sentence (the "...Each" cutoff: openai stream idle for 10.5s (cap 8.0s) — force-closing). 20 s is ~2× headroom over the observed gap and still well under the 300 s HTTP timeout; non-reasoning models are unaffected (gaps < 1 s). Bump to 30 in the dashboard if a harder reasoning step ever exceeds it.

v0.5.50

Output voice EQ — in-app 10-band graphic EQ + end filters. Replaces the EasyEffects (Linux PipeWire) chain the robot used to shape its voice: the app now pushes PCM straight to the robot speaker, so a pure-numpy/scipy RBJ biquad chain does the same shaping in-app — identical on Linux / Windows / macOS, applied per chunk before push_audio_sample. 10 graphic peak bands (31 Hz, 62, 125, 250, 500, 1k, 2k, 4k, 6k, 8 kHz) as classic vertical faders — every fader works now (the old first band was a disabled high-pass; a dead slider). Separate end filters — high-pass, low shelf, high shelf — pulled out of the graphic row into their own labeled panel (still user-tunable, just not disguised as a band). Master gain + tanh soft-clip so a boost doesn't hard-clip. Presets (Clear & loud / Warm / Radio) move the 10 gains + the two shelf gains. Filter state carries across chunks (no edge clicks) and resets per reply.

The 16 kHz ceiling is the hardware, not a knob. Confirmed against the Reachy Mini SDK: the ReSpeaker XVF3800 DAC, the GStreamer capsfilter(rate=16000), and the .asoundrc dmix slave all pin the speaker to 16 kHz (AudioBase.SAMPLE_RATE = 16000). Nyquist = 8 kHz, so the top graphic band is 8 kHz — this is the full-range EQ for the robot; a 16 kHz band would be inaudible (the speaker can't reproduce it). The 48 kHz you may see in the SDK is the AEC (mic echo-cancellation) path, not the speaker.

LOGS tab now shows the full LLM reply. The conversation loop used to log a 200-char preview of the assistant answer ending in "…", so you couldn't see what the model actually said when debugging. It now logs the whole reply; the thinking length is still reported as a count alongside.

v0.5.45

Every hard-coded tunable is now in the dashboard. New AUDIO tab (XVF3800 chip config — AGC / NS / NN / gamma / MGSCALE — with a live "Re-apply now" button, no restart). Advanced <details> cards in Conversation (mic gating / self-barge-in: post-TTS silence, barge lead-in, RMS threshold, PTT threshold), Motion (breathing z-amplitude / frequency / antenna sway, blend, idle delay, head-body yaw clamp), STT (partial-transcription window), and Hermes (stop timeout, log poll interval, log buffer size). Every new knob defaults to its old hard-coded value — zero behavior change unless you move it. config.yaml is left untouched; missing fields fall back to pydantic defaults.

LLM completion-api as a first-class engine. Separate CompletionApiClient (OpenAI-compat read surface: /v1/models list + ping) — not mixed into OllamaClient. The LLM-tab model picker and the STATUS probe now dispatch by llm.engine, so completion_api (LM Studio / llama.cpp / vLLM / OpenAI) lists its own models instead of silently showing a stale "saved, not on server" entry. The golden Ollama chat path is untouched.

Thinking on/off works on the completion-api path. think=false now sends reasoning_effort + (for the default none) chat_template_kwargs: {enable_thinking: false} — the levers LM Studio and vLLM actually honor (verified: qwen3.5 stops reasoning, gemma unaffected, no 400). New llm.reasoning_effort knob (none / minimal / low / medium / high) lets you match whatever the server accepts — switch to minimal for OpenAI direct so chat_template_kwargs is omitted and the 400 is avoided.

Mic mute / orb / daemon wobble. Orb click mutes/unmutes the mic (s2s onMicTap); arm/disarm has its own buttons. Head wobble uses the daemon-native robot.enable_wobbling (playback-synced) driven by the _speaking_until window, not the LISTENING state.

v0.5.43

Natural human speech across all TTS engines: the full LLM reply is synthesized and played as one continuous utterance (batched playback) instead of per-sentence chunks, so prosody carries across sentence boundaries. Made the Reachy 16 kHz constraint explicit and added a warning on oversized rates. Hotfix line (v0.5.43.2 → v0.5.43.6): removed garbage files that broke the Windows install, and fixed a dashboard-save ReferenceError that broke every TTS engine except edge.

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