Dash: An Open-Source Home Assistant Built on the Gemini 3.8 Live API A developer released Dash, an open-source ambient display for tablets and kiosk screens, with a "Hey Dash" voice assistant built on Google's Gemini 3.8 Live API over a single stateful WebSocket. The project keeps the API key off the device by proxying audio through an Express server, and uses the Live API's built-in barge-in, input/output transcription, tool calls and Google Search grounding instead of stitching together separate speech-to-text, LLM and text-to-speech services. Dash is MIT-licensed and configured through one config.json. I wanted a Nest Hub that I could actually program. Something on the kitchen wall that shows the time, the weather, my teams, what's cooking and what's playing, and that answers when I talk to it. That is Dash https://github.com/xavidop/dash : an open-source ambient display for any tablet or screen in kiosk mode, one config.json , six languages, free data sources, MIT. Most of Dash needs no account at all. The part that does is the one I want to talk about here: the "Hey Dash" voice assistant , which runs on Google's realtime voice model, Gemini 3.8 Live , through the Gemini Live API . If you have ever wanted to build a voice assistant without stitching speech-to-text, an LLM and text-to-speech together yourself, this is the article. You will see how the Live API works at the protocol level, how Dash keeps the API key off the device, how tool calls turn speech into shopping list items and timers, and what a question costs. Everything below is the real code from the repository, as of October 2026 , against gemini-3.8-live . Dash is an Express server plus a Vite and vanilla TypeScript front end, built to be left alone on a wall for weeks. Panels rotate every minute: Home with a "Today" strip weather warnings, holidays, your teams playing today, reminders, bin day, the cheapest electricity hour in Spain, the moon , Agenda from any iCal link, news, markets with sparklines, Spotify with synced lyrics, a kitchen that suggests six recipes a day and walks you through them one step at a time, and live sports boards that take over the screen when one of your teams plays, with a celebration when they score. Everything on screen also has an HTTP endpoint, so an iOS Shortcut or Home Assistant can add to the shopping list or start a timer. And then there is the voice layer, which is where Gemini comes in. The classic way to build a voice assistant is a pipeline: a wake word engine, a speech-to-text service, an LLM call, a text-to-speech service, and a lot of glue to make the whole thing feel responsive. You can build Dash that way. You will spend most of your time on the glue, and it will still feel like a walkie-talkie. The Gemini Live API collapses that pipeline into one stateful WebSocket. You stream raw microphone audio in, and the model streams spoken audio back, with transcripts of both sides, tool calls in the middle, and Google Search when it needs a current fact. This is what that buys you: | Concern | Pipeline STT + LLM + TTS | Gemini Live API | |---|---|---| | Latency | Three sequential network calls per turn | One bidirectional stream, audio starts before the sentence is finished | | Interruptions | Hand-rolled: detect speech, cancel TTS, re-prompt | Built in: barge-in, interrupted event, playback stops | | Language | Configure STT and TTS per language, detect yourself | Reply in whatever language the user spoke, 70+ languages | | Transcripts | Two separate services, two formats | inputTranscription and outputTranscription on the same socket | | Tools | Parse LLM output, dispatch, re-prompt, re-synthesize | toolCall and toolResponse messages inside the session | | Current facts | Your own search integration | googleSearch tool with grounding | | Voice | Pick a TTS vendor and voice | voiceName in the session config Dash ships with Kore | The trade-off is control: you do not get to inspect the text between STT and TTS, because there is no text in between. For a kitchen assistant that is the right deal. Here is the whole shape of it. Three parties: the browser running the display, the Dash server on the same machine, and the Gemini Live API. The two rules that drive the design: Dash is on the kitchen wall, so a third rule follows from the first two: Gemini is only on the line while you are talking to it. Waiting for the wake word is free and runs in the browser. node --test , native fetch , and the WebSocket global .env file next to config.json http://localhost:3000 git clone https://github.com/xavidop/dash && cd dash npm install cp config.example.json config.json echo "GEMINI API KEY=your-key" .env npm run build && npm start http://localhost:3000 Then switch on Settings → General → Voice assistant or set features.assistant to true in config.json . The rest of this article walks through what happens from the moment you say "Dash". A Live session is billed per minute of audio, so you do not want it open all day. Dash uses the browser's own speech recognition to wait for the wake word, and only opens a Gemini session once it hears it. This is the relevant part of web/src/assistant.ts : function startWake { if cfg || wakeBlocked || phase == 'off' || wakeOn return; const SR = window as any .SpeechRecognition || window as any .webkitSpeechRecognition; if SR return; startMic ; // keeps the pre-roll buffer filling rec = new SR as Recognition; rec.lang = getState .config?.locale || defaultLocale ; rec.continuous = true; rec.interimResults = true; const re = wakeRegex ; rec.onresult = e: any = { for let i = e.resultIndex; i < e.results.length; i++ { // Cooking: short commands "next", "siguiente paso" act at once, without Gemini. if e.results i .isFinal && cookingCommand norm e.results i 0 .transcript return; if re.test ${norm e.results i 0 .transcript } { restartDelay = 300; open true ; return; } } }; // ... } Two details matter here. The wake regex is forgiving. A recognizer set to Spanish or German hears "Dash" as "das", "dach" or "dasch". Exact wake words match anywhere; sound-alikes only count after "hey", "oye", "ok" and friends, because "das" is just a word in German: js function wakeRegex { const words = cfg ?.wakeWords || 'dash' .map norm .map w = w.trim .filter Boolean ; const alike = words.flatMap w = w === 'dash' ? 'dasch', 'dach', 'dache', 'dass', 'dahs', ... getState .config?.language === 'de' ? : 'das' : ; const exact = words.map esc .join '|' ; const loose = alike.map esc .join '|' ; return new RegExp ?:^|\\s ?:${exact} ?:\\s|$ ${loose ? | ?:^|\\s ?:hey|hei|ei|ey|oye|ok|okay|hola|hallo|ehi|eh \\s+ ?:${loose} ?:\\s|$ : ''} ; } The microphone keeps a 2.5-second ring buffer in memory. People say "Hey Dash, what's the weather?" in one breath. By the time the recognizer fires, the question is already half spoken. An AudioWorklet converts the mic to 16-bit PCM at 16 kHz in 100 ms chunks, and the last 25 chunks are kept so the whole sentence reaches Gemini when the session opens: js const IN RATE = 16000; const CHUNK = 1600; // 100 ms const PREROLL CHUNKS = 25; // 2.5 s of audio before the wake word fired node.port.onmessage = e = { ring.push e.data ; if ring.length PREROLL CHUNKS ring.shift ; onChunk?. e.data ; }; Nothing in that buffer leaves the device before the wake word. What the browser's own speech service hears while waiting is a different matter, and Dash is explicit about it in docs/voice-assistant.md https://github.com/xavidop/dash/blob/main/docs/voice-assistant.md : in Edge and Chrome, recognition runs on the browser vendor's online service, not on Gemini, and Dash stores none of it. Cooking mode adds a shortcut: while a recipe is open, "next", "back", "repeat", "start the timer" and "stop cooking" in each of the six languages are matched locally and act instantly, without opening a Gemini session at all. This is the part of the Live API that most examples skip, and the one that makes a client-side connection safe. Instead of handing the browser your API key, your backend calls the token endpoint and gets back a short-lived credential that is locked to a full session setup . The browser can connect with it, but it cannot change the model, the voice, the instructions or the tools. server/services/assistant.js : js const API = 'https://generativelanguage.googleapis.com/v1beta'; export const LIVE WS = 'wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContentConstrained'; / Mints a single-use token valid for 30 min a new session must start within 1 min . / async session { if this.enabled throw Object.assign new Error 'assistant disabled' , { status: 404 } ; if Date.now - this.lastSession < 2000 throw Object.assign new Error 'too many sessions' , { status: 429 } ; this.lastSession = Date.now ; const iso = min = new Date Date.now + min 60e3 .toISOString ; const res = await fetch ${API}/auth tokens , { method: 'POST', headers: { 'x-goog-api-key': this.key, 'content-type': 'application/json' }, body: JSON.stringify { uses: 1, expireTime: iso 30 , newSessionExpireTime: iso 1 , bidiGenerateContentSetup: this.setup } , signal: AbortSignal.timeout 10000 , } ; const d = await res.json .catch = {} ; if res.ok || d.name throw Object.assign new Error d.error?.message || token request failed HTTP ${res.status} , { status: 502 } ; return { url: ${LIVE WS}?access token=${encodeURIComponent d.name } , idleSeconds: this.config.assistant.idleSeconds }; } The fields, one by one: uses: 1 : the token starts exactly one session. expireTime : how long messages can flow over a connection opened with it 30 minutes here; the API's default is also 30 . newSessionExpireTime : how long you have to open that session 1 minute; again the default . The browser requests a token and connects immediately, so a minute is plenty. bidiGenerateContentSetup : the whole setup message the client would normally send on the WebSocket, baked into the token. In the SDKs this is called liveConnectConstraints ; on the REST endpoint it is The response is tiny, and it is all the browser ever gets from the server: { "name": "auth tokens/