Skip to content

Instantly share code, notes, and snippets.

@ahmadrosid
Created August 13, 2026 18:58
Show Gist options
  • Select an option

  • Save ahmadrosid/155eb1e65167ab1afd6e196e2cd9d889 to your computer and use it in GitHub Desktop.

Select an option

Save ahmadrosid/155eb1e65167ab1afd6e196e2cd9d889 to your computer and use it in GitHub Desktop.
CLI: download Instagram Reels with yt-dlp and transcribe with Whisper
#!/usr/bin/env python3
"""Download Instagram Reels (or any yt-dlp URL) and print a Whisper transcript."""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
import whisper
def download_media(url: str, out_dir: Path, cookies_from_browser: str | None) -> Path:
out_tmpl = str(out_dir / "%(id)s.%(ext)s")
cmd = [
"yt-dlp",
"--no-playlist",
"-f",
"bv*+ba/b",
"--merge-output-format",
"mp4",
"-o",
out_tmpl,
"--print",
"after_move:filepath",
"--no-simulate",
]
if cookies_from_browser:
cmd.extend(["--cookies-from-browser", cookies_from_browser])
cmd.append(url)
result = subprocess.run(cmd, check=False, capture_output=True, text=True)
if result.returncode != 0:
err = (result.stderr or result.stdout or "yt-dlp failed").strip()
raise SystemExit(f"Download failed:\n{err}")
paths = [line.strip() for line in result.stdout.splitlines() if line.strip()]
if not paths:
# Fallback: newest media file in out_dir
candidates = sorted(
out_dir.glob("*"),
key=lambda p: p.stat().st_mtime,
reverse=True,
)
media = next(
(p for p in candidates if p.suffix.lower() in {".mp4", ".m4a", ".webm", ".mkv", ".wav", ".mp3"}),
None,
)
if media is None:
raise SystemExit("Download finished but no media file was found.")
return media
return Path(paths[-1])
def transcribe(
media_path: Path,
model_name: str,
language: str | None,
) -> dict:
model = whisper.load_model(model_name)
return model.transcribe(str(media_path), language=language, verbose=False)
def format_timestamp(seconds: float) -> str:
millis = int(round(seconds * 1000))
hours, rem = divmod(millis, 3_600_000)
minutes, rem = divmod(rem, 60_000)
secs, ms = divmod(rem, 1000)
return f"{hours:02d}:{minutes:02d}:{secs:02d},{ms:03d}"
def write_srt(segments: list[dict], path: Path) -> None:
lines: list[str] = []
for i, segment in enumerate(segments, start=1):
start = format_timestamp(float(segment["start"]))
end = format_timestamp(float(segment["end"]))
text = str(segment["text"]).strip()
lines.extend([str(i), f"{start} --> {end}", text, ""])
path.write_text("\n".join(lines), encoding="utf-8")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="reel-transcript",
description="Download a Reel/video with yt-dlp and transcribe it with Whisper.",
)
parser.add_argument("url", help="Instagram Reel URL (or any yt-dlp-supported URL)")
parser.add_argument(
"--model",
default="small",
choices=["tiny", "base", "small", "medium", "large", "turbo"],
help="Whisper model size (default: small)",
)
parser.add_argument(
"--language",
default="en",
help="Language code, or 'auto' to detect (default: en)",
)
parser.add_argument(
"--cookies-from-browser",
default="chrome",
metavar="BROWSER",
help="Browser for yt-dlp cookies (default: chrome). Use '' to disable.",
)
parser.add_argument(
"--keep-video",
type=Path,
metavar="PATH",
help="Save downloaded video to this path",
)
parser.add_argument(
"--srt",
type=Path,
metavar="PATH",
help="Also write an .srt subtitle file",
)
parser.add_argument(
"--txt",
type=Path,
metavar="PATH",
help="Also write a plain .txt transcript",
)
parser.add_argument(
"--timestamps",
action="store_true",
help="Print timed segments instead of plain text",
)
return parser
def main(argv: list[str] | None = None) -> int:
if shutil.which("yt-dlp") is None:
print("yt-dlp is required. Install with: brew install yt-dlp", file=sys.stderr)
return 1
if shutil.which("ffmpeg") is None:
print("ffmpeg is required. Install with: brew install ffmpeg", file=sys.stderr)
return 1
args = build_parser().parse_args(argv)
language = None if args.language == "auto" else args.language
cookies = args.cookies_from_browser or None
with tempfile.TemporaryDirectory(prefix="reel-transcript-") as tmp:
tmp_dir = Path(tmp)
print(f"Downloading…", file=sys.stderr)
media_path = download_media(args.url, tmp_dir, cookies)
print(f"Downloaded: {media_path.name}", file=sys.stderr)
if args.keep_video:
args.keep_video.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(media_path, args.keep_video)
print(f"Saved video: {args.keep_video}", file=sys.stderr)
print(f"Transcribing with Whisper `{args.model}`…", file=sys.stderr)
result = transcribe(media_path, args.model, language)
text = str(result["text"]).strip()
segments = result.get("segments") or []
if args.txt:
args.txt.parent.mkdir(parents=True, exist_ok=True)
args.txt.write_text(text + "\n", encoding="utf-8")
print(f"Wrote {args.txt}", file=sys.stderr)
if args.srt:
args.srt.parent.mkdir(parents=True, exist_ok=True)
write_srt(segments, args.srt)
print(f"Wrote {args.srt}", file=sys.stderr)
if args.timestamps:
for segment in segments:
start = format_timestamp(float(segment["start"]))
end = format_timestamp(float(segment["end"]))
print(f"[{start} --> {end}] {str(segment['text']).strip()}")
else:
print(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())
openai-whisper>=20240930
yt-dlp>=2025.1.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment