Open source · step by step

Build your own Lumi

A voice concierge that hears a question, thinks, and answers out loud, with a face. Built on the computer you already have.

MIT licensed Can run fully offline macOS · Windows · Linux ~1,000 lines of Python Events, home, front desk, help desk…

Where this comes from

One person, an AI coding partner, and four weeks.

The original Lumi greeted people at an event: scan your badge, ask where your session is, and she tells you out loud and shows you the way. Everything ran on one box in the room, so no visitor's voice ever left it. This site is the recipe, rebuilt small enough to run on your own machine.

4 weeksidea to working kiosk
191commits, written with an AI coding agent
866automated tests in the original
≈1.5 squestion to answer with this kit (tested)
What you're building

Five steps, in a loop.

Wake Hear Think Speak Show
1 · Wake

A badge scan starts a visit

Press Enter or scan a badge.

In minilumi: cli.py
Pick your hardware

Same Lumi, three ways to run her.

Choose one. Every command on this page changes to match. You can switch at any time from the bar at the top.

Or let an AI coding partner drive

Build it with Claude Code.

The original Lumi was written with Claude Code, an AI coding agent that works in your terminal. It can build this kit with you too: it reads the code, runs the commands, explains what it's doing, and asks before it changes anything. Switch to Claude Code here (or with Build with in the bar at the top), and every step below gets a prompt you can paste.

Steps show
1 · Install it

macOS / Linux:

curl -fsSL https://claude.ai/install.sh | bash

Windows (PowerShell):

irm https://claude.ai/install.ps1 | iex

You need a Claude subscription (Pro or Max) or an Anthropic API account; it asks you to log in the first time. On Windows, install Git for Windows first.

2 · Open the kit

Get the kit and start Claude Code in its folder:

git clone https://github.com/ckellum/build-your-own-lumi
cd build-your-own-lumi
claude

It reads CLAUDE.md first, so it already knows the files, the presets and how to run the tests.

3 · Use the shortcuts

The kit comes with three slash commands. Type one and press Enter:

/setup
Installs everything for your preset, runs the tests, and checks each model.
/my-event
Paste your agenda, room list or attendees, in any format, and it writes Lumi's event files for you.
/make-it-yours
A new name, personality and voice, then a test run to hear it.
Good habits
  • Read what it's about to change before you say yes. It shows you the edit or command first.
  • Never paste an API key into the chat. Ask it how to set the key as an environment variable, then set it yourself.
  • Ask it to explain. "Why did that fail?" and "Walk me through listen.py" are great prompts. You'll learn the code, not just get it running.
  • Keep real attendee data in minilumi/my-event/. It's git-ignored, and CLAUDE.md tells Claude Code never to commit it.
Build it

Nine steps. Each one works on its own.

Set the Detail switch to Beginner for the idea, Intermediate for how it works, or Expert for what bit us. Set Build with to Claude Code to get a prompt for each step (set it up first).

0Setup

Get the code running

Install one tool, download the kit, and run its tests. Five minutes.

Ask Claude Code
/setup laptopworkstationcloud

Then answer its questions. It installs uv if you don't have it, syncs the kit for your preset, and runs the tests.

The idea

uv is a tool that installs Python and everything Lumi needs, without touching the rest of your computer. Copy each block into a terminal (Terminal on macOS, PowerShell on Windows).

How it's laid out

minilumi/ is one small Python package: listen.py (when you stop talking), hear.py, think.py, speak.py, visit.py (the conversation loop) and screen.py (the face). config.toml holds three presets: workstation, laptop and cloud.

Under the hood

Python 3.11+. Base dependencies are just numpy, sounddevice and anthropic; local speech is the optional local extra. HTTP to model servers is the standard library, so any OpenAI-compatible server works.

Install uv:

# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

Get the kit:

git clone https://github.com/ckellum/build-your-own-lumi
cd build-your-own-lumi/minilumi
uv sync --extra local
uv sync

On Linux, also install the audio library: sudo apt install libportaudio2

Check it works: uv run pytest -q ends with 62 passed.
Watch out for
  • On Windows, open a new terminal after installing uv so it's on your PATH.
  • The first local run downloads the speech models (a few hundred MB), once.
1Hear

Turn speech into text

A speech-to-text model writes down what the visitor said.

Ask Claude Code
Run uv run minilumi check --preset laptopworkstationcloud and look at the hear line. If it fails, explain why in plain words and fix what you can.

Paste it into Claude Code. The commands below are what it will run, if you'd rather do it yourself.

The idea

Lumi records you until you stop talking, then hands the recording to a model that turns it into words. Those words are what she "heard".

How it works

hear.py takes 16 kHz mono audio. With kind = "whisper-local" it runs faster-whisper in-process; with kind = "openai" it posts a WAV to any /v1/audio/transcriptions endpoint.

What we learned

The original used NVIDIA Parakeet behind an OpenAI-style server: fast and accurate for English. Accuracy problems were almost always the microphone, not the model: a directional mic pointed at the visitor beat every software fix.

Nothing to install beyond step 0. The laptop preset uses Whisper base.en on your CPU.

Run any server with an OpenAI-style transcription endpoint (for example Speaches), then point minilumi at it in config.toml:

[presets.workstation.hear]
kind = "openai"
model = "whisper-1"
base_url = "http://127.0.0.1:8084/v1"

macOS / Linux:

export OPENAI_API_KEY=sk-...

Windows (PowerShell):

$env:OPENAI_API_KEY="sk-..."
Check it works: uv run minilumi check --preset laptopworkstationcloud prints ✓ hear.
Watch out for
  • Lumi must not hear herself. minilumi throws away whatever the mic caught while she was speaking; a headset or directional mic helps more.
  • macOS asks for microphone permission for your terminal the first time.
2Think

Answer with a language model

The model reads the event's facts and the question, and writes a short spoken answer.

Ask Claude Code
Set up the language model for the laptop preset: check that Ollama is installed and running, pull the model named in config.toml, then run the check until think passes.My llama.cpp server is at http://127.0.0.1:8080. Point the workstation preset's think step at it, then run the check until think passes.Set up the cloud preset's think step. Don't ask me for my API key: tell me how to set ANTHROPIC_API_KEY myself, then run the check until think passes.

Paste it into Claude Code. The commands below are what it will run, if you'd rather do it yourself.

The idea

A language model is great at conversation but knows nothing about your event. So Lumi gives it a cheat sheet first: the rooms, the sessions, where lunch is. Then it answers using only that.

How it works

event.py builds the system prompt from your CSV files: persona, event facts, rooms and sessions. The visitor's name, the time and their next sessions go into their first message, not the system prompt. think.py streams the reply back word by word.

What we learned

Keep the system prompt byte-for-byte identical for every visitor. Then the model server can reuse its prompt cache and skip re-reading thousands of tokens. Anything per-person in the system prompt breaks the cache for everyone. Small models sometimes "think out loud" in <think> tags; minilumi asks the server not to, and strips any that slip through.

Install Ollama, then download a model:

ollama pull qwen3:1.7b

More RAM? An 8B model gives better answers (a much bigger download):

ollama pull qwen3:8b

If you pull the 8B model, change model under [presets.laptop.think] in config.toml to qwen3:8b.

Honest note: we tested this preset with the same model served by llama.cpp's llama-server (any OpenAI-compatible server works: set base_url to it), not through Ollama itself.

Serve a model with llama.cpp's OpenAI-compatible server:

llama-server -m your-model.gguf --host 127.0.0.1 --port 8080

macOS / Linux:

export ANTHROPIC_API_KEY=sk-ant-...

Windows (PowerShell):

$env:ANTHROPIC_API_KEY="sk-ant-..."

The cloud preset uses Claude Haiku: fast and inexpensive for short spoken answers.

Check it works: uv run minilumi check --preset laptopworkstationcloud prints ✓ think.
Watch out for
  • The very first answer is slow while the model loads. Later ones are much faster.
  • Ask for "one to three short sentences, no lists". Bullet points sound terrible read aloud.
3Speak

Give her a voice

A text-to-speech model turns each sentence into audio.

Ask Claude Code
Run the check for the laptopworkstationcloud preset until speak passes. Then suggest five Kokoro voices I could try and show me where to change it.

Paste it into Claude Code. The commands below are what it will run, if you'd rather do it yourself.

The idea

Lumi doesn't wait for the whole answer. As soon as the first sentence is written, she starts saying it while the model writes the next one. That's why she feels quick.

How it works

visit.py regroups the streamed text into sentences; speak.py turns each into audio with Kokoro (in-process via kokoro-onnx, or any /v1/audio/speech server that returns WAV).

What we learned

Names are where TTS falls apart. Keep a small pronunciation list (word → respelling) and apply it right before speaking, never to the captions. Some phrases are simply misread ("Uh oh" came out wrong; "Oh no" didn't). Test every scripted line by listening to it.

Nothing to install: the laptop preset runs Kokoro on your CPU. Voices are set with voice in config.toml (af_heart is Lumi's).

Run a Kokoro server with an OpenAI-style speech endpoint (for example Kokoro-FastAPI), and set speak.base_url to it.

Uses the same OPENAI_API_KEY as step 1, with the gpt-4o-mini-tts model.

Check it works: uv run minilumi check --preset laptopworkstationcloud prints ✓ speak.
Watch out for
  • Times and numbers: write "10:00 AM", not "10:00". The kit formats times for you.
  • Speakers that come up muted after a reboot. Set the volume when Lumi starts.
4Turn-taking

Know when someone has finished talking

The hardest part of a voice kiosk isn't the AI. It's knowing when it's Lumi's turn.

Ask Claude Code
Run minilumi on samples/hello.wav with the laptopworkstationcloud preset, muted and with no screen. Explain each line of the output, then walk me through how listen.py decides I've finished talking.

Paste it into Claude Code. The commands below are what it will run, if you'd rather do it yourself.

The idea

Lumi listens to how loud the room is all the time. When someone is clearly louder than the room, she starts recording. When they've been quiet for a moment, she stops and answers.

How it works

listen.py measures the room as a low percentile of the last 1.5 s of loudness. A turn starts at 4× the room level, continues while it's above 2×, and ends after 0.7 s below that. Anything shorter than 0.25 s, or never really loud, is dropped (coughs, clicks).

What we learned

We recorded 75 seconds of real crowd noise and replayed it through the endpointer over and over while tuning. The room level must be re-measured continuously; one measurement at startup goes stale the moment a crowd walks in. Known limit: someone speaking softly in a loud crowd won't be heard. Lowering the bar let stray "yeah"s from passers-by in. We kept the bar high.

Check it works: no mic needed:
uv run minilumi run --preset laptopworkstationcloud --input samples/hello.wav --badge EX-0001 --mute --no-screen
prints a You: line with the recorded question, then Lumi's spoken answer as Lumi: lines.
Watch out for
  • After 30 s of silence Lumi asks "Still there?", and ends the visit 15 s later. Tune both in visit.py.
  • Fans and air conditioning raise the room level. Point the mic at the visitor, away from them.
5Face

A face that talks

A browser page shows Lumi, her captions, and what she heard.

Ask Claude Code
Start minilumi with the laptopworkstationcloud preset and the avatar page, tell me what to open in my browser, and explain how the mouth stays in sync with the voice.

Paste it into Claude Code. The commands below are what it will run, if you'd rather do it yourself.

The idea

The face is just a web page. Python tells the page what's happening ("listening", "speaking", "here's the sentence") and the page animates the mouth to match the voice.

How it works

screen.py serves ui/ and pushes messages with Server-Sent Events. Each spoken sentence arrives with its envelope, the loudness of the audio every 20 ms, and the mouth's height follows it.

What we learned

When sentences queue up, a sentence's audio starts in the future (after the ones ahead of it). The original screen guarded against clock skew with a symmetric clamp, which treated "starts in 8 seconds" as an error. Lip sync silently broke for every multi-sentence reply, and that got through eleven separate code reviews. Only a review of the whole branch caught it. Clamp asymmetrically: tolerate a little lateness, a lot of earliness.

uv run minilumi run --preset laptopworkstationcloud
# then open http://127.0.0.1:8765/
Check it works: press Enter in the terminal; the face says hello and the mouth moves with the voice.
Watch out for
  • For a real kiosk, open the page full screen: google-chrome --kiosk http://127.0.0.1:8765/
6Your event

Teach her about your event

Four small files are everything Lumi knows.

Ask Claude Code
/my-event Here's my event: (paste your agenda, room list and attendees, or just describe it)

It asks about anything it can't tell from what you pasted (directions to each room, for example), writes the four files into my-event/, and tests a greeting.

The idea

Copy example-event/ to my-event/, open the copy in a spreadsheet or text editor, and replace the made-up "Example Con" with your own event: rooms with spoken directions, sessions, attendees, and any facts in event.txt. Then set event_dir = "my-event" in config.toml. (my-event/ is git-ignored, so your real data stays out of git.)

How it works

rooms.csv: name, floor, directions. sessions.csv: title, room, start, end, speaker. attendees.csv: badge, name, and their sessions separated by ;. A scanned badge greets that person with their next session.

What we learned

Build a "pretend it's 8 AM on day one" clock for rehearsals, so you can test what Lumi says at every point of the event. In minilumi that's the now= argument of Visit. And keep the last good copy of the data: a half-saved CSV at the wrong moment shouldn't leave Lumi knowing nothing.

Check it works: run the step 4 command with your own badge ID; the greeting names your next session.
Watch out for
  • Badge IDs ignore upper/lower case and spaces around them, so ex-0001 works.
  • Never put private data in a public repo. Keep your real CSVs out of git.
7Make it yours

Give her a personality

The difference between a demo and something people talk about.

Ask Claude Code
/make-it-yours Make her a cheerful lighthouse keeper named Pearl who calls everyone "sailor"

Swap in your own idea. It writes persona.txt, picks a matching voice, and runs a test so you can hear her.

The idea

Change her name, tone and voice. Have her greet people by nickname. The original had a pronunciation list, playful greetings, and a few surprise "shows" for specific badges.

How it works

Personality is the PERSONA line at the top of event.py. For nicknames, add a column to attendees.csv, read it in load_event (event.py), and use it in Visit._greeting.

What we learned

Scripted moments should skip the language model: match the badge before calling it, then play pre-written lines with pre-rendered audio and a scripted animation. Every word is reviewed in advance, and nothing can improvise its way into an awkward moment in front of a crowd.

Watch out for
  • Make "once only" surprises truly once: remember that they've played on disk, not just in memory.
8Kiosk-proof

Make it survive a whole event

Demos run for five minutes. Kiosks run for three days.

Ask Claude Code
Set minilumi up to start when this computer starts and restart itself if it ever stops, using the laptopworkstationcloud preset. Show me every change before you make it, and how to undo it.

On a kiosk machine, also ask it for a checklist: sleep, screen saver and automatic updates off.

The idea

Have Lumi start by herself when the computer starts, and restart herself if she ever stops. The simplest way: a terminal that opens at login and runs her in a loop.

How it works

A barcode scanner types into whichever window has focus, so Lumi's terminal must keep focus. Wrap the command in a loop so a crash restarts it in two seconds.

What we learned

On Linux, the original read the scanner directly (evdev, with a udev rule for permissions), so focus didn't matter. It also had a self-check command that failed on a muted speaker, kept transcripts out of logs entirely, and capped the GPU clock after the machine powered itself off under sustained load.

A restart loop (macOS / Linux):

# change this to wherever you cloned the repo
cd ~/build-your-own-lumi/minilumi
while true; do uv run minilumi run --preset laptopworkstationcloud; sleep 2; done

Windows (save as lumi.bat and put a shortcut in shell:startup):

rem change this to wherever you cloned the repo
cd %USERPROFILE%\build-your-own-lumi\minilumi
:loop
uv run minilumi run --preset laptopworkstationcloud
timeout /t 2
goto loop
Watch out for
  • Print QR badges on matte paper, about 5 cm, with high error correction. Codes on a phone screen scan badly.
  • Turn off sleep, screen savers and automatic updates on the kiosk machine.
  • Never write what visitors say to a log file. If you must debug, log to the screen only.
  • Rehearse with the real mic, the real speaker and real background noise. Lumi's first full run found two bugs that hundreds of tests didn't.
Not just events

It's a voice assistant. Point it at anything.

Lumi only knows what's in four small files plus a personality. Swap them and she becomes something else. The kit ships a made-up example of a personal assistant you can try right now.

Try it: your personal assistant

examples/my-day/ is a made-up household: today's calendar (times without a date mean today), places, the Wi-Fi password, trash day, the grocery list, a quick dinner recipe, and a persona.txt that makes her a kitchen-counter assistant. Set it in config.toml:

event_dir = "examples/my-day"
uv run minilumi run --preset laptopworkstationcloud --input samples/my-day.wav --badge ME --mute --no-screen
What we got: “Hi Alex! Next up for you: Focus time: quarterly report at 10:00 AM, Home Office.” Then, asked about the afternoon and dinner: “This afternoon you have a dentist appointment with Dr. Osei at 3:00 PM at the Maple Street Clinic. For dinner, try the garlic pasta.”
Your day

Personal assistant

Calendar in sessions.csv (export it from your calendar app), places in rooms.csv, the household's facts in event.txt. Badge in with a key-ring QR code, or just press Enter.

Reception

Front desk

Visitors scan in; attendees.csv says who they're meeting and rooms.csv how to get there. A receptionist persona, and an after-hours message in event.txt.

Support

IT help desk

Put the top twenty questions and their fixes in event.txt: resetting a password, joining the Wi-Fi, the printer. Rooms become “where to pick up a loaner laptop”.

Guide

Museum or store

Galleries or aisles in rooms.csv with spoken directions, exhibit or product facts in event.txt, guided tours as sessions.

Hands busy

Kitchen or workshop

Recipes, checklists or machine procedures in event.txt, read out step by step while your hands are full. (Check safety-critical steps yourself.)

Access

A kiosk anyone can use

For people who can't read a screen or use a touchscreen: everything works by voice, and the screen is a bonus.

Learning

Study buddy

Class schedule as sessions, course notes in event.txt, and a persona that quizzes you instead of answering.

Languages

Conversation partner

One line in persona.txt: “Always answer in simple Spanish.” Pick a matching Kokoro voice.

Going further

The data is static on purpose: easy to read, easy to trust. To make her do things (set a timer, add to the grocery list, turn on a light), add a small tool step in visit.py: ask the model to emit a tag like [timer:10], act on it in Python, and strip it before speaking. The original Lumi drives its screen exactly this way.

Lessons from the build

Six things that broke, so yours won't.

Tap a card for what happened, why, and the fix.

01

The 21-second hello

The first answer took 21 seconds. Nobody waits that long at a kiosk.

What happened →
Why
The model had to read the whole event cheat sheet (thousands of tokens) before saying a word, and nothing told it to keep that work.
Fix
An identical system prompt for every visitor, a warm-up request at startup, and server settings that save the prompt cache at the right point. First answers dropped to about half a second.
02

The bug that passed eleven reviews

Lip sync broke for every reply longer than one sentence.

What happened →
Why
A "sanity" clamp treated audio scheduled to start a few seconds in the future as clock error, and threw the timing away. Each small change looked fine on its own.
Fix
An asymmetric bound, plus the habit of reviewing the whole branch at the end, not only each piece.
03

One voice in a crowd

Quiet room: perfect. Busy hallway: she answered strangers.

What happened →
Why
Fixed loudness thresholds can't work when the background changes minute to minute.
Fix
Measure the room continuously, require speech to be several times louder than it, and require a real peak. Tuned by replaying recorded crowd noise, with a shotgun mic aimed at the visitor.
04

The computer that turned itself off

Twice, under heavy load, the whole machine just powered off. No logs.

What happened →
Why
Sustained AI load caused power spikes on early firmware.
Fix
Cap the GPU clock at boot and keep test load to one request at a time. Plan for the hardware, not just the software.
05

QR codes vs. screens

Badges on a phone screen took forever to scan.

What happened →
Why
Glare and screen refresh confuse the scanner.
Fix
Printed, matte, about 5 cm, high error correction. They scan instantly.
06

The silent speaker

After a reboot, Lumi was talking. Nobody could hear her.

What happened →
Why
The USB speaker came up at 0% volume after every boot.
Fix
Set the volume when Lumi starts, and a self-check that fails loudly on a muted speaker.
Honest results

What we actually ran.

Each preset was run end to end: a recorded question in, a spoken answer out. "Question to answer" is from the transcript appearing to Lumi's first sentence being ready.

PresetMachineHearThinkSpeakQuestion to answerResult
workstationNVIDIA DGX Spark, 128 GB unified memoryParakeet serverQwen3.8 Flash-Next, llama.cppKokoro server≈ 1.3 s✓ correct room and directions
laptopSame machine, speech on the CPU (laptop stand-in)faster-whisper base.enQwen3 1.7B, llama.cppkokoro-onnx≈ 1.5 s✓ correct room
cloud—————not tested yet

macOS and Windows: the automated tests run on both on every push. A full voice run on them is not tested by us yet. Tried it? Tell us how it went.