Extract YouTube video transcripts and captions as text
Fetches a YouTube transcript via DeepAPI's server-side scraper, falling back to a local yt-dlp path when needed.
Why it matters
Retrieve clean, readable transcripts from YouTube videos for analysis, documentation, or content repurposing. The asset tries a server-side API first to avoid bot detection, then falls back to local extraction if needed.
Outcomes
What it gets done
Fetch transcript via DeepAPI with automatic retry and status polling
Fall back to yt-dlp when API key is missing or credits insufficient
Parse JSON3 caption files into clean plain text without duplicates
Save transcript with Channel_Title naming convention to working directory
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/ag-youtube-transcript | bash Overview
YouTube Transcript (via DeepAPI, yt-dlp fallback)
Fetches a YouTube transcript via DeepAPI's server-side scraper, avoiding the local-IP bot-flagging that plagues yt-dlp, and falls back to a local yt-dlp plus json3-flattening path when DeepAPI is unavailable. Use it whenever the user needs a saved YouTube transcript or captions. Fall back to yt-dlp only on missing key, insufficient credits, or repeated DeepAPI failure, and always disclose the fallback.
What it does
Fetches a YouTube video's transcript and saves it as a clean, raw .txt file. The primary path calls DeepAPI's transcript-scraping endpoint, which runs server-side and so avoids the local-IP bot-flagging problem that plagues local tools like yt-dlp; a fallback path runs yt-dlp locally when DeepAPI isn't available or fails.
When to use - and when NOT to
Use it when the user asks for a YouTube transcript, captions, subtitles, or spoken-content extraction, and DeepAPI or a local fallback can fetch it safely. Fall back to the local yt-dlp path only when the DeepAPI key is missing from the environment, when DeepAPI returns an insufficient-credits error (in which case the user should be told to top up first, and the fallback used only if credits genuinely aren't available), or when a DeepAPI request has failed twice in a row - and the user should always be told when a fallback happens, since a fallback means the primary path missed a real use case. If a 429 or a "sign in to confirm you're not a bot" style error shows up during the yt-dlp fallback, that means the local IP has been flagged, and the correct response is to stop rather than retry in a loop, since retrying makes the flag worse. Never fall back to downloading audio for a separate transcription model unless the user explicitly asks for that.
Inputs and outputs
Output always saves to the user's real project or working directory if one is in play, or to the Downloads folder otherwise, and the file is always named in a Channel_Title form with spaces replaced by underscores, falling back to the raw video ID if channel or title metadata isn't available. The DeepAPI key must already be present in the environment before starting - never read shell startup files or print secrets while checking for it. The scrape request itself:
IDK=$(uuidgen)
curl -s --max-time 120 "$BASE/v1/scrape/youtube/transcript" \
-H "Authorization: Bearer $DEEPAPI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDK" \
-d '{"url": "VIDEO_URL", "maxCostUsd": "0.05", "waitForFinishSecs": 60}' \
> /tmp/yt_transcript.json
must keep the same idempotency key across any retries of the same request. Non-English videos need an added language field in the request body. A running status means waiting the given delay and polling the returned next-step path until the job succeeds or fails. Once it succeeds, the transcript text is pulled out of the response and written to the output file, and the response also reports the exact cost of the run in micro-dollars alongside the transcript itself. The response's segment data separately carries per-segment start time and duration if the user wants timestamps rather than plain flowing text, and an empty transcript in the response means the video genuinely has no captions - that gets reported back to the user rather than retried.
Integrations
The local yt-dlp fallback pulls channel and title metadata first to build the same Channel_Title filename convention, falling back from the channel field to the uploader field to the uploader id if the channel is genuinely null, then downloads captions only, never the video itself, preferring manually-authored subtitles and falling back to auto-generated ones. It always requests the json3 subtitle format rather than VTT or SRT, since auto-generated VTT captions repeat every line twice in a rolling-caption style that would corrupt a clean transcript. A separate small Python step then flattens that json3 file into plain text by walking its timed caption segments, unescaping HTML entities, and collapsing whitespace into a clean single-spaced transcript file. On a yt-dlp failure for a non-English or unknown-language video, listing the video's available subtitle languages first and setting the language flag explicitly usually resolves it; a newer yt-dlp install may also need a separate JavaScript runtime available on the system path for YouTube's extraction to keep working; and on a first general failure, updating yt-dlp once and retrying once is the right move, after which it should stop rather than loop. Whichever path succeeds, the final report always states the saved file path, prints the transcript text directly if it's short enough, and additionally reports the dollar cost of the run whenever the DeepAPI path was used.
Who it's for
Anyone who needs a clean, saved transcript of a YouTube video's spoken content or captions, without hand-copying subtitles or hitting the bot-detection wall that a locally-run scraper often triggers.
Source README
YouTube Transcript (via DeepAPI, yt-dlp fallback)
When to Use
- Use when the user asks for a YouTube transcript, captions, subtitles, or spoken-content extraction.
- Use when DeepAPI or a local fallback can fetch the transcript safely.
Fetch a YouTube video's transcript and save a clean raw .txt file. Primary path is DeepAPI POST /v1/scrape/youtube/transcript. It runs server-side, so it avoids the local-IP bot flagging that plagues yt-dlp.
Save location
- If the user is in a real project/working dir → save there.
- Otherwise (no dir given, or cwd makes no sense) → save to
~/Downloads. - Always name the file
Channel_Titlewith spaces replaced by_(e.g.David_Ondrej_title_of_video.txt). If metadata is unavailable, fall back to the video ID.
Primary path - DeepAPI
DEEPAPI_API_KEY must already be present in the environment. Do not read shell
startup files or print secrets:
test -n "$DEEPAPI_API_KEY" || { echo "DEEPAPI_API_KEY is not set"; exit 1; }
BASE=${DEEPAPI_API_BASE_URL:-https://deepapi.co}
Run the scrape (keep the Idempotency-Key; retries must reuse the SAME one):
IDK=$(uuidgen)
curl -s --max-time 120 "$BASE/v1/scrape/youtube/transcript" \
-H "Authorization: Bearer $DEEPAPI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDK" \
-d '{"url": "VIDEO_URL", "maxCostUsd": "0.05", "waitForFinishSecs": 60}' \
> /tmp/yt_transcript.json
- Non-English videos: add
"language": "de"(etc.) to the body. status: running→ waitnext.afterSecs, thencurl "$BASE$(jq -r '.next.path' /tmp/yt_transcript.json)" -H "Authorization: Bearer $KEY"untilsucceededorfailed.
Extract the text and save it:
jq -r '.status' /tmp/yt_transcript.json # succeeded | running | failed
jq -r '.output[0].text' /tmp/yt_transcript.json > "$OUT/$NAME.txt"
jq -r '.debitMicrousd' /tmp/yt_transcript.json # cost (50000 = $0.05)
.output[0].segments also has timed segments (startSecs, durationSecs, text) if the user wants timestamps. Empty output = video has no captions; report it, don't retry.
For the Channel_Title filename, get metadata with a quick yt-dlp --print "%(channel)s|%(title)s" --skip-download "URL"; if that fails, use the video ID.
When to fall back to yt-dlp
DEEPAPI_API_KEYmissing from the environment.- HTTP 402
insufficient_credits(tell the user to top up at deepapi.co/credits first; fall back only if they're unavailable). - DeepAPI request
failedtwice.
Tell the user whenever you fall back - a fallback means the product missed a real use case.
Fallback path - yt-dlp (local)
OUT="$(pwd)" # or ~/Downloads if cwd makes no sense
META=$(yt-dlp --print "%(channel)s|%(title)s" --skip-download "URL")
NAME=$(echo "$META" | tr '| ' '__' | tr -cd '[:alnum:]_.-') # "Channel_Title", spaces -> _, strip unsafe chars
yt-dlp --skip-download --write-subs --write-auto-subs \
--sub-langs "en.*" --sub-format json3 \
-o "$OUT/$NAME.%(ext)s" "URL"
- Fall back
channel→uploader→uploader_idifchannelis null. --skip-download= captions only.--write-subs+--write-auto-subs= manual first, auto as fallback.- Always use
json3, never VTT/SRT - auto VTT repeats every line twice (rolling captions).
Flatten json3 → raw text:
python3 - "$OUT" <<'PY'
import json, html, re, glob, sys, pathlib
f = glob.glob(sys.argv[1] + "/*.json3")
if not f: sys.exit("no json3 file")
data = json.load(open(f[0], encoding="utf-8"))
parts = ["".join(s.get("utf8","") for s in e.get("segs") or []) for e in data.get("events", [])]
txt = re.sub(r"\s+", " ", html.unescape(" ".join(p.strip() for p in parts if p.strip()))).strip()
out = pathlib.Path(f[0]).with_suffix(".txt")
out.write_text(txt, encoding="utf-8"); print(out)
PY
yt-dlp failure handling
- Non-English / unknown language: run
yt-dlp --list-subs "URL"first, then set--sub-langs. - Newer yt-dlp may need
denoon PATH for YouTube extraction. - On first failure: run
yt-dlp -Uonce, retry once, then stop. - 429 / "Sign in to confirm you're not a bot" = IP flagged. STOP - do NOT retry in a loop (makes it worse).
- Never fall back to downloading audio for Whisper unless the user explicitly asks.
Output
Report the saved path; print the text if short. If DeepAPI was used, also report the cost in dollars.
Limitations
- Adapted from
davidondrej/skills; verify local paths, tools, credentials, and agent features before acting. - For commands, remote access, scheduling, browser automation, or file-changing workflows, get explicit user approval and confirm the target environment first.
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.