Batch Transcribe an Entire YouTube Playlist to Markdown
For one or two videos, paste-and-convert is fine. For 50, 100, or an entire YouTube channel's back catalog, you need a script. The MDisBetter web tool accepts several video URLs at once, one per line, but it has no API or CLI and can't run unattended: that's the right surface for ad-hoc use, not for batch. For batch, the right answer is open-source: yt-dlp to fetch, faster-whisper to transcribe, a Python loop to glue them together. Total cost: $0 plus your hardware time. Here is the working pipeline, what to expect from GPU vs CPU, and a Markdown output structure you can adapt.
Be honest about the web tool's limits
MDisBetter's video to Markdown accepts several video URLs at once, one per line, and returns a Markdown transcript for each. It doesn't have an API or CLI, doesn't accept playlist URLs, and each video is a separate conversion that uses credits. For a handful of videos, it's the simplest path. For 50+, it's the wrong tool, and pretending otherwise wastes your time.
The OSS path scales to thousands of videos with no per-video cost. It does require Python, basic command-line comfort, and ideally a GPU for reasonable speed. If those are dealbreakers, the right move is to spread the work across the web tool over several sessions, pasting a few URLs at a time (one per line), rather than expect a browser tool to run unattended like a script.
The stack
- yt-dlp: the modern, actively-maintained successor to youtube-dl. Downloads any YouTube video or playlist, extracts audio.
- faster-whisper: CTranslate2 reimplementation of OpenAI's Whisper. 3-4x faster than the reference whisper package with the same accuracy.
- Python 3.10+: for the loop and Markdown formatting
All MIT/Apache licensed. All free. All run locally.
Setup
# Install
pip install yt-dlp faster-whisper
# Verify yt-dlp
yt-dlp --version
# Test on one video
yt-dlp -x --audio-format mp3 -o 'test.%(ext)s' \
'https://www.youtube.com/watch?v=dQw4w9WgXcQ'For GPU acceleration with NVIDIA cards, also install CUDA toolkit and the right cuDNN version (faster-whisper docs have the matrix). For Apple Silicon, set device="auto" and faster-whisper will use Metal. CPU-only works on any platform without extra setup.
The full pipeline
#!/usr/bin/env python3
"""Batch transcribe a YouTube playlist to structured Markdown files."""
import argparse
import json
import subprocess
import sys
from pathlib import Path
from faster_whisper import WhisperModel
def get_playlist_videos(playlist_url):
"""Return list of (video_id, title) tuples for the playlist."""
cmd = ['yt-dlp', '--flat-playlist', '-J', playlist_url]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
data = json.loads(result.stdout)
return [(e['id'], e['title']) for e in data.get('entries', [])]
def download_audio(video_id, audio_dir):
"""Download just the audio for a single video. Returns the file path."""
output = audio_dir / f'{video_id}.mp3'
if output.exists():
return output
cmd = [
'yt-dlp', '-x', '--audio-format', 'mp3',
'-o', str(output.with_suffix('.%(ext)s')),
f'https://www.youtube.com/watch?v={video_id}',
]
subprocess.run(cmd, check=True)
return output
def transcribe(audio_path, model):
"""Transcribe an audio file. Returns list of (start, end, text) segments."""
segments, info = model.transcribe(
str(audio_path),
beam_size=5,
language=None, # auto-detect
)
return [(s.start, s.end, s.text.strip()) for s in segments]
def segments_to_markdown(title, video_id, segments):
"""Format segments as structured Markdown with H2 sections every ~5 minutes."""
lines = [f'# {title}', '']
lines.append(f'**Source:** https://www.youtube.com/watch?v={video_id}')
if segments:
duration = segments[-1][1]
m, s = divmod(int(duration), 60)
h, m = divmod(m, 60)
if h:
lines.append(f'**Duration:** {h}:{m:02d}:{s:02d}')
else:
lines.append(f'**Duration:** {m}:{s:02d}')
lines.append('')
# Group segments into sections every 5 minutes
section_seconds = 300
current_section = -1
for start, end, text in segments:
section = int(start // section_seconds)
if section != current_section:
current_section = section
mm = int(start // 60)
ss = int(start % 60)
lines.append('')
lines.append(f'## [{mm:02d}:{ss:02d}] Section {section + 1}')
lines.append('')
lines.append(text)
return '\n'.join(lines)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('playlist_url')
parser.add_argument('--out', default='transcripts')
parser.add_argument('--model', default='large-v3',
help='tiny | base | small | medium | large-v3')
parser.add_argument('--device', default='auto',
help='auto | cpu | cuda')
args = parser.parse_args()
out_dir = Path(args.out)
out_dir.mkdir(exist_ok=True)
audio_dir = out_dir / '_audio'
audio_dir.mkdir(exist_ok=True)
md_dir = out_dir / 'markdown'
md_dir.mkdir(exist_ok=True)
print(f'Loading model: {args.model} on {args.device}...')
compute = 'float16' if args.device == 'cuda' else 'int8'
model = WhisperModel(args.model, device=args.device, compute_type=compute)
videos = get_playlist_videos(args.playlist_url)
print(f'Playlist has {len(videos)} videos')
for i, (vid, title) in enumerate(videos, 1):
out_md = md_dir / f'{vid}.md'
if out_md.exists():
print(f'[{i}/{len(videos)}] SKIP {title} (already done)')
continue
print(f'[{i}/{len(videos)}] {title}')
try:
audio = download_audio(vid, audio_dir)
segments = transcribe(audio, model)
md = segments_to_markdown(title, vid, segments)
out_md.write_text(md, encoding='utf-8')
print(f' -> {out_md}')
except Exception as e:
print(f' FAIL: {e}', file=sys.stderr)
if __name__ == '__main__':
main()Save as batch_transcribe.py. Run:
python batch_transcribe.py 'https://www.youtube.com/playlist?list=PLxxxxx' \
--out my-channel-transcripts --model large-v3 --device cudaWhat this produces
For a 50-video playlist, you get a folder structure like:
my-channel-transcripts/
├── _audio/ # cached MP3s (delete after if you want)
├── markdown/
│ ├── dQw4w9WgXcQ.md
│ ├── jNQXAC9IVRw.md
│ ├── ...
Each .md file has an H1 title, a source link, the duration, then H2 sections every 5 minutes with timestamps, followed by the transcript prose. Drop the folder into Obsidian, Notion, or any Markdown system.
What to expect from your hardware
Transcription speed varies wildly with hardware and model size. A recent NVIDIA GPU running large-v3 is far faster than a CPU; on a CPU-only machine, large-v3 is slow enough that the medium model is the practical choice. Before you queue a whole playlist, time the script on one or two videos on your own machine and multiply by the length of the playlist: that tells you whether the job fits in an afternoon or needs to run overnight.
Speed/accuracy tradeoffs by model
| Model | Size (parameters) |
|---|---|
| tiny | 39M |
| base | 74M |
| small | 244M |
| medium | 769M |
| large-v3 | 1550M |
Smaller models run faster and make more mistakes; larger models are slower and more accurate. If accuracy matters, check a few minutes of output from two model sizes against a hand-corrected excerpt before you commit to one for the whole batch.
For batch, our recommendation: large-v3 if you have a GPU (the accuracy gain is worth it for permanent archives), medium if you're CPU-only (good enough for most uses, finishes in reasonable time).
Adding speaker labels
The faster-whisper output above is text-only, no speaker diarization. To add speaker labels, layer on WhisperX:
pip install whisperx pyannote.audioimport whisperx
model = whisperx.load_model('large-v3', device='cuda', compute_type='float16')
audio = whisperx.load_audio('audio.mp3')
result = model.transcribe(audio, batch_size=16)
# Align
model_a, metadata = whisperx.load_align_model(
language_code=result['language'], device='cuda')
result = whisperx.align(result['segments'], model_a, metadata, audio, 'cuda')
# Diarize
diarize_model = whisperx.DiarizationPipeline(
use_auth_token='YOUR_HF_TOKEN', device='cuda')
diarize_segments = diarize_model(audio)
result = whisperx.assign_word_speakers(diarize_segments, result)WhisperX needs a free HuggingFace token for the pyannote diarization models. Setup is fiddlier, but you get multi-speaker transcripts with speaker labels.
Batch tips
Use a manifest file for resumability
The script above checks for existing .md files and skips them, which gives you resumability for free. If transcription crashes after video 37 of 50, re-running the script picks up at video 38.
Delete audio cache after transcription
The _audio/ folder gets large (MP3 ~1MB per minute of audio). Once transcripts are done and you've verified them, rm -rf _audio/ reclaims the space.
Parallel processing with care
You can speed up the loop by transcribing N files in parallel, but you'll quickly saturate the GPU. For most setups, single-threaded transcription is already GPU-bound; parallel just thrashes. Where parallel helps: download (yt-dlp) running in parallel with transcription on the previous video. Easiest implementation: producer-consumer with Python's concurrent.futures.
Respect YouTube's rate limits
yt-dlp can hit rate limits if you hammer the API. Default delays are usually fine for casual playlist downloads. For large channels (500+ videos), spread across multiple days or use yt-dlp's --sleep-interval flag.
What about the language?
The script auto-detects language per video via Whisper's built-in detector. For multi-language playlists this works correctly. If you know all videos are in one language, force it with language='en' in the transcribe call, which skips the detection step.
Comparison: web tool vs OSS batch
| Aspect | MDisBetter web tool | OSS batch |
|---|---|---|
| Setup time | Create a free account, then paste a URL | 30-60 min first time |
| Per-video effort (manual) | Paste the URLs (one per line), convert, download each result | 0 (script runs unattended) |
| Speaker labels | No | Optional via WhisperX |
| Markdown output | Built in (title, language, transcript) | You write the formatter |
| Cost | Free account (credits per conversion) or paid plan | Free (your hardware) |
| Privacy | Server-side processing | Fully local |
| Best for | Few videos, no setup | 50+ videos, automation |
Recommendation
Under 10 videos: use the web tool, manual paste-and-convert. 10-50 videos: split into multiple sessions with the web tool, or take a couple hours to set up the OSS pipeline (the script becomes reusable forever after). 50+ videos or recurring batch needs: the OSS pipeline pays back in the first run. For knowledge-system integration after batch transcription, see Obsidian video vault setup; for individual workflows see 5 methods compared and how to benchmark YouTube transcript tools; and for non-YouTube batch (uploaded MP4s, Zoom recordings), the same script works by skipping the yt-dlp step. Cross-reference: the same OSS approach applies to a folder of audio files by stripping the download step; for a handful of audio files, our audio to Markdown converter accepts several at once in the browser.