You have tried the 10-minute tutorials and ended up with a robot that takes 3 seconds to respond, cannot be interrupted, and fails silently when the microphone gain drifts. Target end-to-end latency for a perceived instant response is 300-800ms, but most DIY builds hit 2-5 seconds. This guide shows you how to build a production-ready voice AI agent open source stack with streaming speech-to-text, interruptible responses, and graceful degradation to local models when APIs fail.

Table of Contents

  1. What is the best open source stack for a voice AI agent?
  2. How do you architect a low-latency voice AI pipeline?
  3. Which open source tools form the best stack in 2024?
  4. How do you handle interruptions and barge-in?
  5. Prerequisites
  6. Step-by-step implementation
  7. Trade-offs and when to choose another approach
  8. Common errors and fixes

What is the best open source stack for a voice AI agent?

A voice AI agent open source stack combines wake word detection (Porcupine or openWakeWord), streaming speech recognition (faster-whisper), local LLM inference (Ollama or vLLM), and text-to-speech (Piper or Coqui). Frameworks like Pipecat and LiveKit Agents orchestrate these components with real-time streaming, interruption handling, and latency targeting. As of 2024, these modular stacks outperform monolithic solutions because you can optimize or replace individual stages [github.com/pipecat-ai].

Core components: VAD, STT, LLM, TTS, orchestration

A voice agent pipeline has five stages. Voice Activity Detection (VAD) determines when audio contains speech versus silence. Speech-to-Text (STT) converts audio to text. The Large Language Model (LLM) generates a response. Text-to-Speech (TTS) synthesizes audio. The orchestration layer manages state, handles interrupts, and coordinates timing.

When open source beats SaaS: compliance, latency, cost modeling

Open source wins when you need data sovereignty, predictable costs, or latency control. Voice SaaS APIs charge $0.01-0.06 per minute. Self-hosting becomes cost-effective above 500 minutes per day. Local inference also eliminates round-trip latency to cloud APIs, which is critical for real-time conversation.

How do you architect a low-latency voice AI pipeline?

Modern voice agents use streaming architectures with WebRTC or WebSocket connections, processing audio chunks in parallel rather than waiting for turn completion. A typical latency budget breaks down as 50ms for VAD, 100-300ms for STT, 200-500ms for LLM time-to-first-token, and 50-150ms for TTS. You must measure each stage to identify your bottleneck.

Streaming vs. turn-based architectures

Turn-based architectures wait for the user to stop speaking before processing. This adds latency because you cannot start STT until the utterance ends. Streaming architectures process audio in real-time chunks. The LLM can begin generating responses before the user finishes speaking, dramatically reducing perceived latency.

The 5-stage pipeline: capture to TTS

flowchart LR
    A[Capture] --> B[VAD]
    B --> C[STT]
    C --> D[LLM]
    D --> E[TTS]
    E -->|Audio Output| A

Audio capture pulls frames from the microphone at 16kHz or 48kHz. VAD filters out silence. STT streams partial transcripts. The LLM yields tokens as they generate. TTS streams audio back. Each stage runs in a separate thread or process.

Managing state and context across interruptions

When a user interrupts, you must truncate the LLM context. If the LLM was generating “The weather in San Francisco is” and the user says “stop”, the next turn should not include that partial output. Use a sliding window context manager that discards unfinished assistant responses.

Which open source tools form the best stack in 2024?

The dominant production stack combines Porcupine or openWakeWord for wake word, Whisper (faster-whisper) for STT, local LLMs via llama.cpp or Ollama for privacy, and Piper or Coqui TTS for synthesis [github.com/SYSTRAN]. Pipecat or LiveKit Agents provide the orchestration layer. Each tool has trade-offs between speed, accuracy, and resource usage.

Wake word: Porcupine vs. openWakeWord vs. custom

Porcupine (Picovoice) offers patented, reliable wake word detection with paid licensing for commercial use. openWakeWord is free and supports training custom wake words from 10-20 samples. Choose Porcupine for production reliability. Choose openWakeWord when you need customization or want to avoid licensing fees [github.com/dscripka].

STT: Whisper variants benchmarked

faster-whisper uses CTranslate2 to achieve a 4x speedup over original Whisper with identical accuracy. whisper.cpp runs efficiently on CPU. Distil-Whisper is 49% smaller and faster but slightly less accurate on rare words. For real-time agents, use faster-whisper on GPU or whisper.cpp on CPU. Use full Whisper for archival transcription [github.com/SYSTRAN].

VariantLatency (RTF)AccuracyHardware
Whisper large-v30.5-1.0HighestGPU
faster-whisper large-v30.1-0.2HighestGPU
Distil-Whisper0.05-0.1HighGPU/CPU
whisper.cpp tiny.en0.3-0.5MediumCPU

RTF = Real-Time Factor. Lower is better. RTF 0.1 means 1 second of audio processes in 0.1 seconds.

LLM inference: Ollama, vLLM, and llama.cpp trade-offs

Ollama simplifies local LLM deployment with a single command. vLLM optimizes throughput for batched requests. llama.cpp offers the most control over quantization and memory. For voice agents, time-to-first-token matters more than throughput. Use Ollama for ease of use. Use llama.cpp when you need precise memory control [github.com/ollama].

TTS: Piper for speed, Coqui for quality, XTTS for voice cloning

Piper TTS synthesizes speech in 1ms on CPU but sounds robotic. Coqui TTS produces natural speech in 200-500ms but requires GPU. XTTS v2 supports voice cloning from a reference sample. Use Piper for low-latency utility responses. Use Coqui or XTTS for personas requiring natural intonation [github.com/rhasspy].

How do you handle interruptions and barge-in?

True conversational agents must detect when the user speaks during TTS playback, immediately halt audio output, and process the new utterance. This requires duplex audio streams, VAD on the input channel during output, and state management to reset the LLM context appropriately.

Implementing duplex audio with pyaudio or sounddevice

You must play audio while simultaneously recording. Use pyaudio or sounddevice with callback modes. Run input and output streams in separate threads. Queue audio chunks for output while continuously checking VAD on input.

import sounddevice as sd
import numpy as np
from queue import Queue

# Configuration
SAMPLE_RATE = 16000
CHANNELS = 1
DTYPE = np.int16

# Audio queues
input_queue = Queue()
output_queue = Queue()

def audio_callback(indata, outdata, frames, time, status):
    # Put input audio in queue for VAD processing
    input_queue.put(indata.copy())
    
    # Get output audio if available
    if not output_queue.empty():
        outdata[:] = output_queue.get()
    else:
        outdata.fill(0)

# Start duplex stream
stream = sd.RawStream(
    samplerate=SAMPLE_RATE,
    channels=CHANNELS,
    dtype=DTYPE,
    callback=audio_callback,
    blocksize=480  # 30ms at 16kHz
)

VAD-based barge-in detection patterns

Run VAD continuously on the input stream, even during TTS playback. When VAD detects speech above a threshold, stop TTS output immediately. Use Silero VAD for accuracy or WebRTC VAD for speed. Tune the threshold to your acoustic environment to avoid false triggers from ambient noise.

Context truncation strategies when interrupted mid-generation

When interrupted, the LLM may have generated partial output. Do not include this in the conversation history. Truncate the assistant message at the interruption point. Mark the turn as interrupted so the LLM understands context continuity.

Prerequisites

Install the required packages with pinned versions.

pip install faster-whisper==1.0.3
pip install pyaudio==0.2.14
pip install sounddevice==0.5.0
pip install numpy==1.26.4
pip install openai==1.51.0
pip install pvporcupine==3.0.2
pip install openwakeword==0.6.0
pip install piper-tts==1.2.0
pip install silero-vad==4.0

Hardware requirements:

  • Microphone configured at 16kHz or 48kHz
  • GPU recommended for faster-whisper (CUDA 11.8+)
  • 8GB+ RAM for local LLM (16GB+ for larger models)
  • Ollama or llama.cpp server running locally

Last verified: 2026-10-12

[VERIFY: Is Pipecat still actively maintained and at what version as of 2026-10-12?]

Step-by-step implementation

Build a minimal voice agent that listens for a wake word, transcribes speech, generates a response, and speaks it back.

1. Setting up the audio capture

Configure audio capture at 16kHz, which most speech models expect. Resample if your microphone only supports 48kHz.

import sounddevice as sd
import numpy as np
from collections import deque

class AudioCapture:
    def __init__(self, sample_rate=16000, channels=1):
        self.sample_rate = sample_rate
        self.channels = channels
        self.buffer = deque(maxlen=sample_rate * 10)  # 10 seconds buffer
        self.stream = None
    
    def start(self):
        self.stream = sd.InputStream(
            samplerate=self.sample_rate,
            channels=self.channels,
            dtype=np.float32,
            callback=self._callback
        )
        self.stream.start()
    
    def _callback(self, indata, frames, time, status):
        self.buffer.extend(indata[:, 0])
    
    def get_audio(self, duration_seconds):
        samples = int(self.sample_rate * duration_seconds)
        if len(self.buffer) < samples:
            return None
        return np.array(list(self.buffer)[-samples:])

2. Adding wake word detection

Use openWakeWord for a free, customizable wake word detector. Load the doorbell model or train your own.

import openwakeword
import numpy as np

class WakeWordDetector:
    def __init__(self, model_name="hey_jarvis"):
        self.model = openwakeword.Model()
        self.threshold = 0.5
    
    def detect(self, audio_chunk: np.ndarray) -> bool:
        # audio_chunk should be 16kHz float32 numpy array
        prediction = self.model.predict(audio_chunk)
        # Check if any model scored above threshold
        for model_name, score in prediction.items():
            if score > self.threshold:
                return True
        return False

# Usage
detector = WakeWordDetector()
if detector.detect(audio_chunk):
    print("Wake word detected!")

3. Streaming speech-to-text with faster-whisper

faster-whisper streams partial transcripts. Configure it to use GPU if available.

from faster_whisper import WhisperModel

class StreamingSTT:
    def __init__(self, model_size="base", device="cuda"):
        self.model = WhisperModel(model_size, device=device, compute_type="float16")
    
    def transcribe(self, audio: np.ndarray):
        # audio must be float32, 16kHz
        segments, info = self.model.transcribe(audio, beam_size=5)
        full_text = ""
        for segment in segments:
            full_text += segment.text
            yield segment.text  # Streaming partial results
        return full_text

# Usage
stt = StreamingSTT(model_size="base.en")
for partial in stt.transcribe(audio_chunk):
    print(f"Partial: {partial}")

4. LLM integration with Ollama

Connect to a local Ollama server. Use streaming to get time-to-first-token under 200ms.

import openai
import os

class LLMClient:
    def __init__(self, base_url="http://localhost:11434/v1", model="llama3.2"):
        self.client = openai.OpenAI(
            base_url=base_url,
            api_key=os.getenv("OLLAMA_API_KEY", "ollama")
        )
        self.model = model
        self.messages = []
    
    def add_message(self, role, content):
        self.messages.append({"role": role, "content": content})
    
    def generate(self, user_input: str):
        self.add_message("user", user_input)
        response = self.client.chat.completions.create(
            model=self.model,
            messages=self.messages,
            stream=True
        )
        full_response = ""
        for chunk in response:
            if chunk.choices[0].delta.content:
                text = chunk.choices[0].delta.content
                full_response += text
                yield text
        self.add_message("assistant", full_response)

5. Text-to-speech with Piper

Piper TTS runs fast on CPU. It outputs WAV audio you can play directly.

import subprocess
import tempfile
import sounddevice as sd
import soundfile as sf

class PiperTTS:
    def __init__(self, model_path="en_US-lessac-medium.onnx"):
        self.model_path = model_path
    
    def speak(self, text: str):
        with tempfile.NamedTemporaryFile(suffix=".wav", delete=True) as f:
            subprocess.run(
                ["piper", "--model", self.model_path, "--output_file", f.name],
                input=text.encode(),
                check=True
            )
            data, samplerate = sf.read(f.name)
            sd.play(data, samplerate)
            sd.wait()

# Usage
tts = PiperTTS()
tts.speak("Hello, I am your voice assistant.")

6. Building the main orchestration loop

Combine all components into a single loop that manages state and handles interruptions.

import time
import threading

class VoiceAgent:
    def __init__(self):
        self.capture = AudioCapture()
        self.wake_word = WakeWordDetector()
        self.stt = StreamingSTT()
        self.llm = LLMClient()
        self.tts = PiperTTS()
        self.running = False
        self.speaking = False
    
    def start(self):
        self.capture.start()
        self.running = True
        self._run_loop()
    
    def _run_loop(self):
        print("Listening for wake word...")
        while self.running:
            # Get recent audio
            audio = self.capture.get_audio(duration_seconds=1.5)
            if audio is None:
                time.sleep(0.1)
                continue
            
            # Check for wake word
            if not self.speaking and self.wake_word.detect(audio):
                print("Wake word detected! Listening...")
                self._handle_turn()
    
    def _handle_turn(self):
        # Record user speech
        audio = self.capture.get_audio(duration_seconds=5)
        
        # Transcribe
        user_text = ""
        for partial in self.stt.transcribe(audio):
            user_text = partial
        print(f"User: {user_text}")
        
        # Generate response
        print("Generating response...")
        response_text = ""
        for chunk in self.llm.generate(user_text):
            response_text += chunk
        print(f"Assistant: {response_text}")
        
        # Speak response
        self.speaking = True
        self.tts.speak(response_text)
        self.speaking = False

# Run the agent
if __name__ == "__main__":
    agent = VoiceAgent()
    agent.start()

Trade-offs and when to choose another approach

This open source stack optimizes for flexibility and cost at scale. It is not the right choice for every situation.

FactorOpen Source StackSaaS APIs
Setup complexityHigh (days)Low (hours)
Latency controlFull controlLimited
Data privacyOn-premiseCloud processing
Cost at low volumeHigher (GPU time)Lower
Cost at high volumeLower (fixed hardware)Higher (per-minute)
Voice qualityModerate to highHighest

Choose open source when:

  • You process over 500 minutes per day
  • Data residency requires on-premise processing
  • You need low-latency customization

Choose SaaS when:

  • You need instant setup
  • Quality matters more than cost
  • You have sporadic usage patterns

Common errors and fixes

Real error messages and proven solutions.

1. Audio buffer underrun causes robotic speech

Error: ALSA lib pcm.c:8545:(snd_pcm_recover) underrun occurred

Cause: The audio callback is not providing data fast enough. The output queue empties and you get pops or clicks.

Fix: Reduce block size and pre-buffer audio. Use a ring buffer to maintain at least 100ms of audio ahead.

# Pre-fill queue before playing
for _ in range(3):
    output_queue.put(silence_chunk)

2. Sample rate mismatch causes pitch distortion

Error: Audio sounds too fast or slow, or has clicking artifacts.

Cause: Your microphone captures at 48kHz but Whisper expects 16kHz. Resampling is incorrect or missing.

Fix: Explicitly resample using a high-quality resampler.

import librosa

audio_16k = librosa.resample(audio_48k, orig_sr=48000, target_sr=16000)

3. VAD false positives from keyboard typing

Error: Agent activates randomly without speech.

Cause: Silero VAD is sensitive to percussive sounds like keyboard clicks.

Fix: Increase the VAD threshold or add a frequency filter. Use a bandpass filter to reject low-frequency thumps.

from scipy.signal import butter, lfilter

def bandpass_filter(audio, lowcut=300, highcut=3400, fs=16000, order=5):
    nyq = 0.5 * fs
    low = lowcut / nyq
    high = highcut / nyq
    b, a = butter(order, [low, high], btype='band')
    return lfilter(b, a, audio)

filtered_audio = bandpass_filter(raw_audio)

4. LLM streaming stalls without timeout

Error: Agent hangs silently after user speaks.

Cause: The LLM API call hangs. No timeout handler exists to detect and recover.

Fix: Wrap the generation in a timeout using asyncio.wait_for or a thread timeout.

import asyncio

async def generate_with_timeout(llm_client, prompt, timeout=10.0):
    try:
        return await asyncio.wait_for(
NR

Nilesh Raut

Senior Backend Engineer

Nilesh is a backend and distributed systems engineer specializing in Node.js, Express, PostgreSQL, system design, and AI agent architectures. He writes production-tested engineering tutorials and interview guides on NileshBlog.Tech.

Verified Technical Author • Code Tested on Production Stacks
Written by

’m Nilesh, a Software Development Engineer with 2+ years of experience, specializing in Go, JavaScript, Python, Docker, Kubernetes, Git, Jenkins, microservices, and system design (LLD/HLD), backed by a strong foundation in data structures and algorithms. Alongside my engineering journey, I bring 4+ years of hands-on experience in SEO, where I’ve worked extensively on content strategy, keyword research, technical SEO, and organic growth, helping products and businesses scale efficiently by aligning solid technology with search-driven performance.