Showing posts with label Voice AI. Show all posts
Showing posts with label Voice AI. Show all posts

Monday, April 6, 2026

Build Your First Voice Agent: Python Tutorial with Pipecat

Level: Intermediate
Topic: Voice AI, TTS, STT

Hero Image: Python code flowing into a voice assistant speaking to a user

In the previous posts, we explored TTS engines and STT models individually. Now it's time to wire them together into something that actually talks back. In this tutorial, you'll build a voice agent from scratch using Python and Pipecat -- an open-source framework for building real-time voice and multimodal AI pipelines.

By the end, you'll have a working voice agent that listens to your microphone, processes your speech through an LLM, and speaks the response back to you -- all in real time. We'll start with the simplest possible agent (under 80 lines), then progressively add function calling, conversation memory, error handling, and phone connectivity.

Pipecat has grown into a mature framework with 60+ provider integrations, client SDKs for JavaScript, React, iOS, Android, and C++, and a managed cloud hosting option. It's the most popular open-source choice for voice agent development in 2026.


What Is Pipecat?

Pipecat is an open-source Python framework created by Daily.co for building real-time voice and multimodal AI applications. It provides a pipeline-based architecture where you chain together processors -- STT, LLM, TTS, transport -- and data flows through them automatically.

Why Pipecat Over Building From Scratch?

  • Pipeline abstraction: Chain STT, LLM, and TTS together declaratively -- no manual threading or async coordination
  • Turn-taking: Built-in support for interruptions, barge-in, and conversational flow
  • Transport layer: Handles WebRTC, WebSocket, and local audio I/O
  • Provider-agnostic: Swap STT/LLM/TTS providers without rewriting your pipeline (60+ integrations including Deepgram, OpenAI, Anthropic, ElevenLabs, Cartesia, Kokoro, and more)
  • Real-time optimized: Frame-based processing designed for sub-second latency
  • Client SDKs: JavaScript, React, React Native, iOS, Android, C++ for building front-ends
graph LR subgraph Pipecat Pipeline A[Transport Input
Microphone/WebRTC] --> B[STT
Deepgram Nova-3] B --> C[Context Aggregator
User Message] C --> D[LLM
GPT-4o / Claude] D --> E[TTS
OpenAI / ElevenLabs] E --> F[Transport Output
Speaker/WebRTC] F --> G[Context Aggregator
Assistant Message] end style A fill:#4CAF50,color:#fff style D fill:#FF9800,color:#fff style F fill:#2196F3,color:#fff

Prerequisites

Before we start, make sure you have:

  • Python 3.10 or higher
  • A microphone and speakers (or headphones -- recommended to avoid echo)
  • API keys for:
  • OpenAI (for the LLM and TTS) -- platform.openai.com
  • Deepgram (for STT) -- free tier gives you $200 in credits at console.deepgram.com
  • Basic Python async/await knowledge

Step 1: Set Up the Project

Create a new project directory and install dependencies:

mkdir voice-agent && cd voice-agent
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install Pipecat with the providers we need
pip install "pipecat-ai[daily,openai,deepgram,silero]"

# Install PyAudio for local microphone access
pip install pyaudio

# If PyAudio fails on macOS:
# brew install portaudio
# pip install pyaudio

Create a .env file for your API keys:

OPENAI_API_KEY=your-openai-key
DEEPGRAM_API_KEY=your-deepgram-key

Project structure:

voice-agent/
  .env
  agent.py           # Basic agent (Step 3)
  agent_tools.py     # Agent with function calling (Step 5)
  agent_full.py      # Production-ready agent (Step 8)
  venv/

Step 2: Understand the Pipeline Architecture

A voice agent pipeline has four stages that process data in sequence:

Microphone Audio
      |
      v
[1. STT] -- Deepgram Nova-3 converts speech to text
      |
      v
[2. LLM] -- GPT-4o processes text and generates response
      |
      v
[3. TTS] -- OpenAI TTS converts response to speech audio
      |
      v
Speaker Output

In Pipecat, each stage is a processor that receives frames (units of data) and outputs new frames. Audio frames flow in, text frames flow between processors, and audio frames flow out.

The key insight: everything streams. The STT starts outputting text before you finish speaking. The LLM starts generating tokens before the full input arrives. The TTS starts producing audio from the first sentence while the LLM is still generating the rest. This streaming overlap is what makes sub-second response times possible.

Architecture Diagram: Data flow through Pipecat pipeline stages

Latency Budget

Here's where time is spent in a well-optimized pipeline:

Stage Latency Optimization
VAD (end-of-speech detection) 50-100ms Silero VAD with tuned thresholds
STT finalization 100-300ms Deepgram streaming with endpointing
LLM time-to-first-token 150-400ms GPT-4o-mini for speed, GPT-4o for quality
TTS time-to-first-byte 100-300ms OpenAI tts-1 or ElevenLabs Flash
Network + buffering 50-150ms Connection pooling, edge deployment
Total 450-1250ms Target: <800ms P95

Step 3: Build a Minimal Voice Agent

Here's the simplest possible voice agent with Pipecat. Create a file called agent.py:

import asyncio
import os
from dotenv import load_dotenv

from pipecat.frames.frames import EndFrame, LLMMessagesFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.deepgram import DeepgramSTTService
from pipecat.services.openai import OpenAILLMService, OpenAITTSService
from pipecat.transports.local.audio import LocalAudioTransport
from pipecat.vad.silero import SileroVADAnalyzer

load_dotenv()

async def main():
    # --- Transport: handles microphone input and speaker output ---
    transport = LocalAudioTransport(
        mic_enabled=True,
        speaker_enabled=True,
        vad_analyzer=SileroVADAnalyzer()  # Detects when you're speaking
    )

    # --- STT: Deepgram Nova-3 for real-time transcription ---
    stt = DeepgramSTTService(
        api_key=os.getenv("DEEPGRAM_API_KEY"),
        model="nova-3",
        language="en"
    )

    # --- LLM: OpenAI GPT-4o for conversation ---
    llm = OpenAILLMService(
        api_key=os.getenv("OPENAI_API_KEY"),
        model="gpt-4o"
    )

    # --- TTS: OpenAI TTS for speech output ---
    tts = OpenAITTSService(
        api_key=os.getenv("OPENAI_API_KEY"),
        voice="nova",
        model="tts-1"
    )

    # --- Conversation context ---
    messages = [
        {
            "role": "system",
            "content": (
                "You are a helpful voice assistant. Keep your responses "
                "concise -- aim for 1-2 sentences. You're having a real-time "
                "voice conversation, so be natural and conversational. "
                "Don't use markdown, lists, or formatting in your responses."
            ),
        }
    ]

    context = OpenAILLMContext(messages)
    context_aggregator = llm.create_context_aggregator(context)

    # --- Build the pipeline ---
    pipeline = Pipeline([
        transport.input(),       # Microphone audio in
        stt,                     # Speech to text
        context_aggregator.user(),  # Add user message to context
        llm,                     # Generate response
        tts,                     # Text to speech
        transport.output(),      # Speaker audio out
        context_aggregator.assistant()  # Add assistant message to context
    ])

    task = PipelineTask(
        pipeline,
        PipelineParams(
            allow_interruptions=True,  # Let user interrupt the AI
            enable_metrics=True        # Track latency metrics
        )
    )

    # --- Run ---
    runner = PipelineRunner()

    # Send initial greeting
    await task.queue_frames([
        LLMMessagesFrame(messages),
    ])

    print("Voice agent is running! Speak into your microphone.")
    print("Press Ctrl+C to stop.")

    await runner.run(task)

if __name__ == "__main__":
    asyncio.run(main())

Run it:

python agent.py

Speak into your microphone, and the agent will respond through your speakers. That's a working voice agent in under 80 lines of code.


Step 4: Add Turn-Taking and Interruptions

The basic agent already supports interruptions thanks to allow_interruptions=True. But let's understand how turn-taking works and how to customize it for your use case.

How Pipecat Handles Turns

graph TD A[User starts speaking] --> B[VAD detects speech] B --> C[Audio streams to STT] C --> D[STT produces interim transcripts] D --> E{User stops speaking?} E -->|No - still talking| C E -->|Yes - silence detected| F[STT produces final transcript] F --> G[Transcript sent to LLM] G --> H[LLM generates response tokens] H --> I[Tokens stream to TTS] I --> J[TTS produces audio chunks] J --> K[Audio plays through speaker] K --> L{User interrupts?} L -->|Yes| M[Stop TTS playback immediately] M --> A L -->|No| N[Response completes] N --> O[Wait for next user utterance] O --> A style A fill:#4CAF50,color:#fff style M fill:#f44336,color:#fff style N fill:#2196F3,color:#fff

Customizing VAD Sensitivity

The Voice Activity Detection (VAD) parameters control when the agent thinks you've started and stopped speaking:

from pipecat.vad.silero import SileroVADAnalyzer, VADParams

# Configure VAD sensitivity
vad_analyzer = SileroVADAnalyzer(
    params=VADParams(
        threshold=0.5,              # Speech detection sensitivity (0-1)
                                     # Lower = more sensitive, higher = less false positives
        min_speech_duration_ms=250,  # Minimum speech to trigger (ignore brief sounds)
        max_speech_duration_s=30,    # Maximum single utterance before forced turn end
        min_silence_duration_ms=500, # Silence before end-of-turn
                                     # THIS IS THE MOST IMPORTANT PARAMETER
        speech_pad_ms=100            # Padding around detected speech
    )
)

transport = LocalAudioTransport(
    mic_enabled=True,
    speaker_enabled=True,
    vad_analyzer=vad_analyzer
)

Tuning min_silence_duration_ms

This parameter determines how long the agent waits after you stop talking before it responds:

Value Behavior Best For
200-300ms Very responsive, but interrupts natural pauses Quick Q&A, command-driven agents
400-600ms Good balance for most conversations General-purpose voice agents
700-1000ms Very patient, lets user collect thoughts Therapy bots, elderly users, complex topics
1000-2000ms Extremely patient Dictation, users with speech difficulties

Start with 500ms and adjust based on user feedback.


Step 5: Add Function Calling

A voice agent becomes truly useful when it can take actions. Let's add function calling so our agent can check the weather, set reminders, or look up information.

import json
from datetime import datetime, timedelta

# Define tools the agent can use
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "City name, e.g. 'San Francisco'"
                    }
                },
                "required": ["location"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "set_reminder",
            "description": "Set a reminder for the user",
            "parameters": {
                "type": "object",
                "properties": {
                    "message": {
                        "type": "string",
                        "description": "The reminder message"
                    },
                    "minutes": {
                        "type": "integer",
                        "description": "Minutes from now"
                    }
                },
                "required": ["message", "minutes"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_knowledge_base",
            "description": "Search internal documentation or knowledge base",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The search query"
                    }
                },
                "required": ["query"]
            }
        }
    }
]

# Handler for function calls
async def handle_function_call(function_name, tool_call_id, args, llm, context, result_callback):
    if function_name == "get_weather":
        location = args["location"]
        # In production, call a real weather API (OpenWeatherMap, etc.)
        result = json.dumps({
            "location": location,
            "temperature": 72,
            "condition": "sunny",
            "humidity": 45
        })
        await result_callback(result)

    elif function_name == "set_reminder":
        message = args["message"]
        minutes = args["minutes"]
        reminder_time = datetime.now() + timedelta(minutes=minutes)
        # In production, schedule via APScheduler, Celery, or system cron
        result = json.dumps({
            "status": "set",
            "message": message,
            "trigger_at": reminder_time.isoformat()
        })
        await result_callback(result)

    elif function_name == "search_knowledge_base":
        query = args["query"]
        # In production, call your RAG pipeline (Pinecone, Weaviate, etc.)
        result = json.dumps({
            "results": [
                {"title": "Getting Started Guide", "relevance": 0.95},
                {"title": "API Reference", "relevance": 0.87}
            ],
            "query": query
        })
        await result_callback(result)

# Register handlers with the LLM
llm = OpenAILLMService(
    api_key=os.getenv("OPENAI_API_KEY"),
    model="gpt-4o"
)
llm.register_function("get_weather", handle_function_call)
llm.register_function("set_reminder", handle_function_call)
llm.register_function("search_knowledge_base", handle_function_call)

# Update context to include tools
messages = [
    {
        "role": "system",
        "content": (
            "You are a helpful voice assistant with access to tools. "
            "You can check the weather, set reminders, and search a knowledge base. "
            "Keep responses concise and conversational. Never use markdown."
        ),
    }
]

context = OpenAILLMContext(messages, tools)

Now you can say "What's the weather in Tokyo?" and the agent will call the function and speak the result naturally.


Step 6: Add Conversation Memory

Pipecat's context aggregator automatically tracks conversation history. Every user message and assistant response is added to the context window. But for longer conversations, you need a strategy to manage context size.

from pipecat.processors.frame_processor import FrameProcessor
from pipecat.frames.frames import Frame, LLMMessagesFrame

class ConversationMemoryManager(FrameProcessor):
    """Manages conversation context to prevent token overflow.

    Strategy: Keep system message + summary of old messages + last N messages.
    This preserves important context while staying within token limits.
    """

    def __init__(self, max_messages: int = 20, summary_threshold: int = 15):
        super().__init__()
        self.max_messages = max_messages
        self.summary_threshold = summary_threshold
        self.conversation_summary = ""

    async def process_frame(self, frame: Frame, direction):
        if isinstance(frame, LLMMessagesFrame):
            messages = frame.messages

            if len(messages) > self.max_messages:
                system_msg = messages[0]  # Always keep system message

                # Summarize older messages (in production, use the LLM for this)
                old_messages = messages[1:-(self.summary_threshold)]
                topics = set()
                for msg in old_messages:
                    content = msg.get("content", "")
                    if len(content) > 20:
                        topics.add(content[:50])

                self.conversation_summary = (
                    f"Earlier in this conversation, the following topics "
                    f"were discussed: {', '.join(list(topics)[:5])}. "
                    f"Continue naturally from the recent context."
                )

                summary_msg = {
                    "role": "system",
                    "content": self.conversation_summary
                }

                recent = messages[-(self.summary_threshold):]
                frame.messages = [system_msg, summary_msg] + recent

        await self.push_frame(frame, direction)

Token Budget Planning

LLM Context Window Recommended Conversation Limit
GPT-4o 128K tokens ~50-100 exchanges before summarizing
GPT-4o-mini 128K tokens ~50-100 exchanges (cheaper per token)
Claude Sonnet 200K tokens ~100-200 exchanges before summarizing

For most voice agents, conversations last 5-15 exchanges. Context overflow is mainly a concern for long customer service calls or ongoing assistant sessions.


Step 7: Add Error Handling and Resilience

Real voice agents need to handle failures gracefully. Users can't see error logs -- they only hear silence or confusion. Every failure mode needs a spoken recovery.

from pipecat.processors.frame_processor import FrameProcessor
from pipecat.frames.frames import Frame, TextFrame, ErrorFrame
import logging

logger = logging.getLogger(__name__)

class VoiceErrorHandler(FrameProcessor):
    """Catches errors in the pipeline and converts them to spoken feedback.

    Without this, errors cause dead silence -- the worst possible UX.
    """

    def __init__(self):
        super().__init__()
        self.consecutive_errors = 0
        self.max_retries = 3

    async def process_frame(self, frame: Frame, direction):
        if isinstance(frame, ErrorFrame):
            self.consecutive_errors += 1
            logger.error(f"Pipeline error ({self.consecutive_errors}): {frame.error}")

            if self.consecutive_errors >= self.max_retries:
                # Too many errors -- graceful shutdown
                error_response = TextFrame(
                    "I'm experiencing technical difficulties and need to restart. "
                    "Please try again in a moment."
                )
                await self.push_frame(error_response, direction)
                # In production: alert on-call, restart pipeline
            else:
                # Recoverable error -- ask user to repeat
                error_response = TextFrame(
                    "I'm sorry, I ran into a brief issue. "
                    "Could you please repeat that?"
                )
                await self.push_frame(error_response, direction)
        else:
            # Reset error counter on successful frames
            self.consecutive_errors = 0
            await self.push_frame(frame, direction)


class LatencyMonitor(FrameProcessor):
    """Tracks and logs latency between pipeline stages.

    Critical for production monitoring -- alerts when TTFB exceeds targets.
    """

    def __init__(self, stage_name: str, warn_threshold_ms: float = 500):
        super().__init__()
        self.stage_name = stage_name
        self.warn_threshold_ms = warn_threshold_ms
        self.frame_count = 0
        self.total_latency = 0

    async def process_frame(self, frame: Frame, direction):
        import time
        start = time.monotonic()
        await self.push_frame(frame, direction)
        elapsed_ms = (time.monotonic() - start) * 1000

        self.frame_count += 1
        self.total_latency += elapsed_ms

        if elapsed_ms > self.warn_threshold_ms:
            logger.warning(
                f"[{self.stage_name}] High latency: {elapsed_ms:.0f}ms "
                f"(threshold: {self.warn_threshold_ms}ms)"
            )

    @property
    def avg_latency_ms(self) -> float:
        return self.total_latency / max(self.frame_count, 1)


# Add to pipeline
pipeline = Pipeline([
    transport.input(),
    stt,
    LatencyMonitor("stt", warn_threshold_ms=300),
    context_aggregator.user(),
    llm,
    LatencyMonitor("llm", warn_threshold_ms=500),
    VoiceErrorHandler(),         # Catch errors before TTS
    tts,
    LatencyMonitor("tts", warn_threshold_ms=300),
    transport.output(),
    context_aggregator.assistant()
])

Step 8: Connect to WebRTC (Phone & Web)

To make your voice agent accessible beyond your local machine -- via a web browser or phone -- replace the local audio transport with Daily's WebRTC transport.

from pipecat.transports.services.daily import DailyTransport, DailyParams

# Replace LocalAudioTransport with DailyTransport
transport = DailyTransport(
    room_url="https://your-domain.daily.co/your-room",
    token="your-daily-token",
    bot_name="AmtocBot",
    params=DailyParams(
        audio_in_enabled=True,
        audio_out_enabled=True,
        vad_enabled=True,
        vad_analyzer=SileroVADAnalyzer(
            params=VADParams(
                threshold=0.5,
                min_silence_duration_ms=500
            )
        )
    )
)

Web Browser Integration

Daily provides a JavaScript SDK for embedding voice agents in web pages:

// Frontend: Connect to the voice agent via WebRTC
import DailyIframe from '@daily-co/daily-js';

const callFrame = DailyIframe.createFrame();
await callFrame.join({
    url: 'https://your-domain.daily.co/your-room',
    token: 'your-participant-token'
});

// Audio is automatically routed to/from the voice agent

Phone Connectivity

Connect a Twilio phone number to a Daily room for telephone access:

# Twilio webhook handler (Flask example)
from flask import Flask, request
from twilio.twiml.voice_response import VoiceResponse, Connect

app = Flask(__name__)

@app.route("/incoming-call", methods=["POST"])
def handle_incoming_call():
    response = VoiceResponse()
    connect = Connect()
    # Route the phone call to the Daily room where your agent lives
    connect.stream(
        url="wss://your-domain.daily.co/your-room/stream",
        name="phone-caller"
    )
    response.append(connect)
    return str(response)

Step 9: Swap Providers Without Rewriting

One of Pipecat's biggest strengths is provider swappability. Here's how to switch between different STT, LLM, and TTS providers with minimal code changes:

# --- STT Options ---
# Deepgram (best for real-time streaming)
from pipecat.services.deepgram import DeepgramSTTService
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"), model="nova-3")

# Google (best for enterprise/multilingual)
from pipecat.services.google import GoogleSTTService
stt = GoogleSTTService(credentials=os.getenv("GOOGLE_CREDENTIALS"))

# AssemblyAI (best streaming accuracy)
from pipecat.services.assemblyai import AssemblyAISTTService
stt = AssemblyAISTTService(api_key=os.getenv("ASSEMBLYAI_API_KEY"))

# --- LLM Options ---
# OpenAI GPT-4o
from pipecat.services.openai import OpenAILLMService
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o")

# Anthropic Claude
from pipecat.services.anthropic import AnthropicLLMService
llm = AnthropicLLMService(api_key=os.getenv("ANTHROPIC_API_KEY"), model="claude-sonnet-4-20250514")

# Groq (ultra-fast inference)
from pipecat.services.groq import GroqLLMService
llm = GroqLLMService(api_key=os.getenv("GROQ_API_KEY"), model="llama-3.3-70b")

# --- TTS Options ---
# OpenAI TTS (simple, reliable)
from pipecat.services.openai import OpenAITTSService
tts = OpenAITTSService(api_key=os.getenv("OPENAI_API_KEY"), voice="nova")

# ElevenLabs (highest quality)
from pipecat.services.elevenlabs import ElevenLabsTTSService
tts = ElevenLabsTTSService(api_key=os.getenv("ELEVENLABS_API_KEY"), voice_id="...")

# Cartesia (lowest latency -- 40ms)
from pipecat.services.cartesia import CartesiaTTSService
tts = CartesiaTTSService(api_key=os.getenv("CARTESIA_API_KEY"), voice_id="...")

# Kokoro (self-hosted, free)
from pipecat.services.kokoro import KokoroTTSService
tts = KokoroTTSService(voice="af_heart")

The pipeline code stays exactly the same -- only the service initialization changes.


The Complete Production-Ready Agent

Here's the full agent combining everything we've built:

import asyncio
import os
import json
import logging
from datetime import datetime, timedelta
from dotenv import load_dotenv

from pipecat.frames.frames import LLMMessagesFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.deepgram import DeepgramSTTService
from pipecat.services.openai import OpenAILLMService, OpenAITTSService
from pipecat.transports.local.audio import LocalAudioTransport
from pipecat.vad.silero import SileroVADAnalyzer, VADParams

load_dotenv()
logging.basicConfig(level=logging.INFO)

# --- Tools ---
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string", "description": "City name"}
                },
                "required": ["location"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "set_reminder",
            "description": "Set a reminder for the user",
            "parameters": {
                "type": "object",
                "properties": {
                    "message": {"type": "string"},
                    "minutes": {"type": "integer"}
                },
                "required": ["message", "minutes"]
            }
        }
    }
]

async def handle_function_call(function_name, tool_call_id, args, llm, context, result_callback):
    if function_name == "get_weather":
        result = json.dumps({
            "temperature": 72, "condition": "sunny",
            "location": args["location"]
        })
        await result_callback(result)
    elif function_name == "set_reminder":
        trigger = datetime.now() + timedelta(minutes=args["minutes"])
        result = json.dumps({
            "status": "set", "message": args["message"],
            "trigger_at": trigger.strftime("%I:%M %p")
        })
        await result_callback(result)

async def main():
    # Transport with tuned VAD
    transport = LocalAudioTransport(
        mic_enabled=True,
        speaker_enabled=True,
        vad_analyzer=SileroVADAnalyzer(
            params=VADParams(
                threshold=0.5,
                min_silence_duration_ms=500,
                min_speech_duration_ms=250
            )
        )
    )

    # Services
    stt = DeepgramSTTService(
        api_key=os.getenv("DEEPGRAM_API_KEY"),
        model="nova-3",
        language="en"
    )

    llm = OpenAILLMService(
        api_key=os.getenv("OPENAI_API_KEY"),
        model="gpt-4o"
    )
    llm.register_function("get_weather", handle_function_call)
    llm.register_function("set_reminder", handle_function_call)

    tts = OpenAITTSService(
        api_key=os.getenv("OPENAI_API_KEY"),
        voice="nova",
        model="tts-1"
    )

    # Context
    messages = [
        {
            "role": "system",
            "content": (
                "You are a friendly voice assistant called Amtoc. "
                "Keep responses to 1-3 sentences. Be conversational "
                "and natural. You can check the weather and set reminders. "
                "Never use markdown, lists, or formatting. "
                "If you're not sure about something, say so honestly."
            ),
        }
    ]
    context = OpenAILLMContext(messages, tools)
    context_aggregator = llm.create_context_aggregator(context)

    # Pipeline
    pipeline = Pipeline([
        transport.input(),
        stt,
        context_aggregator.user(),
        llm,
        tts,
        transport.output(),
        context_aggregator.assistant()
    ])

    task = PipelineTask(
        pipeline,
        PipelineParams(
            allow_interruptions=True,
            enable_metrics=True
        )
    )

    runner = PipelineRunner()
    await task.queue_frames([LLMMessagesFrame(messages)])

    print("=" * 50)
    print("  Amtoc Voice Agent is Running!")
    print("  Speak into your microphone.")
    print("  Press Ctrl+C to stop.")
    print("=" * 50)

    await runner.run(task)

if __name__ == "__main__":
    asyncio.run(main())

Troubleshooting Common Issues

"No audio input detected"

  • Check microphone permissions in your OS settings
  • Verify PyAudio can see your microphone: python -c "import pyaudio; p = pyaudio.PyAudio(); print(p.get_device_count())"
  • On macOS: grant Terminal/IDE microphone permission in System Settings > Privacy
  • Try specifying a device index in the transport configuration

High latency (>1 second response time)

  • Switch from tts-1-hd to tts-1 (quality vs speed trade-off)
  • Use gpt-4o-mini instead of gpt-4o for the LLM (2-3x faster)
  • Reduce min_silence_duration_ms to detect end-of-turn faster (try 300ms)
  • Check network: API calls need low latency (<50ms round trip)
  • Use Cartesia TTS (40ms TTFA) instead of OpenAI (200-400ms)

Agent interrupts you mid-sentence

  • Increase min_silence_duration_ms (try 700-800ms)
  • Increase min_speech_duration_ms to avoid triggering on brief sounds (try 300ms)
  • Adjust VAD threshold higher (0.6-0.7) to require stronger speech signal

Echo or feedback loop

  • Use headphones -- this is the #1 fix
  • Enable acoustic echo cancellation in your OS audio settings
  • In production, use WebRTC transport (Daily) which has built-in echo cancellation

API rate limits

  • Implement exponential backoff for retries
  • Use connection pooling (Pipecat does this automatically)
  • For high-volume: negotiate enterprise API rates or self-host STT/TTS

Cost of Running This Agent

For a typical deployment handling 1,000 minutes of conversation per month:

Component Provider Cost/Month
STT Deepgram Nova-3 (streaming) $7.70
LLM GPT-4o $10-30 (varies by conversation length)
TTS OpenAI tts-1 ~$15
Total ~$33-53/month

For budget optimization, swap GPT-4o for GPT-4o-mini ($3-10/month) and you're under $30/month for 1,000 minutes.


Next Steps

You now have a working voice agent. From here, you can:

  1. Add more tools: Connect to calendars, databases, CRMs -- anything your agent needs
  2. Swap providers: Try ElevenLabs TTS for higher quality, or self-hosted Kokoro for zero API costs
  3. Deploy to the cloud: Use Daily rooms and Twilio for phone access, or Pipecat Cloud for managed hosting
  4. Add a personality: Tune the system prompt for your specific use case
  5. Build a web interface: Use the React or JavaScript SDK to embed the agent in a web page
  6. Add analytics: Track conversation metrics, user satisfaction, and task completion rates

In the next post, we'll go deeper into voice agent architectures -- comparing the pipeline approach we just built to end-to-end models like GPT-4o Voice and managed platforms like Retell and VAPI.

Sources & References:
1. Pipecat — "Voice Agent Framework" — https://github.com/pipecat-ai/pipecat
2. OpenAI — "Whisper API" — https://platform.openai.com/docs/guides/speech-to-text
3. Deepgram — "Streaming Speech-to-Text" — https://developers.deepgram.com/


This is part 4 of the AmtocSoft Voice AI series. Full source code is available in the examples above -- copy, paste, and start experimenting.


Tools mentioned in this post

Disclosure: the links below are affiliate links. If you sign up via them, we earn a small commission at no extra cost to you. This helps fund the writing of more posts like this one.

  • Pinecone — production vector database. Sign up
  • Anthropic Claude API — production LLM access. Sign up
  • OpenAI Platform — GPT-4 and embedding APIs. Sign up

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-04-06 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

Sunday, April 5, 2026

Speech-to-Text Showdown: Whisper, Deepgram, Google, and the New Challengers

Level: Intermediate
Topic: Voice AI, STT

Hero Image: Audio waveform being transcribed into text by AI

Speech-to-text is the front door of every voice AI system. If your transcription is wrong, everything downstream -- the LLM's understanding, the response, the user experience -- falls apart. Getting STT right is non-negotiable.

But the STT landscape in 2026 looks radically different from even a year ago. OpenAI has added GPT-4o-powered transcription alongside Whisper. NVIDIA's Canary model hit #1 on the Open ASR leaderboard with a hybrid ASR-LLM architecture. Deepgram Nova-3 claims 53% lower error rates than competitors. And the gap between academic benchmarks and real-world production performance remains a critical trap for teams that don't test on their own data.

In this post, we'll compare the major STT solutions head-to-head with real benchmark numbers, production latency profiles, and cost analysis at scale. We'll cover Whisper, Deepgram, Google, AssemblyAI, and the new challengers -- then give you a decision framework for your specific use case.


The Contenders

Solution Type Model Best For
Whisper Open-source / API Large v3 Turbo Accuracy, offline use, self-hosted
GPT-4o Transcribe Commercial API GPT-4o architecture Context-aware transcription
Deepgram Commercial API Nova-3 Real-time streaming, low latency
Google Speech Commercial API Chirp 3 Enterprise, massive language support
AssemblyAI Commercial API Universal-2 Best streaming accuracy, rich features
NVIDIA Canary Open-source Canary-Qwen-2.5B Highest benchmark accuracy
Architecture Diagram: STT pipeline from audio input to text output

How Modern STT Works

Before diving into comparisons, let's understand the two dominant STT architectures in 2026:

Encoder-Decoder (Whisper, NVIDIA Canary)

Audio is converted to a mel spectrogram, processed by a conformer/transformer encoder, then decoded into text tokens. The model processes audio in chunks (typically 30 seconds for Whisper).

End-to-End Streaming (Deepgram, Google, AssemblyAI)

Audio is processed as a continuous stream. The model outputs interim transcripts as audio arrives and refines them into final transcripts when the speaker pauses.

graph LR A[Raw Audio] --> B[Preprocessing] B --> C[Mel Spectrogram] C --> D{Architecture} D -->|Encoder-Decoder| E[Transformer Encoder] D -->|Streaming| F[Conformer Encoder] E --> G[Autoregressive Decoder] F --> H[CTC/Transducer Decoder] G --> I[Text Tokens] H --> J[Streaming Text] I --> K[Final Transcript] J --> K style A fill:#4CAF50,color:#fff style K fill:#2196F3,color:#fff style D fill:#FF9800,color:#fff

The key trade-off: encoder-decoder models like Whisper tend to be more accurate (they see the full context before decoding), while streaming models prioritize low latency (they output text as audio arrives). In 2026, hybrid architectures like NVIDIA's SALM are starting to combine both advantages.


OpenAI Whisper

Whisper changed the STT landscape when OpenAI released it as open-source in 2022. By 2026, Whisper Large v3 and its Turbo variant remain among the most accurate transcription models available -- and you can run them on your own hardware for free.

Models Available

Model Decoder Layers VRAM WER (LibriSpeech Clean) Speed
Large v3 32 ~10 GB 2.1% Baseline
Large v3 Turbo 4 ~6 GB ~3.1% 216x real-time
GPT-4o Transcribe - API only Improved API only
GPT-4o Mini Transcribe - API only Good API only

The Large v3 Turbo is the sweet spot for most use cases: it prunes 28 of 32 decoder layers (keeping only 4), which reduces VRAM from 10 GB to 6 GB and increases speed dramatically, with only about 1% higher WER than the full model.

GPT-4o Transcribe is OpenAI's newest addition -- it uses the GPT-4o architecture for context-aware transcription that better handles ambiguous audio, domain-specific terms, and code-switching between languages.

How Whisper Works

Whisper is an encoder-decoder transformer trained on 680,000 hours of multilingual audio from the web. It processes audio in 30-second chunks, converting mel spectrograms into text tokens. The model handles transcription, translation, language detection, and timestamp generation in a single pass.

Strengths

  • Accuracy: Among the best word error rates across languages and conditions (2.1% WER on clean English)
  • Open-source: Run it anywhere -- your laptop, your server, an air-gapped facility
  • Multilingual: Supports 100+ languages out of the box
  • Zero cost: No per-minute API charges when self-hosted
  • Robust: Handles background noise, accents, and poor audio quality well
  • Ecosystem: faster-whisper, whisper.cpp, insanely-fast-whisper, and dozens of optimization tools

Weaknesses

  • Not real-time by default: Designed for batch processing, not streaming
  • GPU-hungry: Large v3 needs a beefy GPU for fast inference
  • No built-in streaming: Requires additional frameworks (faster-whisper, whisper-streaming) for real-time use
  • 30-second chunking: Can cause issues at chunk boundaries (mid-word splits)
  • No built-in diarization: Need separate models for speaker identification

Code Example

# Using faster-whisper for optimized inference (4x faster than original)
from faster_whisper import WhisperModel

# Load model -- uses CTranslate2 for massive speedup
model = WhisperModel("large-v3-turbo", device="cuda", compute_type="float16")

# Transcribe a file with VAD filtering
segments, info = model.transcribe(
    "audio.wav",
    beam_size=5,
    language="en",
    vad_filter=True,         # Skip silence -- reduces processing time
    vad_parameters=dict(
        min_silence_duration_ms=500
    )
)

print(f"Detected language: {info.language} ({info.language_probability:.0%})")

for segment in segments:
    print(f"[{segment.start:.2f}s -> {segment.end:.2f}s] {segment.text}")
# Using the OpenAI API (GPT-4o Transcribe -- newest model)
from openai import OpenAI

client = OpenAI(api_key="your-api-key")

with open("audio.wav", "rb") as audio_file:
    # GPT-4o Transcribe: context-aware, handles domain jargon better
    transcript = client.audio.transcriptions.create(
        model="gpt-4o-transcribe",
        file=audio_file,
        response_format="verbose_json",
        timestamp_granularities=["word"]
    )

print(transcript.text)
for word in transcript.words:
    print(f"  {word.word} ({word.start:.2f}s - {word.end:.2f}s)")

Performance Benchmarks

Configuration Speed Notes
Large v3 Turbo, RTX 4090 216x real-time 60-min file in ~17 seconds
Large v3 Turbo, RTX 3080 ~50x real-time 60-min file in ~1.2 minutes
insanely-fast-whisper + Flash Attention 2, RTX 4090 70-100x real-time 10-min file in <8 seconds
Large v3 Turbo, Apple Silicon (CoreML) ~1.23s for short clips Encoder on Apple Neural Engine
Large v3, CPU (16-core) ~1-2x real-time Usable for batch, not real-time

API Pricing

Model Per Minute Per Hour
Whisper-1 $0.006 $0.36
GPT-4o Transcribe $0.006 $0.36
GPT-4o Mini Transcribe $0.003 $0.18

Deepgram Nova-3

Deepgram built their business on speed. Nova-3 (GA February 2025, multilingual GA April 2025) is designed from the ground up for real-time streaming transcription with minimal latency. If you're building a voice agent that needs instant transcription, Deepgram is the first place to look.

How It Works

Deepgram uses a proprietary end-to-end deep learning architecture optimized for streaming. Unlike Whisper's chunk-based approach, Deepgram processes audio as a continuous stream, outputting interim and final transcripts with minimal delay.

Key Claims (Deepgram's Own Benchmarks)

  • 53.4% WER reduction on streaming vs competitors
  • 47.4% WER reduction on batch vs competitors
  • Up to 36% lower WER than OpenAI Whisper on select datasets
  • Up to 40x faster than competing diarization-enabled models

Independent Benchmarks

  • 88-92% accuracy on clear English audio
  • ~18% WER on mixed real-world datasets (AA-WER benchmarks)
  • Sub-300ms transcript delivery for streaming

Strengths

  • Streaming-first: Built for real-time, sub-300ms latency on final results
  • Speed: Fastest commercial STT for real-time applications
  • Smart features: Built-in diarization, topic detection, sentiment, summarization
  • Real-time redaction: Automatically redact up to 50 entity types (SSN, credit cards, etc.)
  • Word-level timestamps: Precise alignment for subtitle generation
  • Custom vocabulary: Boost recognition of domain-specific terms
  • Endpointing: Excellent at detecting when a speaker has finished talking

Weaknesses

  • Cost at scale: More expensive than self-hosted Whisper
  • Proprietary: No self-hosting option, API-only
  • English-centric: Best performance in English, other languages lag
  • Independent benchmarks differ from claims: Deepgram's own benchmarks show much better results than independent tests

Code Example

import asyncio
from deepgram import DeepgramClient, LiveTranscriptionEvents, LiveOptions

async def transcribe_stream():
    deepgram = DeepgramClient(api_key="your-api-key")

    # Create a live transcription connection
    connection = deepgram.listen.live.v("1")

    # Configure options
    options = LiveOptions(
        model="nova-3",
        language="en-US",
        smart_format=True,      # Punctuation, casing, numbers
        interim_results=True,    # Get partial results as user speaks
        utterance_end_ms=1000,   # Detect end of utterance
        vad_events=True,         # Voice activity detection
        endpointing=300,         # 300ms silence = end of speech
        diarize=True,            # Speaker identification
        redact=["pci", "ssn"]    # Auto-redact sensitive info
    )

    # Handle transcription events
    @connection.on(LiveTranscriptionEvents.Transcript)
    def on_transcript(self, result, **kwargs):
        transcript = result.channel.alternatives[0]
        if transcript.transcript:
            prefix = "INTERIM" if not result.is_final else "FINAL"
            print(f"[{prefix}] {transcript.transcript}")

    @connection.on(LiveTranscriptionEvents.UtteranceEnd)
    def on_utterance_end(self, utterance_end, **kwargs):
        print("--- Speaker finished ---")

    # Start connection
    await connection.start(options)

    # Stream audio from microphone or file
    with open("audio.wav", "rb") as audio:
        while chunk := audio.read(4096):
            await connection.send(chunk)
            await asyncio.sleep(0.01)  # Pace the stream

    await connection.finish()

asyncio.run(transcribe_stream())

Pricing

Plan Streaming Batch Notes
Pay-as-you-go $0.0077/min $0.0043/min No commitment
Growth $0.0065/min ~$0.004/min Volume discounts
Enterprise Custom Custom Dedicated support

Latency

Mode Latency
Interim results 100-200ms
Final results 300-500ms
Endpointing Configurable (100-2000ms)

Google Cloud Speech-to-Text

Google's Speech-to-Text API now runs on Chirp 3 (GA), their latest universal speech model. It's the enterprise choice with the widest language support and the deepest integration with Google Cloud services.

Chirp 3 Features

  • 85+ languages and locales
  • Speaker diarization and automatic language detection
  • Built-in denoiser for noisy audio
  • Real-time streaming transcription
  • Speech adaptation for custom vocabularies
  • Trained on millions of hours of audio + 28 billion text sentences across 100+ languages

Strengths

  • Language support: 85+ languages and variants -- the most extensive coverage
  • Enterprise features: Speaker diarization, content filtering, data logging controls, HIPAA compliance
  • Medical/Legal models: Specialized models for domain-specific vocabulary
  • Global infrastructure: Low-latency endpoints worldwide
  • Built-in denoiser: Hardware-level noise reduction
  • Dynamic Batch: 75% discount for non-urgent transcription (results within 24 hours)
  • Multi-channel: Process stereo audio with per-channel recognition

Weaknesses

  • Pricing: Most expensive standard pricing among major providers ($0.016/min standard)
  • Complexity: More configuration and boilerplate than competitors
  • API verbosity: V2 API is significantly more complex than V1
  • Independent accuracy: ~11.6% WER on mixed benchmarks -- behind Whisper and specialized providers

Code Example

from google.cloud import speech_v2 as speech

def transcribe_streaming(audio_file: str, project_id: str):
    """Stream audio to Google Chirp 3 for real-time transcription."""
    client = speech.SpeechClient()

    # Configure Chirp 3 recognition
    config = speech.RecognitionConfig(
        auto_decoding_config=speech.AutoDetectDecodingConfig(),
        language_codes=["en-US"],
        model="chirp_3",
        features=speech.RecognitionFeatures(
            enable_automatic_punctuation=True,
            enable_word_time_offsets=True,
            enable_spoken_punctuation=True,
            diarization_config=speech.SpeechAdaptation.DiarizationConfig(
                min_speaker_count=2,
                max_speaker_count=6
            )
        )
    )

    recognizer = f"projects/{project_id}/locations/global/recognizers/_"

    def stream_generator():
        yield speech.StreamingRecognizeRequest(
            recognizer=recognizer,
            streaming_config=speech.StreamingRecognitionConfig(
                config=config,
                streaming_features=speech.StreamingRecognitionFeatures(
                    interim_results=True
                )
            )
        )
        # Stream audio in 100ms chunks (Google's recommendation)
        with open(audio_file, "rb") as f:
            while chunk := f.read(3200):  # 100ms at 16kHz, 16-bit
                yield speech.StreamingRecognizeRequest(audio=chunk)

    responses = client.streaming_recognize(requests=stream_generator())
    for response in responses:
        for result in response.results:
            status = "FINAL" if result.is_final else "INTERIM"
            print(f"[{status}] {result.alternatives[0].transcript}")

transcribe_streaming("audio.wav", "your-gcp-project")

Pricing

Model Per Minute Dynamic Batch Notes
Standard $0.016 $0.004 General purpose
Enhanced (phone/video) $0.036 - Optimized for telephony
Medical dictation $0.078 - Specialized vocabulary

AssemblyAI Universal-2

AssemblyAI has quietly become one of the most feature-rich STT providers. Universal-2 supports 99 languages with built-in features that competitors charge extra for -- diarization, sentiment analysis, topic detection, and more.

Key Improvements Over Predecessor

  • 24% improvement in rare word recognition (names, brands, locations)
  • 15% improvement in transcript structure (punctuation, casing)
  • 21% improvement on numerical data (phone numbers, zip codes)
  • 64% fewer speaker counting errors

Strengths

  • Streaming accuracy: ~14.5% WER on independent streaming benchmarks -- among the best
  • 99 languages: Including automatic code-switching (mid-sentence language switching)
  • Rich features included: Diarization, sentiment, topics, PII redaction, summarization
  • Custom vocabulary: Boost up to 200 key terms for domain-specific recognition
  • Pricing: Cheapest base rate at $0.0025/min ($0.15/hour)

Weaknesses

  • Add-on costs: Base pricing is cheap, but features stack up (diarization +$0.02/hr, PII redaction +$0.08/hr)
  • Smaller brand: Less enterprise recognition than Google or AWS
  • API-only: No self-hosting option

Code Example

import assemblyai as aai

aai.settings.api_key = "your-api-key"

# Real-time streaming transcription
transcriber = aai.RealtimeTranscriber(
    sample_rate=16000,
    word_boost=["AmtocSoft", "Pipecat", "LiveKit"],  # Boost domain terms
    encoding=aai.AudioEncoding.pcm_s16le,
    on_data=lambda transcript: print(
        f"[{'FINAL' if transcript.message_type == 'FinalTranscript' else 'PARTIAL'}] "
        f"{transcript.text}"
    ),
    on_error=lambda error: print(f"Error: {error}")
)

transcriber.connect()

# Stream audio chunks...
# transcriber.stream(audio_bytes)

transcriber.close()
# Batch transcription with all features
config = aai.TranscriptionConfig(
    speech_model=aai.SpeechModel.best,
    speaker_labels=True,         # Diarization
    sentiment_analysis=True,     # Per-utterance sentiment
    entity_detection=True,       # Named entities
    auto_chapters=True,          # Auto-chapter generation
    summarization=True,          # Meeting summary
    language_detection=True      # Auto-detect language
)

transcript = aai.Transcriber().transcribe("audio.wav", config)

for utterance in transcript.utterances:
    print(f"Speaker {utterance.speaker}: {utterance.text}")

if transcript.summary:
    print(f"\nSummary: {transcript.summary}")

Pricing

Feature Cost
Base transcription $0.15/hr ($0.0025/min)
Speaker diarization +$0.02/hr
Sentiment analysis +$0.02/hr
PII redaction +$0.08/hr
Summarization +$0.03/hr
All features combined ~$0.30/hr

NVIDIA Canary-Qwen-2.5B (The New #1)

NVIDIA's Canary model hit #1 on the Hugging Face Open ASR Leaderboard in July 2025 and has held the position since. It represents a new breed of STT: a hybrid ASR-LLM architecture that combines a FastConformer encoder with a Qwen3-1.7B language model decoder.

Architecture: SALM (Speech-Augmented Language Model)

  • Encoder: FastConformer (audio processing)
  • Decoder: Qwen3-1.7B LLM with LoRA adapters
  • Total parameters: 2.5B
  • Training data: 234,000 hours of public speech data
  • License: CC-BY-4.0 (open-source)

Key Stats

  • 418x real-time factor -- extremely fast inference
  • Average WER: 5.63% across all benchmarks
  • LibriSpeech Clean: 1.6% WER (best-in-class)
  • LibriSpeech Other: 3.1% WER

Why It Matters

Canary proves that combining ASR with LLM reasoning produces fundamentally better transcription. The LLM decoder can use linguistic context to resolve ambiguous audio -- "their" vs "there" vs "they're" becomes trivial when the model understands grammar.

Other Notable NVIDIA Models

  • Canary-1b-v2: 25 languages, comparable to models 3x larger, up to 10x faster
  • Parakeet-tdt-0.6b-v3: Transcribes 24-minute audio in a single inference pass

Accuracy Comparison: Word Error Rate

Word Error Rate (WER) is the standard metric for STT accuracy. Lower is better. But there's a critical caveat: academic benchmarks and production performance are very different things.

Academic Benchmarks (Clean Audio)

Model LibriSpeech Clean LibriSpeech Other
NVIDIA Canary-Qwen-2.5B 1.6% 3.1%
Whisper Large v3 2.1% ~4.5%
Whisper Large v3 Turbo ~3.1% ~5.5%
Soniox ~2.5% ~5.0%

Independent Real-World Benchmarks (Mixed Audio)

Model WER (mixed real-world) Notes
Soniox ~6.5% English-focused
Speechmatics ~9.3% Enterprise
Google Chirp 3 (batch) ~11.6% Broad language support
AssemblyAI Universal-2 (streaming) ~14.5% Best streaming accuracy
Deepgram Nova-3 (mixed) ~18% Optimized for speed

The Academic-Production Gap

Critical caveat: Academic benchmarks (LibriSpeech) show 95%+ accuracy. But production performance with background noise, overlapping speakers, accents, and domain jargon often drops to 70-85%. A model with 2% WER on LibriSpeech might have 15-20% WER on your actual call center audio.

Always benchmark on your own data before choosing a provider.

graph TD A[Audio Quality] --> B{Clean Studio?} B -->|Yes| C[WER 2-5%
All models perform well] B -->|No| D{Phone/Compressed?} D -->|Yes| E[WER 6-12%
Deepgram excels here] D -->|No| F{Noisy Environment?} F -->|Yes| G[WER 10-20%
Pre-process with denoiser] F -->|No| H{Heavy Accent?} H -->|Yes| I[WER 8-15%
Whisper best for accents] H -->|No| J{Multi-language?} J -->|Yes| K[WER 10-20%
Google Chirp or Whisper] J -->|No| L[WER 5-10%
Standard use case] style A fill:#9C27B0,color:#fff style C fill:#4CAF50,color:#fff style E fill:#2196F3,color:#fff style G fill:#FF9800,color:#fff style I fill:#FF9800,color:#fff style K fill:#FF9800,color:#fff style L fill:#4CAF50,color:#fff

Streaming vs Batch: Why It Matters

The biggest architectural decision in STT is whether you need real-time streaming or batch processing. This choice affects accuracy, cost, and infrastructure.

Batch Processing

Process a complete audio file after recording. Best for:
- Transcribing meetings after they end
- Processing podcast episodes and generating subtitles
- Analyzing call center recordings
- Generating training data for fine-tuning

All solutions handle batch well. Self-hosted Whisper is the cost winner. Google Dynamic Batch offers 75% discounts for non-urgent jobs.

Real-Time Streaming

Process audio as it arrives, generating text with minimal delay. Required for:
- Voice agents and chatbots (the 300ms rule)
- Live captioning and accessibility
- Real-time translation
- Voice-controlled interfaces

For streaming, the ranking: Deepgram > AssemblyAI > Google > Whisper. Deepgram was purpose-built for streaming. AssemblyAI has the best streaming accuracy. Google has solid enterprise streaming. Whisper requires additional tooling and still can't match native streaming latency.

The 300ms Rule

Human conversation has a natural pause of about 300ms between turns. When voice AI response time exceeds this threshold, it triggers neurological stress in users -- the conversation feels "off." Industry median reality is 1.4-1.7 seconds, which is 5x slower than the human expectation. Getting STT latency down is critical because it's the first link in the chain.


Handling the Hard Cases

Background Noise

  • Pre-process audio with noise reduction (RNNoise, Demucs, Google's built-in denoiser)
  • Use Voice Activity Detection (VAD) to skip silence and noise-only segments
  • Chirp 3's built-in denoiser handles this automatically

Domain-Specific Vocabulary

# Deepgram: Boost specific keywords
options = LiveOptions(
    model="nova-3",
    keywords=["AmtocSoft:2", "Pipecat:2", "LiveKit:1.5", "WebRTC:2"]
    # Numbers are boost weights (higher = stronger bias)
)

# AssemblyAI: Word boost (up to 200 terms)
config = aai.TranscriptionConfig(
    word_boost=["AmtocSoft", "Pipecat", "LiveKit", "WebRTC"],
    boost_param=aai.WordBoost.high
)

# Google: Speech adaptation phrases
config = speech.RecognitionConfig(
    adaptation=speech.SpeechAdaptation(
        phrase_sets=[
            speech.SpeechAdaptation.AdaptationPhraseSet(
                inline_phrase_set=speech.PhraseSet(
                    phrases=[
                        speech.PhraseSet.Phrase(value="AmtocSoft", boost=10),
                        speech.PhraseSet.Phrase(value="Pipecat", boost=10),
                    ]
                )
            )
        ]
    )
)

Code-Switching (Multilingual Speakers)

When speakers switch between languages mid-sentence:
- AssemblyAI Universal-2: Built-in code-switching detection across 99 languages
- Whisper: Handles this well due to multilingual training on 680K hours
- Google Chirp 3: Automatic language detection, but less reliable on mid-sentence switches
- Deepgram: Requires specifying a single primary language


Cost Analysis at Scale

Let's model costs for realistic workloads:

Per-Minute Pricing Comparison

Provider Per Minute Per Hour 10K Hours/Month
AssemblyAI (base) $0.0025 $0.15 $1,500
GPT-4o Mini Transcribe $0.003 $0.18 $1,800
Google Dynamic Batch $0.004 $0.24 $2,400
Deepgram (batch) $0.0043 $0.26 $2,580
OpenAI Whisper-1 / GPT-4o Transcribe $0.006 $0.36 $3,600
Deepgram (streaming, Growth) $0.0065 $0.39 $3,900
Deepgram (streaming, PAYG) $0.0077 $0.46 $4,620
Google (standard) $0.016 $0.96 $9,600
Google (enhanced) $0.036 $2.16 $21,600

Self-Hosted Whisper

For batch processing at massive scale, self-hosting Whisper is unbeatable:
- Salad Cloud benchmark: $5,110 for 1 million hours of transcription using distributed GPU compute
- Single RTX 4090 server: ~$150-300/month handles ~30,000 hours/month at 216x real-time
- Break-even point: Self-hosting beats API pricing above ~5,000 hours/month

Hidden Costs to Watch

  • AssemblyAI add-ons: Base is cheap ($0.15/hr), but diarization + PII redaction + sentiment = $0.30/hr
  • Google enhanced models: 2.25x the standard price
  • Concurrency limits: Free/lower tiers cap concurrent streams -- production workloads need plan upgrades
  • Deepgram streaming vs batch: Streaming costs nearly 2x batch pricing
Comparison Visual: Cost per hour across all providers

The STT Decision Flow

graph TD START[What's your primary use case?] --> Q1{Real-time streaming?} Q1 -->|Yes| Q2{Latency or accuracy priority?} Q2 -->|Latency| DEEPGRAM[Deepgram Nova-3
Sub-300ms streaming] Q2 -->|Accuracy| ASSEMBLY[AssemblyAI Universal-2
Best streaming WER] Q1 -->|No - Batch| Q3{Budget priority?} Q3 -->|Minimum cost| Q4{Have GPU infrastructure?} Q4 -->|Yes| WHISPER[Self-hosted Whisper
$5K per 1M hours] Q4 -->|No| ASSEMBLY2[AssemblyAI
$0.0025/min base] Q3 -->|Best accuracy| CANARY[NVIDIA Canary
#1 on Open ASR] Q3 -->|Enterprise compliance| GOOGLE[Google Chirp 3
85+ languages, HIPAA] START --> Q5{Multilingual?} Q5 -->|99 languages| ASSEMBLY3[AssemblyAI Universal-2] Q5 -->|100+ languages| WHISPER2[Whisper Large v3] Q5 -->|85+ with enterprise| GOOGLE2[Google Chirp 3] style START fill:#9C27B0,color:#fff style DEEPGRAM fill:#4CAF50,color:#fff style ASSEMBLY fill:#4CAF50,color:#fff style WHISPER fill:#4CAF50,color:#fff style ASSEMBLY2 fill:#4CAF50,color:#fff style CANARY fill:#4CAF50,color:#fff style GOOGLE fill:#4CAF50,color:#fff style ASSEMBLY3 fill:#2196F3,color:#fff style WHISPER2 fill:#2196F3,color:#fff style GOOGLE2 fill:#2196F3,color:#fff

Recommendation Summary

Use Case Recommended Why
Voice agent (real-time) Deepgram Nova-3 Lowest streaming latency, built for real-time
Voice agent (accuracy-first) AssemblyAI Universal-2 Best streaming WER at $0.0025/min
Meeting transcription (batch) Whisper (self-hosted) Zero API costs, excellent accuracy
Maximum accuracy (batch) NVIDIA Canary (self-hosted) #1 on Open ASR leaderboard
Multilingual (100+ languages) Whisper Large v3 Best multilingual coverage
Enterprise / compliance Google Chirp 3 HIPAA, global infrastructure, 85+ languages
Budget-constrained API GPT-4o Mini Transcribe $0.003/min, good quality
Offline / air-gapped Whisper (self-hosted) Fully self-contained

The best approach for most voice AI projects: use Deepgram or AssemblyAI for real-time streaming (voice agents, live captioning) and self-hosted Whisper or NVIDIA Canary for batch processing (transcription, subtitles, analysis). This gives you the best of both worlds -- speed where it counts, cost savings where it doesn't.

Sources & References:
1. OpenAI — "Whisper" — https://openai.com/index/whisper/
2. Deepgram — "Nova-3 Speech-to-Text" — https://deepgram.com/
3. Hugging Face — "Open ASR Leaderboard" — https://huggingface.co/spaces/open-asr-leaderboard/open_asr_leaderboard


This is part 3 of the AmtocSoft Voice AI series. Next: build your first voice agent with Python and Pipecat.

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-04-06 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

What Is Voice AI? TTS, STT, and Voice Agents Explained

What Is Voice AI Hero

What Is Voice AI? TTS, STT, and Voice Agents Explained

Level: Beginner
Topic: Voice AI, TTS, STT

Voice AI is everywhere in 2026 -- calling your doctor's office, answering support lines, running drive-through orders, and powering real-time translation earbuds. But most people have no idea how it actually works under the hood.

In this post, we'll break down the three pillars of voice AI, explain how they fit together into a complete system, and show you where this technology is headed.


graph LR
  A["User Speech"] --> B["Microphone"]
  B --> C["Speech-to-Text (STT)"]
  C --> D["Natural Language Understanding"]
  D --> E["AI Processing"]
  E --> F["Text-to-Speech (TTS)"]
  F --> G["Speaker"]
  G --> H["User Hears Response"]

The Three Pillars of Voice AI

Architecture Diagram

Every voice AI system is built on three core technologies. Think of them as a relay race where each runner handles one leg:

1. Speech-to-Text (STT) -- Listening

Speech-to-text, also called automatic speech recognition (ASR), converts spoken audio into written text. When you talk to Siri, Alexa, or Google Assistant, the first thing that happens is your voice gets transcribed into words.

How it works at a high level:

  1. Audio capture: A microphone records your voice as a waveform
  2. Feature extraction: The system breaks the audio into small frames (usually 20-30ms each) and extracts acoustic features
  3. Model inference: A neural network maps those features to text tokens
  4. Language model: A decoder uses language patterns to pick the most likely sequence of words

Modern STT models like OpenAI's Whisper and Deepgram Nova can achieve word error rates below 5% on clean English audio -- meaning they get 95+ words right out of every 100.

2. Text-to-Speech (TTS) -- Speaking

Text-to-speech does the reverse: it takes written text and produces natural-sounding audio. This is what makes AI assistants sound human instead of robotic.

The evolution has been dramatic:

  • 2015 and earlier: Concatenative synthesis -- stitching pre-recorded phonemes together. Sounded choppy and mechanical
  • 2018-2022: Neural TTS models like Tacotron and WaveNet brought natural intonation and rhythm
  • 2024-2026: Models like ElevenLabs and OpenAI TTS produce voices nearly indistinguishable from real humans, with emotional expressiveness and consistent character

Modern TTS systems work by:

  1. Text analysis: Breaking text into phonemes, handling punctuation, numbers, abbreviations
  2. Prosody prediction: Determining pitch, speed, emphasis, and emotional tone
  3. Audio synthesis: Generating the actual waveform using a neural vocoder

3. Voice Agents -- Thinking

A voice agent combines STT and TTS with an AI brain (typically a large language model) to hold actual conversations. This is where things get interesting.

Instead of just transcribing or speaking, a voice agent:

  • Listens to what you say (STT)
  • Understands your intent and formulates a response (LLM)
  • Speaks the response back to you (TTS)
  • Manages the conversation flow -- knowing when to speak, when to listen, and when to interrupt

Voice agents are what power the AI receptionists, customer support lines, and interview bots you've been encountering more and more in 2026.


How the Full Stack Works End-to-End

Let's trace what happens when you call an AI-powered support line:

You speak: "I need to reschedule my appointment to next Tuesday"
        |
        v
[1. Audio Capture] -- Microphone picks up your voice
        |
        v
[2. STT Engine] -- Converts speech to text
   Output: "I need to reschedule my appointment to next Tuesday"
        |
        v
[3. LLM Processing] -- Understands intent, checks calendar,
   formulates response     finds available slots
        |
        v
[4. TTS Engine] -- Converts response text to speech audio
        |
        v
[5. Audio Playback] -- You hear: "I can reschedule you for
   Tuesday at 10am or 2pm. Which works better?"

This entire loop -- from the moment you stop speaking to the moment you hear a response -- needs to happen fast. How fast? That brings us to the most critical metric in voice AI.


The 500ms Latency Threshold

Research in conversational dynamics has consistently shown that humans expect responses within about 500 milliseconds in natural conversation. Go beyond that, and the interaction starts to feel awkward. Past 1 second, it feels broken. Past 2 seconds, people hang up.

Here's what eats into that budget:

Stage Typical Latency Target
Audio capture + network 50-100ms 50ms
STT processing 100-300ms 100ms
LLM inference 200-800ms 200ms
TTS generation 100-300ms 100ms
Audio delivery 50-100ms 50ms
Total 500-1600ms 500ms

Getting the total under 500ms requires optimization at every stage:

  • Streaming STT: Start processing audio before the user finishes speaking
  • LLM streaming: Begin generating the response token by token, don't wait for the complete answer
  • TTS chunking: Start synthesizing audio from the first sentence while the LLM is still generating the rest
  • Edge deployment: Run components closer to the user to reduce network round trips

The best systems in 2026 achieve 300-500ms end-to-end latency by overlapping these stages -- the STT is still finishing while the LLM starts reasoning, and the TTS begins speaking while the LLM is still generating the tail end of the response.


Where Voice AI Is Used Today

Customer Support

The most visible application. Companies like airlines, banks, and healthcare providers use voice agents to handle routine calls -- appointment scheduling, order status, account inquiries. The best implementations handle 60-80% of calls without human transfer.

Healthcare

AI scribes listen to doctor-patient conversations and automatically generate clinical notes. Voice agents handle appointment scheduling, prescription refill requests, and symptom triage. This saves clinicians 1-2 hours of documentation time per day.

Drive-Through Ordering

Fast food chains are deploying voice AI to take orders at drive-through windows. The system handles menu questions, customizations, upselling, and payment -- all through natural conversation.

Real-Time Translation

Voice AI powers real-time translation devices and apps. Speak in English, and the person across the table hears your words in Japanese within a second. The pipeline is STT (English) then Machine Translation then TTS (Japanese).

Accessibility

Screen readers with natural-sounding TTS voices make digital content accessible to visually impaired users. Voice-controlled interfaces help people with motor disabilities navigate devices and applications.

Podcasting and Content Creation

AI voices now narrate audiobooks, generate podcast episodes, and dub video content into multiple languages. Content creators use TTS to produce audio versions of their written content without recording a single word themselves.

Voice Commerce

Shopping by voice is growing rapidly. Voice agents help customers browse products, compare options, and complete purchases -- all through conversation. Think of it as a personal shopping assistant you can call anytime.


Key Concepts to Know

Wake Words

A wake word (like "Hey Siri" or "Alexa") is a small, always-listening model that detects a specific phrase and activates the full voice AI pipeline. These models are tiny -- they run on microphone chips consuming microwatts of power.

Voice Activity Detection (VAD)

VAD determines when someone is speaking vs. when there's silence or background noise. It's essential for knowing when to start and stop STT processing, and for managing turn-taking in conversations.

Turn-Taking

In human conversation, we naturally know when it's our turn to speak. Voice agents need to replicate this -- detecting when the user has finished their thought (not just paused) and when they're expecting a response.

Voice Cloning

Modern TTS systems can clone a person's voice from as little as 15 seconds of sample audio. This enables personalized voice assistants, preserving a loved one's voice, and dubbing content in the original speaker's voice across languages.


The Voice AI Stack in 2026

If you're building with voice AI today, here's the typical technology stack:

Layer Options
STT Whisper, Deepgram Nova, Google Speech, AssemblyAI
LLM Claude, GPT-4o, Gemini, Llama 3
TTS ElevenLabs, OpenAI TTS, Kokoro, Piper
Orchestration Pipecat, LiveKit Agents, Vocode
Telephony Twilio, Vonage, Telnyx
Infrastructure AWS, GCP, or self-hosted GPU servers

The exciting part: you can build a functional voice agent today with open-source tools and free-tier APIs. The barrier to entry has never been lower.


What's Next

In the next posts in this series, we'll go deeper into each layer:

  • TTS comparison: ElevenLabs vs OpenAI vs open-source models -- quality, cost, and latency benchmarks
  • STT showdown: Whisper vs Deepgram vs Google -- which one should you use?
  • Build a voice agent: A hands-on tutorial using Python and Pipecat
  • Architecture deep dive: Pipeline vs end-to-end approaches
  • Production guide: Scaling, monitoring, and cost optimization

Voice AI is one of the fastest-moving areas in tech right now. Understanding the fundamentals puts you in a strong position to build with it, evaluate vendors, or simply understand what's happening when you talk to an AI on the phone.

Sources & References:
1. Google — "Text-to-Speech Documentation" — https://cloud.google.com/text-to-speech
2. OpenAI — "Whisper: Robust Speech Recognition" — https://openai.com/index/whisper/
3. Pipecat — "Build Voice Agents" — https://github.com/pipecat-ai/pipecat


This is part 1 of the AmtocSoft Voice AI series. Follow along as we go from fundamentals to production-ready voice agents.

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-04-06 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

Attention Is All You Need, Explained Simply

We published a plain-language walkthrough of the 2017 transformer paper — queries, keys, values, multi-head attention, and why no-recurrence...