Back to the blog
Engineering

Transcribing Audio and Video with Python: From File to Captions

Transcribe recordings with the OpenAI API or local Whisper, split long audio at the silences and generate .srt captions.

PythonOpenAIWhisper

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-transcribe model, 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 runsOpenAI's serversYour machine
CostPer minute of audioFree (you pay in CPU/GPU time)
PrivacyThe file leaves your machineNothing leaves your machine
File size limit25 MBNone
Timestamps (for captions)No, it returns only the textYes, segment by segment
Technical termsTakes a list of keywordsTakes 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.mp3

What each option does:

  • -vn drops the video;
  • -ac 1 mixes the channels down to mono (speech doesn't need stereo);
  • -ar 16000 lowers the sample rate to 16 kHz, the same rate Whisper uses internally;
  • -b:a 32k compresses 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 openai

The transcription itself is only a few lines:

transcribe_api.py
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:

  • languages tells 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.
  • keywords is 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:

split_audio.py
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 files

The 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.

transcribe_long.py
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-whisper

It returns the transcription as segments, each with a start, an end and the text. That's practically an .srt file already:

caption.py
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-whisper extracts 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=True skips the silences. That speeds up transcription and avoids a known Whisper flaw: "making up" sentences in stretches with no speech.
  • segments is a generator: transcription happens while the loop runs, not on the transcribe line.

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 keywords handles 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.