Python speech to text: file, stream, and mic code

Rene, Kabir Goel 
Python speech to text: file, stream, and mic code

To convert speech to text in Python, you send audio to a speech recognition model and get text back. The interesting decision is which kind of model, because a recorded file and a live conversation are different jobs. A file can wait for the whole clip. A caller can’t.

This guide covers three ways to do it, each with code that runs as written: transcribe a recorded file, stream audio with automatic turn detection, and transcribe your microphone live. The streaming examples use Cartesia Ink 2, the model we build for voice agents. Where a free local model is the better fit, we say so and show that too.

Pick an approach

You haveUseWhy
Recorded files, no audio can leavefaster-whisperFree, local, accurate on clean audio. Batch only
Recorded files, no GPU to manageA batch STT APIOne HTTP call per file
Live audio: an agent, captions, a phoneA streaming STT API with turn detectionText arrives while the person talks, and the model says when they stop
A weekend scriptThe SpeechRecognition packageFast to try. Its default Google key is for testing only

Most tutorials stop at SpeechRecognition with recognize_google(). That package is a handy wrapper over a dozen engines, but its default Google backend uses a shared key that Google may revoke at any time, and it waits for a full phrase before it sends anything. Fine for a demo. Not something you want answering phones.

Prerequisites

  • Python 3.9 or later
  • For the Cartesia examples, an API key from the Cartesia playground and the SDK:
python -m pip install "cartesia[websockets]"
export CARTESIA_API_KEY="your-api-key"

The websockets extra is needed for the streaming examples. Keep the key in an environment variable or a secret manager, never in code you commit.

1. Transcribe a recorded file

If the audio is already recorded, send the whole file in one request. Cartesia’s batch endpoint accepts containers like WAV and MP3, and can return a timestamp for each word. Save this as transcribe_file.py:

import os
import sys

from cartesia import Cartesia

client = Cartesia(api_key=os.environ["CARTESIA_API_KEY"])

with open(sys.argv[1], "rb") as audio_file:
    result = client.stt.transcribe(
        file=audio_file,
        model="ink-whisper",
        language="en",
        timestamp_granularities=["word"],
    )

print(result.text)
for word in result.words or []:
    print(f"{word.start:6.2f}s  {word.word}")

Run it with python transcribe_file.py meeting.mp3. Batch transcription uses ink-whisper, which covers about 100 languages; pass the language code of your audio. The batch STT example in the docs has the async version.

The local alternative

If the audio can’t leave your machine, or you have a lot of it and a GPU, run Whisper yourself. faster-whisper reimplements OpenAI’s Whisper on CTranslate2 and runs on a laptop CPU:

python -m pip install faster-whisper
from faster_whisper import WhisperModel

model = WhisperModel("base.en", device="cpu", compute_type="int8")
segments, info = model.transcribe("jfk.flac", beam_size=5)

print(f"Detected language: {info.language} ({info.language_probability:.2f})")
for segment in segments:
    print(f"[{segment.start:.2f}s -> {segment.end:.2f}s] {segment.text}")

On the 11-second JFK sample from the Whisper repo, the base.en model printed the quote word for word in about 2.5 seconds on a 4-core server CPU, after a one-time 140 MB model download. Larger models are more accurate and slower. What you don’t get is streaming: Whisper works on finished segments, so it’s the wrong tool for a conversation.

2. Stream audio and get turns back

A voice agent needs two things a batch model can’t give it: text while the person is still talking, and a decision about when they’ve finished. Ink 2 does both on one WebSocket. Its turn detection listens to what’s being said as well as to the silence, so a caller who pauses mid-sentence to think doesn’t get cut off just because the line went quiet.

This script streams a WAV file as if it were live audio. Save it as stream_file.py:

import os
import sys
import time
import wave

from cartesia import Cartesia

with wave.open(sys.argv[1], "rb") as wav:
    if wav.getnchannels() != 1 or wav.getsampwidth() != 2:
        raise SystemExit("Convert the file to 16-bit mono WAV first.")
    sample_rate = wav.getframerate()
    chunks = []
    while data := wav.readframes(sample_rate // 10):  # 100 ms per chunk
        chunks.append(data)

client = Cartesia(api_key=os.environ["CARTESIA_API_KEY"])
transcript = ""

with client.stt.auto_finalize.websocket(
    model="ink-2",
    encoding="pcm_s16le",
    sample_rate=sample_rate,
) as ws:
    for chunk in chunks:
        ws.send_raw(chunk)
        time.sleep(0.1)  # send at the speed of speech
    ws.send({"type": "close"})  # flush buffered audio; the server then closes

    turn_text = ""
    for event in ws:
        if event.type == "turn.update":
            turn_text = event.transcript or turn_text  # the whole turn so far
            print("...", turn_text)
        elif event.type == "turn.end":
            text = event.transcript or turn_text  # fall back if turn.end is empty
            print("turn:", text)
            transcript += text  # join verbatim, no strip()
            turn_text = ""
        elif event.type == "error":
            raise SystemExit(event.message)

print(transcript)

If your file isn’t 16-bit mono WAV, convert it first:

ffmpeg -i input.mp3 -ac 1 -ar 16000 -c:a pcm_s16le input.wav

Three details in that script are easy to get wrong:

  • Raw samples, not a file. Realtime endpoints take audio bytes with no header, which is why the script reads frames out of the WAV instead of sending the file. You declare encoding and sample_rate when you connect, and they have to match the bytes. The audio input guide notes the server might not return an error if they don’t, so a wrong rate shows up as a bad transcript, not an exception.
  • Real-time pacing. The sleep(0.1) after each 100 ms chunk isn’t decoration. Realtime endpoints expect about one second of audio per second, and dumping a whole file into the socket can overload the server-side buffer. For a file you don’t need live, use the batch call from step 1.
  • Cumulative transcripts. Every turn.update holds the whole turn so far, not a new fragment. Concatenating updates repeats words. Take the text from turn.end, once per turn, and join it exactly as it arrives. The script also keeps the latest turn.update and uses it if a turn.end ever arrives with an empty transcript, a case our own clients guard against.

To keep it short, this script reads events after it has sent the whole file, so everything prints at the end. The microphone example below sends and reads at the same time.

Ink 2 transcribes English, French, Hindi, Japanese and Spanish, and detects which one it’s hearing. For other languages on a live stream, see our speech-to-text model comparison.

3. Transcribe your microphone live

Same endpoint, real input. This version uses sounddevice to capture 16 kHz mono audio and the async client to send and receive at the same time:

python -m pip install sounddevice

On Linux you may also need PortAudio (sudo apt install libportaudio2). Save this as microphone.py:

import asyncio
import os

import sounddevice as sd
from cartesia import AsyncCartesia

SAMPLE_RATE = 16000
BLOCK = SAMPLE_RATE // 10  # 100 ms of audio per message


async def main():
    loop = asyncio.get_running_loop()
    audio = asyncio.Queue(maxsize=50)  # about 5 s of audio

    def enqueue(block):
        if audio.full():  # the network fell behind: drop the oldest block
            audio.get_nowait()
        audio.put_nowait(block)

    def on_audio(indata, frames, time_info, status):
        loop.call_soon_threadsafe(enqueue, bytes(indata))

    client = AsyncCartesia(api_key=os.environ["CARTESIA_API_KEY"])
    async with client.stt.auto_finalize.websocket(
        model="ink-2",
        encoding="pcm_s16le",
        sample_rate=SAMPLE_RATE,
    ) as ws:

        async def send():
            with sd.RawInputStream(
                samplerate=SAMPLE_RATE,
                blocksize=BLOCK,
                channels=1,
                dtype="int16",
                callback=on_audio,
            ):
                while True:
                    await ws.send_raw(await audio.get())

        async def receive():
            turn_text = ""
            async for event in ws:
                if event.type == "turn.start":
                    print("(listening)")
                elif event.type == "turn.update":
                    turn_text = event.transcript or turn_text
                elif event.type == "turn.end":
                    print("You said:", event.transcript or turn_text)
                    turn_text = ""
                elif event.type == "error":
                    raise RuntimeError(event.message)

        await asyncio.gather(send(), receive())


asyncio.run(main())

Run python microphone.py, say something, pause, and the transcript of that turn prints. Press Ctrl+C to stop.

The audio callback runs on a separate thread, so it hands each block to the event loop with call_soon_threadsafe instead of touching the socket directly. The microphone keeps sending while you’re quiet, which matters: the server treats missing audio as a network delay, not as silence, so a muted or paused input should send zeros rather than nothing. The queue is capped at about five seconds of audio: if the connection falls behind, the oldest blocks are dropped instead of piling up in memory, because a transcript of something said a minute ago is no use to a live conversation.

In a voice agent, turn.start is where you stop the bot talking, and turn.end is where you send the text to your LLM. To start the reply sooner, handle turn.eager_end too: begin generating when it fires, and throw the draft away if turn.resume follows. The turn detection docs list the thresholds if your callers pause a lot (or barely at all).

Common problems

SymptomLikely cause and fix
Transcript is gibberish or emptyDeclared encoding or sample_rate doesn’t match the audio. Save the bytes you send and play them with ffplay -f s16le -ar 16000 -ac 1 audio.raw
Words appear two or three timesYou joined turn.update events. Use turn.end only
The last second of speech is missingYou closed the socket without sending {"type": "close"}, or stopped reading events before the server closed
Turns never end while the mic is mutedYou stopped sending audio. Send silence (PCM samples of value zero) instead
Server error partway through a fileAudio sent faster than real time. Pace the chunks, or use batch
”You need to install cartesia[websockets]”Install the extra: python -m pip install "cartesia[websockets]"
Phone audio transcribes badlyPhone lines usually carry 8 kHz μ-law. Declare encoding="pcm_mulaw" and sample_rate=8000 instead of converting; see G.711 explained

What it costs

Local models cost compute and nothing else. On Cartesia, Ink 2 uses 3 credits per second of audio, and an hour of audio is $0.39 on the Scale plan; the free plan’s credits cover about an hour and 50 minutes of Ink 2. Current numbers are on the pricing page.

If you’d rather not run the pipeline yourself, Cartesia Managed Agents wire Ink, an LLM and Sonic text to speech onto a phone number for you. And if you only need to transcribe a handful of recordings, faster-whisper on your laptop is probably all you need.

Next steps

  1. Try Ink 2 on your own voice in the browser, no code, on the Ink page.
  2. Feed the transcripts into an LLM and speak the reply with Python text to speech.
  3. Put the pieces in a framework: build a Pipecat voice agent with Cartesia.

FAQs

How do I convert speech to text in Python?

Pick a speech recognition model, send it audio, and read back text. For a recorded file, a local model like faster-whisper or a hosted batch API returns the whole transcript in one call. For live audio, open a WebSocket to a streaming model such as Cartesia Ink 2, send small chunks of 16-bit PCM as they arrive, and handle transcript events as they come back.

What is the best Python library for speech recognition?

It depends on the audio. For recorded files on your own machine, faster-whisper is a strong free default. For a voice agent or anything live, use a streaming API with turn detection, because batch models like Whisper wait for the whole clip. The SpeechRecognition package is a wrapper over several engines; its default Google backend uses a shared test key that Google can revoke, so don't ship it in production.

Can Python do real-time speech to text?

Yes, with a streaming model. Capture microphone audio with a library like sounddevice, send it in chunks of about 100 milliseconds over a WebSocket, and read partial and final transcripts as they arrive. Send audio at the speed it was spoken; pushing a whole file at once can overload a realtime endpoint.

Can I do speech to text in Python offline?

Yes. faster-whisper and Vosk run locally after a one-time model download, so no audio leaves your machine. Expect to manage model size, CPU or GPU speed, and your own streaming logic. Hosted APIs like Cartesia's need a network connection and send audio to the provider.

What audio format does speech-to-text need?

Batch endpoints usually accept common files such as WAV and MP3. Realtime endpoints usually want raw samples with no file header, and you must declare the encoding and sample rate. Mono 16-bit PCM at 16 kHz is the safe default for speech. Phone audio is often 8 kHz μ-law instead.