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.
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.
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.
Response format quick reference
response_format | What you get back | Reach for it when |
|---|---|---|
json (default) | Object with .text | You want the words and nothing else |
text | A bare Python str | You are piping straight into a file |
verbose_json | .text, .language, .duration, .segments | You need timings or the detected language |
srt / vtt | A ready subtitle file as a string | You 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.
Related guides
- Generate SRT Subtitles with Python and AI — turn the
verbose_jsonsegments from Step 3 into a caption file. - Turn a Podcast Episode into a Blog Post — the natural next script once
transcript.txtexists. - Summarize Sales Calls to Your CRM with Python — the same transcription step wired into a business workflow.
- Fix the 429 Rate-Limit Error in Python — what to do when a folder-wide loop hits the limit.
- Create a Python Virtual Environment for AI — the isolated setup pydub and the OpenAI SDK should live in.