Transcription skill
Transcribe a YouTube URL, a local video file, or a local audio file to plain
text using Google's gemini-3.5-flash on Replicate.
export REPLICATE_API_TOKEN=...
python3 <skill-directory>/scripts/transcribe.py <youtube-url | video-file | audio-file> [output-file] [--from TIME] [--to TIME]
Replace <skill-directory> with the directory containing this SKILL.md.
If the script exits saying yt-dlp or ffmpeg is missing, offer to install
it for the user (e.g. brew install yt-dlp or brew install ffmpeg on
macOS) before retrying, rather than just reporting the error.
Transcribing part of a video
When the user names a time range, pass it through with --from and --to
rather than transcribing the whole thing. Times can be SS, MM:SS, or
HH:MM:SS, and either flag can be used alone.
python3 <skill-directory>/scripts/transcribe.py "https://youtu.be/abc123" --from 15:08 --to 16:22
For a YouTube URL this passes --download-sections to yt-dlp, so only
the requested range crosses the network. On a 108-minute podcast, pulling
a 74-second clip moves about 23 MB instead of the full download, and the
prediction sees 74 seconds of audio instead of an hour and a half.
Clipped files carry the range in their names (e.g.
<slug>-15m08s-16m22s.m4a), so clips never overwrite a full-length
transcript of the same video. Trimming a local file needs ffmpeg even
when the input is already audio.
A range that starts past the end of the media is rejected before anything is downloaded or cut. A range that merely ends past it warns and transcribes up to the end.
What it does, in order
- If the input is a YouTube URL, downloads it with
yt-dlpinto the current directory under a slugified filename (lowercased title + video id, e.g.my-video-title-abc123.mp4), fetching only the--from/--torange if one was given. - If the input is a video file (or was just downloaded), extracts its
audio with
ffmpegvia stream copy (no re-encoding) and saves it alongside the video as<slug>.m4a. A local audio input with a time range is trimmed the same way. - Base64-encodes the audio and sends it to
google/gemini-3.5-flashon Replicate with a verbatim-transcription prompt. - Polls until the prediction completes and writes the transcript to
<slug>.txt(or the given output path).
An audio file input skips straight to step 3. google/gemini-3.5-flash is
hardcoded as the transcription model; see the root README.md for how
that choice was benchmarked against other models.
Known gotchas
ffmpeg seeking past the end of a file doesn't fail. With
-ssbeyond the media's duration and-acodec copy, ffmpeg exits 0 and writes the tail of the stream with negative timestamps (time=-02:59:00.00) instead of an empty file. Gemini then hallucinates a plausible sentence over that garbage. This is why the requested range is checked against the source duration up front rather than by inspecting the resulting clip.YouTube 403s.
yt-dlp's extractor breaks against YouTube frequently. If downloads fail withHTTP Error 403: Forbidden, runbrew upgrade yt-dlp(or update however it was installed) and retry.Gemini can't fetch Replicate's own file URLs. Uploading audio to Replicate's
/v1/filesand passing that URL as theaudioinput fails because Gemini's backend can't authenticate the fetch, and gets back a 401 JSON response it reports as "could not determine mimetype". Send the audio as a base64data:URI directly in the prediction input instead..m4amimetype sniffing. Python'smimetypes.guess_typereports.m4aasaudio/mp4a-latm, which Gemini rejects. Useaudio/mp4.api.replicate.comblocks requests with noUser-Agentheader (Cloudflare error 1010). Always set one.Pin to the model's current
latest_version. Don't hardcode a version id — Google ships new Gemini versions on Replicate often enough that hardcoded ids go stale.