When I tried the Gemini Live SDK, I was impressed by how natural the voice interactions felt. It reminded me of the movie "Her" - and I wanted to see if I could bring that experience to life: real-time voice conversations with a minimal UI.
Now sharing the results in case you need Voice UI for your Moltbot 🦞.
OpenClaw + Gemini Live SDK + UX polish = the HER experience.
- Real-time voice conversations - Full-duplex audio streaming with Gemini's native audio model
- Push-to-talk - Hold SPACE to talk, release to listen
- Beautiful waveform visualization - Reactive audio visualization in the Her OS1 style
- Thinking animation - Elegant infinity loop animation when the AI is processing
- Interrupt support - Press SPACE while the AI is speaking to interrupt
- Debug drawer - Server logs and connection status visible in a slide-out panel
- Share URLs/text - Paste content to discuss with the AI
- Python 3.10+
- Node.js (for serving static files)
- A Google AI Studio API key with Gemini Live access
git clone https://github.com/LizMyers/her-voice-ui.git
cd her-voice-uicd server
cp .env.example .env
# Edit .env and add your GEMINI_API_KEYIn your .env file, you can also choose a voice:
GEMINI_API_KEY=your-api-key-here
GEMINI_VOICE=Kore # Options: Aoede, Charon, Fenrir, Kore, PuckEach voice has a distinct personality - try them out to find your favorite!
cd server
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txtcd server
source venv/bin/activate
source .env # Or set GEMINI_API_KEY environment variable
python main.pyIn a new terminal:
cd her-voice-ui
npx serve . -l 3000Navigate to http://localhost:3000 and click anywhere to start.
Controls:
- Hold SPACE - Talk to the AI
- Release SPACE - Listen to response
- Press SPACE while AI speaks - Interrupt
- ESC - Toggle debug drawer
Browser Server Gemini
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Microphone │──audio──▶ │ WebSocket │──audio──▶ │ Gemini Live │
│ │ │ Server │ │ (STT+LLM) │
│ Speaker + │◀──audio── │ (FastAPI) │◀──audio── │ +TTS │
│ Visualizer │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
Frontend: Vanilla JavaScript with WebSocket audio streaming Backend: FastAPI WebSocket server proxying to Gemini Live AI: Gemini 2.5 Flash with native audio dialog capabilities
This implementation includes several optimizations for smooth voice conversations:
Audio chunks are accumulated into ~1-second buffers before playback. Chunks are crossfaded with 50ms overlap to eliminate clicking/popping between segments.
// Crossfade scheduling
gainNode.gain.setValueAtTime(0, startTime);
gainNode.gain.linearRampToValueAtTime(1, startTime + fadeTime);A low-pass filter at 8kHz reduces high-frequency artifacts from resampling (Gemini outputs 24kHz PCM).
this.lowPassFilter = ctx.createBiquadFilter();
this.lowPassFilter.type = 'lowpass';
this.lowPassFilter.frequency.value = 8000;Instead of a fixed timeout, the frontend tracks the actual scheduled audio end time to know when the AI has finished speaking:
const scheduledEnd = this.visualizer?.nextChunkTime || 0;
const now = ctx?.currentTime || 0;
if (scheduledEnd > now + 0.5) {
// Still playing...
}A preloaded CodePen embed provides the "thinking" animation without layout shift. Uses CSS opacity + pointer-events instead of display to avoid flash:
#thinking-container {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}When SPACE is pressed while audio is playing, the queue is cleared and the connection sends a silence buffer to signal turn completion:
interruptPlayback() {
this.audioQueue = [];
this.visualizer.analyser.disconnect();
}The backend accumulates ~48KB of audio (~1 second) before sending to reduce WebSocket message overhead and chunk boundaries:
BUFFER_TARGET_SIZE = 48000 # ~1 second at 24kHz 16-bit mono
if len(audio_buffer) >= BUFFER_TARGET_SIZE:
wav_data = pcm_to_wav(bytes(audio_buffer))
await websocket.send_text(json.dumps({...}))Edit server/gemini_client.py to customize the AI's personality:
DEFAULT_SYSTEM_PROMPT = """You are a helpful, friendly AI assistant..."""Available voices: Aoede, Charon, Fenrir, Kore, Puck
Set in your .env file:
GEMINI_VOICE=KoreNo code changes needed - just update .env and restart the server.
Edit src/styles.css to change the background color:
body {
background: #d1684e; /* Her OS1 coral/salmon */
}her-voice-ui/
├── public/
│ └── index.html # Main HTML page
├── server/
│ ├── main.py # FastAPI WebSocket server
│ ├── gemini_client.py # Gemini Live API integration
│ ├── requirements.txt # Python dependencies
│ └── .env # API key (create from .env.example)
└── src/
├── app.js # Main frontend application
├── audio-visualizer.js # Waveform visualization
├── thinking-animation.js # Thinking state animation
├── websocket-client.js # WebSocket connection handling
└── styles.css # Her-inspired styling
Make sure you've created server/.env with your API key:
GEMINI_API_KEY=your_key_here
Try adjusting the low-pass filter frequency in audio-visualizer.js:
this.lowPassFilter.frequency.value = 6000; // Lower = smootherEnsure the server is running on port 8765 and no firewall is blocking it.
Grant microphone permissions when prompted. The browser requires HTTPS or localhost.
- Created by: Liz Myers & Claude Opus 4.5 (Anthropic)
- OpenClaw: Peter Steinberger, Molty 🦞, and the OpenClaw community
- Thinking Animation: Based on the beautiful Her OS1 CodePen by Siyoung Park (@psyonline)
- Design Inspiration: The movie "Her" (2013) by Spike Jonze
- Voice AI: Google Gemini Live API
MIT License - see LICENSE for details.
Built with love by Liz Myers & Claude Opus 4.5, inspired by Her.
