Recorded meetings, classes, interviews, YouTube videos: a lot of knowledge gets stuck in audio.
While it stays there, you can't search it, quote it, summarize it or caption it.
In this tutorial, we'll turn audio and video into text with Python, in two ways:
- through the OpenAI API, with the
gpt-transcribemodel, when what you need is the text; - locally, with Whisper, when you want timed captions (
.srt) or can't send the file anywhere.
Along the way, we'll solve the two problems everyone runs into on the first try: the video is too big, and the recording is too long.
Two paths, two profiles
Before the code, it's worth knowing what each path gives you.
API (gpt-transcribe) | Local (faster-whisper) | |
|---|---|---|
| Where it runs | OpenAI's servers | Your machine |
| Cost | Per minute of audio | Free (you pay in CPU/GPU time) |
| Privacy | The file leaves your machine | Nothing leaves your machine |
| File size limit | 25 MB | None |
| Timestamps (for captions) | No, it returns only the text | Yes, segment by segment |
| Technical terms | Takes a list of keywords | Takes a context prompt |
The rule of thumb we'll use:
- Need the text to read, search or pass to an LLM? Start with the API.
- Need captions, have sensitive material or will transcribe many hours? Run it locally.
Both paths start in the same place: a good audio file.
From video to audio with ffmpeg
A one-hour video can easily go over 1 GB. Almost all of that is image, which does nothing for transcription.
ffmpeg extracts just the audio, already in the format speech models prefer:
ffmpeg -i meeting.mp4 -vn -ac 1 -ar 16000 -b:a 32k meeting.mp3What each option does:
-vndrops the video;-ac 1mixes the channels down to mono (speech doesn't need stereo);-ar 16000lowers the sample rate to 16 kHz, the same rate Whisper uses internally;-b:a 32kcompresses to 32 kbps, which is enough for speech.
With these options, one hour of audio comes to about 14 MB. In other words, recordings of up to an hour and a half already fit in the API's 25 MB limit without splitting anything.
Path 1: transcribing with the API
Install the SDK and set your key in the OPENAI_API_KEY environment variable:
pip install --upgrade openaiThe transcription itself is only a few lines:
from openai import OpenAI
client = OpenAI()
with open("meeting.mp3", "rb") as audio:
result = client.audio.transcriptions.create(
model="gpt-transcribe",
file=audio,
languages=["en"],
keywords=["LangGraph", "FastAPI", "Salada de Dados"],
)
print(result.text)Two parameters make a real difference:
languagestells the model which language(s) the audio is in, as ISO-639-1 codes (en,pt). It helps when a recording mixes languages, like a Portuguese meeting full of English terms.keywordsis the list of words the model should recognize: product names, acronyms, your field's jargon. It's the simplest way to avoid a "Lang Graf" in the middle of your text.
Notice the open(..., "rb"): the API expects the file opened in binary mode, not its path as a string.
Transcription models change fast.
whisper-1, which many tutorials still use, will be shut down in February 2027. Before going to production, check the current models in the speech-to-text documentation.
Long recordings: splitting without cutting words
Still over 25 MB after compression? Then you need to split the audio into chunks.
The naive way is to cut every X minutes. The problem is that the cut lands in the middle of a word, and the model gets the edges wrong.
The better way is to cut at the silences. ffmpeg can find them too, with the silencedetect filter:
import re
import subprocess
from pathlib import Path
def duration(path: str) -> float:
output = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path],
capture_output=True, text=True, check=True,
)
return float(output.stdout)
def silences(path: str) -> list[float]:
"""Returns the midpoint of each silence in the audio, in seconds."""
output = subprocess.run(
["ffmpeg", "-hide_banner", "-i", path, "-af", "silencedetect=noise=-35dB:d=0.5", "-f", "null", "-"],
capture_output=True, text=True,
)
pairs = re.findall(r"silence_end: ([\d.]+) \| silence_duration: ([\d.]+)", output.stderr)
return [float(end) - float(length) / 2 for end, length in pairs]
def cut_points(path: str, target: float = 20 * 60) -> list[float]:
"""Picks a cut every ~20 minutes, always at the closest silence before the target."""
total, midpoints = duration(path), silences(path)
points = [0.0]
# The 25% slack avoids a last chunk only a few seconds long.
while total - points[-1] > target * 1.25:
start = points[-1]
candidates = [s for s in midpoints if start + target / 2 < s <= start + target]
points.append(max(candidates) if candidates else start + target)
return points + [total]
def split_audio(path: str, folder: str = "chunks") -> list[Path]:
destination = Path(folder)
destination.mkdir(exist_ok=True)
points = cut_points(path)
files = []
for i, (start, end) in enumerate(zip(points, points[1:])):
file = destination / f"{i:03d}.mp3"
subprocess.run(
["ffmpeg", "-y", "-loglevel", "error", "-i", path,
"-ss", str(start), "-to", str(end), "-c", "copy", str(file)],
check=True,
)
files.append(file)
return filesThe logic in cut_points: at each step, it looks for the last silence between the middle and the end of the 20-minute window. If the recording has no pause in that range (rare, but it happens), it cuts at the target anyway.
To transcribe, we send the chunks in order. The detail that matters is the prompt parameter: we pass the end of the previous text, so the model keeps the context, the spelling of names and the punctuation style from one chunk to the next.
from openai import OpenAI
from split_audio import split_audio
client = OpenAI()
def transcribe_long(path: str) -> str:
texts: list[str] = []
for chunk in split_audio(path):
extra = {"prompt": texts[-1][-500:]} if texts else {}
with open(chunk, "rb") as audio:
result = client.audio.transcriptions.create(
model="gpt-transcribe",
file=audio,
languages=["en"],
**extra,
)
texts.append(result.text)
return "\n\n".join(texts)
print(transcribe_long("full-class.mp3"))Path 2: captions with local Whisper
To caption a video, we need to know when each sentence is said. This is where the local path shines.
faster-whisper is a reimplementation of Whisper that is much faster and lighter than the original, and it runs well even on a CPU:
pip install faster-whisperIt returns the transcription as segments, each with a start, an end and the text. That's practically an .srt file already:
from faster_whisper import WhisperModel
def srt_time(seconds: float) -> str:
ms = round(seconds * 1000)
h, ms = divmod(ms, 3_600_000)
m, ms = divmod(ms, 60_000)
s, ms = divmod(ms, 1000)
return f"{h:02}:{m:02}:{s:02},{ms:03}"
model = WhisperModel("turbo", device="cpu", compute_type="int8")
segments, info = model.transcribe("meeting.mp4", language="en", vad_filter=True)
with open("meeting.srt", "w", encoding="utf-8") as srt:
for i, segment in enumerate(segments, start=1):
srt.write(f"{i}\n{srt_time(segment.start)} --> {srt_time(segment.end)}\n{segment.text.strip()}\n\n")A few notes:
- It takes the video directly.
faster-whisperextracts the audio on its own, so the ffmpeg step is optional here. - There's no size limit, and nothing to split: it processes the whole recording, segment by segment.
vad_filter=Trueskips the silences. That speeds up transcription and avoids a known Whisper flaw: "making up" sentences in stretches with no speech.segmentsis a generator: transcription happens while the loop runs, not on thetranscribeline.
About the model:
"turbo"is a good balance between quality and speed.- On a more modest machine,
"small"is much faster, with some loss in quality. - With an NVIDIA GPU, switch to
device="cuda", compute_type="float16", and transcription gets many times faster.
The generated .srt can be uploaded as captions in YouTube Studio, and most video players can read it too.
Which one should you choose?
When in doubt, go by what you need at the end:
- Text to read, search or summarize: the API. It's the path with the least friction, and
keywordshandles technical terms well. - Captions: local. The current API doesn't return timing.
- Sensitive audio (clients, health, legal): local. Nothing leaves your machine.
- Large, recurring volume: do the math. Compare the API's per-minute price with the cost of keeping a GPU machine running Whisper.
What comes after the transcription?
A transcript is the beginning, not the end.
With it in hand, an LLM can write a meeting summary, the chapters of a YouTube video or a list of decisions and next steps. That's the topic of an upcoming article.
