Content & Marketing

Transcribe Audio with the Whisper API and Python

Turn any recording into a clean transcript in Python: check the upload limit, compress with pydub, call whisper-1, and pick the response format you need.

By the end of this guide you will have a Python file that takes a recording from your hard drive — an interview, a webinar, a voice memo — and writes an accurate text transcript next to it, usually in under a minute of runtime. You will also know how to get timestamped segments instead of plain text, and how to stop the model from mangling the names of your product and your guests.

Transcription is the first move in almost every repurposing job, which is why it opens AI Transcription and Content Repurposing with Python. Once the words exist as text, everything downstream — summaries, blog drafts, subtitle files, quote cards — is ordinary text handling.

Prerequisites

You need Python 3.10 or newer inside a virtual environment (an isolated folder of packages, so one project cannot break another). If you have not made one yet, follow Create a Python Virtual Environment for AI first.

Two extra pieces beyond the usual OpenAI setup: pydub, a small library for editing audio in Python, and ffmpeg, the command-line tool pydub leans on to actually decode and re-encode sound. pydub is a pip install; ffmpeg is not, so install it with your system package manager (brew install ffmpeg on Mac, sudo apt install ffmpeg on Ubuntu, or the official Windows build added to your PATH).

pip install "openai>=1.40" "pydub>=0.25" "python-dotenv>=1.0"
pip freeze > requirements.txt

Put your key in a .env file in the project folder:

OPENAI_API_KEY=sk-your-key-here

Then keep it out of version control before you write another line:

echo ".env" >> .gitignore

Grab a test recording you own — two or three minutes is plenty while you are wiring things up. A short file makes every mistake cheap to repeat.

Step 1 — Check the file against the upload limit

One request to the transcription endpoint carries roughly 25 MB of audio. That sounds generous until you meet a real file: an hour of stereo WAV from a podcast recorder is comfortably several hundred megabytes. So the first thing your script does is measure, not upload.

from pathlib import Path

AUDIO = Path("interview.m4a")
LIMIT_MB = 25

size_mb = AUDIO.stat().st_size / (1024 * 1024)
print(f"{AUDIO.name} is {size_mb:.1f} MB")

if size_mb > LIMIT_MB:
    print("Too heavy for one request — compress it first.")
else:
    print("Good to upload as-is.")

If it is too heavy, shrink it rather than splitting it. Speech carries almost no information in the second stereo channel or above a few kilohertz, and the model was trained on 16 kHz audio anyway, so downmixing to one channel at 16 kHz and re-encoding as a low-bitrate MP3 throws away nothing the model uses. A long recording typically drops by an order of magnitude.

from pathlib import Path
from pydub import AudioSegment

source = Path("interview.m4a")
target = source.with_name(f"{source.stem}-compressed.mp3")

audio = AudioSegment.from_file(source)
audio = audio.set_channels(1).set_frame_rate(16000)
audio.export(target, format="mp3", bitrate="32k")

print(f"Wrote {target.name}: {target.stat().st_size / (1024 * 1024):.1f} MB")

If a three-hour recording still lands over the limit after that, cut it into chunks with pydub slicing (audio[0:600_000] is the first ten minutes in milliseconds), transcribe each chunk separately, and join the returned strings in order. Cut on a pause rather than mid-sentence and the seam will be invisible in the finished text.

One detail worth internalising: AudioSegment.from_file reads whatever ffmpeg can decode, so the input extension barely matters. A .mov screen recording, a .opus voice note from a messaging app, a .aac stream — all of them come in through the same call and leave as a compliant MP3. That makes this compression step double as a format converter, which is why it belongs before the size check in any script you intend to run unattended.

Step 2 — Send the file and read the transcript

The call itself is short. You open the file in binary mode"rb", meaning Python hands the raw bytes over instead of trying to read them as text — and pass that open file to client.audio.transcriptions.create with model="whisper-1". The SDK handles the upload for you.

import os
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()  # reads .env, which is already in .gitignore
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

audio_path = Path("interview-compressed.mp3")

with audio_path.open("rb") as audio_file:
    transcript = client.audio.transcriptions.create(
        model="whisper-1",
        file=audio_file,
    )

Path("transcript.txt").write_text(transcript.text, encoding="utf-8")
print(transcript.text[:400])

Run it with python transcribe.py. The with block matters: it closes the file as soon as the request finishes, so a script that loops over a folder of recordings does not slowly run out of open file handles. The default reply is a small object with one useful attribute, .text, holding the whole transcript as a single string. That is the entire happy path — everything below is about controlling what comes back.

Expect the call to block for a while. Transcription is not instant the way a short chat completion is: the whole file uploads, then the model works through it, and a long recording on a slow connection can sit there for minutes. Nothing prints until it finishes, which looks identical to a hang the first time you see it. If you are processing a folder, print the filename before each call so you can tell progress from a stall, and give the client a generous timeout — OpenAI(api_key=..., timeout=600.0) sets ten minutes — rather than letting the default cut a legitimate long upload short.

The path a recording takes from disk to finished transcript A six-stage flow: the source audio file is measured against the upload limit, optionally compressed to mono MP3 with pydub, opened in binary mode, sent to the Whisper API, and returned as transcript text or timed segments. Source audio file interview.m4a Check the size 25 MB per request Compress if over pydub, mono MP3 Open the file binary read mode Whisper API call model whisper-1 Transcript back text or segments
Only the middle box is optional: measure first, compress only when the file exceeds the limit, and the rest of the path is identical for every recording.

Step 3 — Pick the response format that matches your next step

The response_format argument decides what shape the answer arrives in, and picking it correctly saves you from writing a parser later. Four values matter in practice.

text returns the transcript as a bare Python string — note that you get a plain str back, not an object, so transcript.text will raise an AttributeError if you forget you changed the format. json (the default) wraps that same string in an object with a .text attribute. srt and vtt hand you a finished subtitle file, numbering and timecodes included, ready to write straight to disk. And verbose_json gives you the transcript plus structured metadata: the detected language, the duration, and a list of segments with start and end times in seconds.

Reach for verbose_json whenever you plan to do anything with timing — finding a pull quote, locating the moment a topic starts, or building your own caption timings.

import os
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

with Path("interview-compressed.mp3").open("rb") as audio_file:
    result = client.audio.transcriptions.create(
        model="whisper-1",
        file=audio_file,
        response_format="verbose_json",
        timestamp_granularities=["segment"],
    )

print(f"Language: {result.language}  Duration: {result.duration:.1f}s")
for segment in result.segments:
    print(f"[{segment.start:7.2f} - {segment.end:7.2f}] {segment.text.strip()}")

The timestamp_granularities argument is what fills in .segments, and it only works alongside verbose_json. Ask for ["word"] instead and you get a .words list with a start and end time on every single word, which is what you want for karaoke-style captions or for trimming a video to an exact phrase. It is a much longer response for the same audio, so request word timings only when you will actually use them.

Each segment is a sentence or two of speech with its own clock times, which is exactly the raw material a subtitle builder needs — see Generate SRT Subtitles with Python and AI for turning those numbers into a caption file with sensible line breaks.

Choosing a response format from what you plan to do next A decision tree starting from the question of what you need, branching into plain text, verbose JSON with segments, and a subtitle file, each leading to the task it suits. What comes next? pick response_format Just the words format text or json Words plus times verbose_json Caption file srt or vtt Blog and social one plain string Find the clip start, end, text Load in an editor no parsing needed
Work backwards from the job: the format that needs the least post-processing for your next step is the right one to request.

Step 4 — Add a language hint and a name glossary

Left alone, the model detects the spoken language itself and guesses at unfamiliar words. Both guesses cost you something. Language detection occasionally latches onto the wrong language after a few seconds of music or crosstalk, and returns a transcript that is fluent nonsense. Unfamiliar words — your product name, a guest's surname, an internal acronym — come out spelled by ear, differently each time.

Two arguments fix both. language takes a two-letter ISO code and skips detection entirely. prompt takes a short piece of text that the model reads as though it were the audio immediately preceding your file, which is a strange idea until you use it: write the tricky words there with their correct spelling and the model carries that spelling through. Setting temperature=0 makes repeated runs of the same file agree with each other.

import os
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

GLOSSARY = "Whisper, pydub, ffmpeg, webhook, OAuth, Kubernetes"
HINT = f"A product demo recording. Spell these terms exactly: {GLOSSARY}."

with Path("interview-compressed.mp3").open("rb") as audio_file:
    transcript = client.audio.transcriptions.create(
        model="whisper-1",
        file=audio_file,
        language="en",
        prompt=HINT,
        temperature=0,
    )

Path("transcript.txt").write_text(transcript.text, encoding="utf-8")
print(transcript.text[:400])

Keep the prompt to a sentence or two. It is read as context, not as an instruction, so "please remove filler words" will be ignored while "Nunacode, Anastasia, MRR" will be honoured. Writing the hint with full punctuation is a useful trick as well: the model tends to mirror the style it was primed with, so a properly punctuated hint produces a properly punctuated transcript.

What changes when you pass a language code and a prompt A before-and-after matrix comparing transcripts produced without hints and with hints across three rows: proper nouns, language detection, and punctuation. No hints With hints Proper nouns names and brands Guessed by ear spelling drifts Taken from prompt same every run Language which one is spoken Auto-detected can pick wrongly Set by language no guessing step Punctuation commas and casing Long run-on lines harder to skim Sentence breaks mirrors the hint
Two extra arguments change the transcript in three visible ways, and all three of them save editing time later.

Response format quick reference

response_formatWhat you get backReach for it when
json (default)Object with .textYou want the words and nothing else
textA bare Python strYou are piping straight into a file
verbose_json.text, .language, .duration, .segmentsYou need timings or the detected language
srt / vttA ready subtitle file as a stringYou are captioning video and want no parsing

Troubleshooting

Maximum content size limit (26214400) exceeded — your upload is over the roughly 25 MB ceiling. Run the pydub compression from Step 1 and try again; if the compressed file is still too big, slice it into ten-minute chunks and transcribe them in a loop.

Invalid file format. Supported formats: ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm'] — the extension you passed is not on that list, or the extension lies about the actual contents. AudioSegment.from_file(source).export(target, format="mp3") normalises both problems in one step.

FileNotFoundError: [Errno 2] No such file or directory: 'ffprobe' — pydub imported fine but ffmpeg is missing from your PATH. Install it with your system package manager and restart the terminal so the new PATH is picked up; the Python side needs no change. A bare ModuleNotFoundError instead means the pip install landed in a different environment, which Fix ModuleNotFoundError: No Module Named openai walks through.

An empty transcript, or one phrase repeated for pages — the audio is silent or nearly silent, often because the recording used a channel your downmix cancelled out. Check the loudness before uploading: print(AudioSegment.from_file(source).dBFS) returns -inf for pure silence, and anything below about minus 40 is too quiet to read reliably. Amplify with audio + 10 (decibels) and re-export.

AuthenticationError on the very first call means the key never reached the client — usually a .env file in the wrong folder. Fix the 401 Unauthorized Error in OpenAI Python covers the diagnosis in order.

When to use this vs. alternatives

  • Versus running Whisper locally. The open-source model weights run on your own machine with no per-minute cost and no upload, which suits confidential recordings. You pay in setup: a large download, a GPU if you want real-time speeds, and no 25 MB shortcut to think about. The hosted API wins when you transcribe occasionally and value a two-line script.
  • Versus the auto-captions in YouTube or your meeting tool. Those are free and instant, but you cannot script them, cannot glossary them, and cannot get the text out in bulk. If your goal is a pipeline that runs over a folder every week, the API is the only one of the three you can automate. Budget it up front with Estimate OpenAI API Costs with Python.
  • Versus a human transcription service. People still beat any model on heavy accents, four-way crosstalk and specialist jargon. The realistic split is to let the API produce the draft in a minute and pay a human only for the passages that matter, such as pull quotes going into published copy.

With a transcript on disk, the interesting work starts. Feed it to a model to draft an article, as in Turn a Podcast Episode into a Blog Post, or condense a long recording into notes the way Summarize a YouTube Video with Python does. A one-hour transcript is a big lump of text, so if the model complains about length, Split Long Text into Chunks for AI APIs shows how to break it up without losing sentences at the seams. Back to AI Transcription and Content Repurposing with Python.

Frequently asked questions

What audio formats does the Whisper API accept?

The API accepts flac, m4a, mp3, mp4, mpeg, mpga, oga, ogg, wav and webm. Anything else has to be converted first, which pydub does in two lines once ffmpeg is installed. Converting to mono MP3 also shrinks the file, so it usually solves a size problem at the same time.

How large can an audio file be for one Whisper API request?

One request carries about 25 MB of audio. A long recording easily passes that, so downmix it to a single channel at a lower sample rate before uploading. If it is still too heavy, cut it into chunks with pydub, transcribe each chunk, and join the returned text in order.

Which response_format should I use for subtitles?

Ask for srt or vtt and the API returns a finished subtitle file as a plain string that you can write straight to disk. Choose verbose_json instead when you want the timings as Python data so you can merge, trim or re-time the segments yourself before writing the file.

Why does Whisper misspell names in my recording?

The model has no idea what your product, guests or company are called, so it writes what it hears. Pass those words in the prompt parameter as a short comma-separated list. The model treats them as context for the beginning of the audio and reuses that spelling for the rest of the transcript.

Do I need a GPU to transcribe audio with the Whisper API?

No. The model runs on OpenAI's servers, so your script only uploads a file and reads the reply. Any laptop that can run Python works. You need a GPU only if you decide to run the open-source Whisper weights locally instead of calling the hosted API.