This guide takes you from nothing to a Furhat robot that holds a real spoken conversation. It covers the Furhat Realtime API, the WebSocket API shown in the Furhat demo video. The API streams audio, video, and robot commands back and forth in real time between the robot and your Python code.
Whoever reads this page gets: the full setup, the starter code files, and an in-depth explanation of all five workflows from the video (connecting, LLM dialogue with lip-sync, local models, computer vision, and telepresence).
furhat-realtime-api package (version 0.1.3). Anything that could not be verified is marked UNVERIFIED. Do not build on unverified items until you confirm them on the real robot.python --version in a terminal.02_chatbot.py).Open a terminal in the folder that holds the starter files (the files folder linked at the bottom of this page), then follow each step in order.
python -m venv venv.\venv\Scripts\activate
On Mac or Linux:
source venv/bin/activate
You will see (venv) appear at the start of your terminal line. That means it worked.pip install -r requirements.txt
This installs three things: furhat-realtime-api (the official Furhat client), openai (for the chatbot), and python-dotenv (for reading the .env settings file).copy files\.env.example .env
On Mac or Linux:
cp files/.env.example .env
(If you downloaded the files separately, just copy .env.example to .env in the same folder as the scripts.).env in any text editor. It looks like this:
OPENAI_API_KEY=put-your-key-here
FURHAT_HOST=127.0.0.1
FURHAT_AUTH_KEY=
Replace put-your-key-here with your real OpenAI key. Leave FURHAT_HOST as 127.0.0.1 while you use the virtual robot. Leave FURHAT_AUTH_KEY empty unless the robot asks for a key. Save the file.python 01_hello_world.py
Furhat says hello, turns its neck light blue, nods, then asks you to say something and repeats it back. If this works, your connection is good.python 02_chatbot.py
Talk to Furhat. It listens, sends your words to OpenAI, and speaks the answer. Say "goodbye" when you are done..env:
python 01_hello_world.py --host=192.168.0.52 --auth_key=mykey123
Replace the IP with the robot's real address. Drop --auth_key if the robot does not need one.The idea. The Realtime API is a WebSocket server that runs on the robot (or on the virtual robot inside the SDK). Your Python script opens a connection to it, and from then on the two sides exchange messages in real time: your script sends commands (speak, move the face, change the light), and the robot sends back events (I heard speech, here is a camera frame, the user moved).
127.0.0.1 means "this computer", which is where the virtual robot lives. A physical robot has its own IP address on the network.connect(). The client opens the WebSocket and shakes hands with the robot. If the robot requires authentication, the client sends the auth key you passed in.request_speak_text(...). Each method sends one JSON message over the WebSocket and, for the synchronous client, waits for the robot's answer.disconnect() when done. Always do this, even if something crashed (the starter scripts use try/finally for exactly this reason).from furhat_realtime_api import FurhatClient
furhat = FurhatClient("127.0.0.1") # or the robot's IP
# furhat = FurhatClient("192.168.1.50", "YOUR_AUTH_KEY") # if a key is needed
furhat.connect()
furhat.request_speak_text("Hello, I am Furhat.")
furhat.disconnect()
The package ships two clients. FurhatClient is synchronous: each call blocks until the robot answers, which makes simple scripts easy to read. AsyncFurhatClient is asynchronous: calls return immediately and events arrive as they happen, which is what you want when several streams (audio in, audio out, camera) must run at the same time. Start with the sync client; move to async when you add streaming.
http://<ROBOT_IP>:9000/), a Realtime API settings block, per-network auth scopes, the raw endpoint ws://<ROBOT_IP>:9000/v1/events, and the raw request.auth payload shape were not confirmed. If you ever need the raw WebSocket protocol instead of the Python client, confirm these on the real robot first.The idea. A talking robot needs three jobs done: hear the user (speech-to-text), think of an answer (the LLM), and speak it (text-to-speech). The Realtime API lets you arrange these jobs however you like, and the robot's mouth moves in sync with whatever audio comes out.
The robot has built-in speech recognition. request_listen_start() waits until the user finishes speaking and returns the recognized text as a plain string. Your loop is then: listen, send the text to the LLM, speak the answer.
furhat.request_speak_text("What would you like to talk about?")
while True:
user_text = furhat.request_listen_start() # blocks, returns text
if not user_text.strip():
furhat.request_speak_text("Sorry, I did not catch that.")
continue
if user_text.lower() in {"goodbye", "bye"}:
furhat.request_speak_text("Goodbye!")
break
reply = ask_llm(user_text) # your OpenAI / local LLM call
furhat.request_speak_text(reply) # robot speaks, lips move automatically
You can tune the listening behavior (all verified parameters):
furhat.request_listen_config(languages=["en-US"]) # recognition language
furhat.request_listen_start(
partial=False, # True also returns in-progress partial results
no_speech_timeout=8.0, # give up after 8 seconds of silence
end_speech_timeout=1.0, # user is done after 1 second of silence
)
This is exactly what 02_chatbot.py does, with OpenAI as the LLM and the full conversation history passed along so the robot remembers context.
When your text-to-speech runs outside the robot (a cloud voice or a local model), you stream the audio file and the face lip-syncs by itself. This is the mechanism behind the video's karaoke demo: any audio you send plays through the robot's speaker with only about 0.2 seconds of delay, and the mouth matches it.
furhat.request_speak_audio(
url="https://your-server/audio.wav", # any audio URL the robot can reach
text="transcript for context", # optional
lipsync=True, # mouth moves with the audio
abort=False, # True cuts off whatever is currently playing
wait=False,
)
spleeter separate -i song.mp3 -p spleeter:2stems -o out/vocals.wav on a small web server your robot can reach.request_speak_audio(url=<vocals URL>, lipsync=True). The face sings the vocals in real time.Simpler alternative: stream the full mixed song the same way. The lip-sync still follows the vocal frequencies, and you skip the splitting step entirely.
The idea. Instead of sending the user's voice to cloud services, you run everything on your own computer: speech-to-text, the LLM, and text-to-speech. Nothing leaves the lab. This is exactly what the video demonstrates: the whole pipeline on a MacBook next to the robot.
The key architectural point. In the video's diagram, user speech flows from the robot to the computer. You use the robot's microphone, not your laptop's. The robot streams raw audio to your script, your script runs its models, and the answer streams back to the robot's speaker.
Robot mic --(request.audio.start)--> your computer --(request.speak.audio)--> Robot speaker
|- STT: faster-whisper (local)
|- LLM: Ollama, e.g. Llama 3 or Mistral
|- TTS: Piper or Coqui (local WAV)
await furhat.request_audio_start(sample_rate=16000, microphone=True, speaker=False)
# audio chunks arrive as response.audio.data events
# ...
await furhat.request_audio_stop()faster-whisper running on your machine. No audio ever goes to the cloud.ollama run llama3) and get the answer back.furhat.request_speak_audio(url="http://<your-pc>/reply.wav", lipsync=True)request_listen_start() for the hearing step. Run only the LLM and TTS locally. You keep the brain on-premise with a third of the code.The idea. The robot's camera feed streams out through the API into your Python code, where you run real vision models (the video uses YOLO object detection on a GPU). What the model sees then drives what the robot does: look at the person, react, change its light.
There is no request.vision.stream event. The real API is:
frame = furhat.request_camera_once() # one frame, returned directly
# Continuous feed (async client): frames arrive as response.camera.data
await furhat.request_camera_start()
# ... process frames as they arrive ...
await furhat.request_camera_stop()
Similarly, request_users_once() (or the request.users.start stream) gives you tracked-user data: who is in front of the robot and where they are.
The video's dashboard runs yolov8m.pt (YOLOv8-medium) on a CUDA GPU, with the confidence threshold set to 0.25 and classes like person, bicycle, car, and motorcycle selected.
from ultralytics import YOLO
model = YOLO("yolov8m.pt") # same model family as the video's demo
def process_frame(frame):
results = model(frame, conf=0.25)
for r in results:
for box in r.boxes:
label = model.names[int(box.cls)] # e.g. "person"
x1, y1, x2, y2 = box.xyxy[0] # where it is in the frame
furhat.request_attend_user(user_id="closest") # look at the nearest person
furhat.request_attend_location(x=0.0, y=0.0, z=1.0) # or look at a 3D point
furhat.request_gesture_start("Nod") # react with a gesture
furhat.request_led_set("green") # signal state with the neck light
Example behavior: when a "person" box appears, call request_attend_user("closest") so the robot turns to face them, then greet them with request_speak_text(...).
The idea. A remote human operator "becomes" the robot: their voice comes out of its speaker, their facial expressions and gaze drive its face, and they see and hear the room through its camera and microphones. The video shows this working from a VR headset (C# client) and from a plain web browser (JavaScript client). Any language with WebSocket support can do it.
| Direction | Stream | What it does | API piece (verified) |
|---|---|---|---|
| Operator to robot | Operator speech | The operator's voice plays through the robot with lip-sync | request.speak.audio with lipsync=True |
| Operator to robot | Facial and gaze parameters | The operator's expressions drive the robot's face live | request.face.params, request.face.headpose |
| Robot to operator | Robot microphone | The operator hears the room | request.audio.start (microphone=True) |
| Robot to operator | Camera feed | The operator sees through the robot's eyes | request.camera.start |
# Continuous facial parameters from the operator's face tracker
# (e.g. MediaPipe landmarks mapped to Furhat's parameter set)
furhat.request_face_params(params={...})
# Head orientation follows the operator's head
furhat.request_face_headpose(yaw=0.1, pitch=0.0, roll=0.0, relative=True)
In the video's live demo, the operator's video feed carries a face-detection box, and a second view shows the facial landmark mesh (eyes, brows) being tracked. Those landmarks are exactly what gets streamed as facial parameters.
request_face_params / request_face_headpose every frame, and stream the operator's voice with request_speak_audio(..., lipsync=True).telepresence folder in the examples repo was not confirmed. Start from the verified examples at github.com/FurhatRobotics/realtime-api-examples; openai_realtime.py shows the audio-bridging pattern that telepresence builds on."Furhat Realtime API Demo | Gabriel Skantze" (Furhat Robotics, 12:07). This is the video all five workflows above come from. Watch it here:
Direct link: https://youtu.be/WtL7tHFHFJo
Everything you need to run this guide. Click to open or download each file:
.env private. It holds your OpenAI key. Never share it, upload it, or put it in a public folder.| Method | What it does |
|---|---|
FurhatClient(host, auth_key) | Create the sync client |
AsyncFurhatClient(host, auth_key) | Create the async client |
connect() / disconnect() | Open / close the connection |
request_speak_text(text, wait, abort) | Speak text with the robot's voice |
request_speak_audio(url, text, lipsync, abort, wait) | Stream audio with automatic lip-sync |
request_speak_stop() | Stop speaking immediately |
request_listen_config(languages, phrases) | Set the speech recognition language |
request_listen_start(...) | Listen once, returns recognized text |
request_voice_config(...) | Choose the voice (id, name, gender, language) |
request_attend_user(user_id) | Look at a user ("closest" by default) |
request_attend_location(x, y, z) | Look at a point in space |
request_gesture_start(name, ...) | Play a gesture, e.g. "Nod" |
request_face_params(params) | Set facial parameters directly |
request_face_headpose(yaw, pitch, roll, relative) | Turn the head |
request_face_reset() | Reset the face to neutral |
request_led_set(color) | Set the neck LED color |
request_users_once() | Get current tracked-user data |
request_camera_once() | Grab a single camera frame |
request_audio_start(sample_rate, microphone, speaker) (async) | Stream raw audio from the robot |
request_camera_start() / request_camera_stop() (async) | Start / stop the camera stream |
FurhatClient..env and check OPENAI_API_KEY.pip install fails. Make sure the virtual environment is activated (you see (venv) in the terminal) and that you have Python 3.10 or newer.AsyncFurhatClient.02_chatbot.py is the starting point. Instead of one fixed apology, the LLM writes a fresh repair message each time, informed by the conversation history.furhat.request_speak_text(...) call with the same fixed apology text every time.