Given a YouTube video URL or ID, return title, channel, duration, full timestamped transcript segments, and whether captions are auto-generated or human-authored. Read-only.
Automate repetitive workflows and reduce manual effort
Example: Generate reports, summarize documents, draft communications
Save 3-5 hours per week on routine tasks
Learn new skills, understand complex topics, get expert guidance
Example: Explain concepts, provide examples, suggest learning resources
Accelerate learning and skill development by 2x
Enhance output quality through reviews, suggestions, and refinements
Example: Review drafts, suggest improvements, catch errors
Improve work quality by 30-40% with less effort
Time estimate: 15-45 minutes depending on use case complexity
Prerequisites
Submit your Claude Code skill and start earning
browse install youtube.com/extract-transcript-loeudeCompatible with
Supported agents
| name | extract-transcript |
| title | YouTube Video Transcript Extraction |
| description | >- Given a YouTube video URL or ID, return title, channel, duration, full timestamped transcript segments, and whether captions are auto-generated or human-authored. Read-only. |
| website | youtube.com |
| category | video |
| tags | - youtube - transcript - captions - video - read-only - innertube |
| source | 'browserbase: agent-runtime 2026-05-18' |
| updated | '2026-05-18' |
| recommended_method | api |
| alternative_methods | - method: api rationale: >- The InnerTube /youtubei/v1/player POST endpoint with the ANDROID client returns the same captionTracks[] data as the JS player, requires no API key as of late 2024, succeeds from datacenter IPs without proxies, and avoids the 1 MB+ watch-page HTML payload entirely. ~2 HTTP calls, sub-second wall. - method: browser rationale: >- Fallback only when InnerTube returns LOGIN_REQUIRED / 403 sporadic bot-detection. Drive a Browserbase session with --proxies --verified, open /watch, read window.ytInitialPlayerResponse — same shape as the InnerTube response. ~10x more expensive and slower; reserve for the ~5% of videos where the API path fails. - method: url-param rationale: >- https://www.youtube.com/oembed?url=... is the cheapest way to get title + channel (verified working, ~450 byte response, no auth) and a fast existence check before committing to the heavier InnerTube call. Insufficient on its own — does not return transcript or duration. |
| verified | false |
| proxies | false |
Given a YouTube video URL or video ID, return the video's title, channel/uploader name, duration in seconds, the full transcript as timestamped segments, and a flag indicating whether the captions are auto-generated (asr) or human-authored. Read-only — never likes, comments, subscribes, or watches.
YouTube's web UI is a thin client over the public InnerTube API at https://www.youtube.com/youtubei/v1/. The transcript task needs two API calls (one optional) and zero browser pixels for ~95% of videos — only fall back to a browser session when InnerTube returns a LOGIN_REQUIRED / AGE_VERIFICATION_REQUIRED playability status and the caller wants to attempt the consent flow.
Accept any of:
https://www.youtube.com/watch?v=<ID> (canonical)https://youtu.be/<ID>https://www.youtube.com/shorts/<ID>https://www.youtube.com/embed/<ID>https://m.youtube.com/watch?v=<ID>[A-Za-z0-9_-]{11})Strip query params other than v= and any list/playlist context. The video ID is always exactly 11 characters; reject anything else early.
GET https://www.youtube.com/oembed?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D<ID>&format=json
Returns JSON with title, author_name (channel), author_url, and thumbnail_url. No auth, no key. ~450 bytes. Use this for the metadata even if you later succeed at the InnerTube call — it's a sanity check that the video actually exists publicly:
success: false, reason: "video_unavailable" and stop.videoDetails.title / author from the InnerTube response./player for caption track URLs + durationPOST https://www.youtube.com/youtubei/v1/player?prettyPrint=false
Content-Type: application/json
Origin: https://www.youtube.com
{
"context": {
"client": {
"clientName": "ANDROID",
"clientVersion": "19.09.37",
"androidSdkVersion": 30,
"hl": "en",
"gl": "US",
"userAgent": "com.google.android.youtube/19.09.37 (Linux; U; Android 14) gzip"
}
},
"videoId": "<ID>"
}
Why the ANDROID client over WEB?
| Client | Needs API key? | Needs visitorData / PoToken? | Returns captionTracks? | Notes |
|---|---|---|---|---|
WEB | yes (INNERTUBE_API_KEY, harvested from the embed page) | increasingly yes — Google rolled out bot-detection tokens through 2024-2025 | yes | The "official" path the JS player uses. Brittle when Google rotates the key or adds a new gate. |
ANDROID | no (no key= query param required as of mid-2025) | no | yes | The mobile InnerTube client has the loosest validation. Fastest known path. |
IOS | no | no | yes | Equivalent fallback if ANDROID starts requiring extra fields. |
WEB_EMBEDDED_PLAYER | yes | yes | sometimes — returns EMBEDDER_IDENTITY_MISSING_REFERRER when the request lacks a valid Referer, in which case captions is absent | Useful only when the watch endpoint is region-locked. |
If ANDROID returns playabilityStatus.status !== "OK", retry once with IOS (same body, just swap clientName/clientVersion to "IOS" / "19.09.3"). If both fail with the same reason, that's the honest answer.
Parse the response:
{
playabilityStatus: { status: "OK" | "ERROR" | "LOGIN_REQUIRED" | "UNPLAYABLE" | "LIVE_STREAM_OFFLINE", reason?: "..." },
videoDetails: {
videoId: "dQw4w9WgXcQ",
title: "...",
author: "Rick Astley", // channel name
lengthSeconds: "213", // STRING, not number — coerce
isLiveContent: false,
channelId: "UCuAXFkgsw1L7xaCfnd5JJOw",
shortDescription: "..."
},
captions: {
playerCaptionsTracklistRenderer: {
captionTracks: [
{
baseUrl: "https://www.youtube.com/api/timedtext?v=...&caps=asr&...&signature=...",
name: { simpleText: "English" } | { runs: [{ text: "English" }] },
vssId: ".en" | "a.en", // a. prefix = auto-generated
languageCode: "en",
kind: "asr", // present iff auto-generated; absent for human-authored
isTranslatable: true,
trackName: ""
},
...
],
audioTracks: [...],
translationLanguages: [...]
}
} | undefined // entire field is absent when captions are disabled
}
Outcome branches at this point:
playabilityStatus.status === "OK" and captions.playerCaptionsTracklistRenderer.captionTracks non-empty → continue to step 4.playabilityStatus.status === "OK" but no captions field, or empty captionTracks → success: false, reason: "captions_disabled". Still return title/channel/duration.playabilityStatus.status === "LIVE_STREAM_OFFLINE" or videoDetails.isLiveContent === true with no captions → success: false, reason: "live_stream_no_transcript".playabilityStatus.status === "LOGIN_REQUIRED" → success: false, reason: "age_restricted". Optional browser fallback (step 6).playabilityStatus.status === "UNPLAYABLE" (region block, copyright takedown) → success: false, reason: "video_unavailable", copy playabilityStatus.reason verbatim into the error payload.playabilityStatus.status === "ERROR" → success: false, reason: "video_unavailable".Default policy:
kind === "asr".languageCode starts with the preferred language prefix (en-US matches en).language_fallback: true in the output.For "I just want a transcript, any language":
kind is absent).asr track.The kind === "asr" flag IS the authoritative auto_generated signal. The vssId prefix (a. vs .) is a redundant secondary signal — agree-with-kind checks are a useful invariant in tests but not needed at runtime.
The baseUrl is already-signed and returns XML by default. Always append &fmt=json3 for a structured response:
GET <baseUrl>&fmt=json3
Returns:
{
"wireMagic": "pb3",
"pens": [...],
"wsWinStyles": [...],
"wpWinPositions": [...],
"events": [
{
"tStartMs": 18800,
"dDurationMs": 4040,
"segs": [
{ "utf8": "We're no strangers to love" }
]
},
{
"tStartMs": 23900,
"dDurationMs": 3000,
"segs": [
{ "utf8": "You know the rules" },
{ "utf8": " and so do I" } // multiple segs in one event = inline timing inside the line
]
}
]
}
Normalize each event to one segment:
start_seconds = event.tStartMs / 1000duration_seconds = event.dDurationMs / 1000text = event.segs.map(s => s.utf8 ?? "").join("").trim()text is empty (these are pure styling / continuation markers).segs is missing entirely (these are aAppend: 1 continuation events on auto-generated tracks; their text was already emitted on the previous event).For the auto_generated boolean in your output, use kind === "asr". Do NOT infer from the presence of multiple segs per event — both manual and ASR tracks can have multi-seg events.
To translate on-the-fly to a different language, append &tlang=<code> to the baseUrl (Google's machine translation). The response shape is identical; mark the result as translated: true, source_language: <original>.
If both ANDROID and IOS InnerTube calls fail with a non-OK playabilityStatus, OR if Google has temporarily blocked datacenter IPs from the InnerTube endpoint (observed sporadically — 403 with empty body), drive a real browser:
SID=$(browse cloud sessions create --keep-alive --proxies --verified | jq -r '.id')
export BROWSE_SESSION="$SID"
browse open "https://www.youtube.com/watch?v=<ID>" --remote
browse wait load --remote
browse wait timeout 3000 --remote # let the player chrome render
PLAYER_RESPONSE=$(browse eval --remote 'JSON.stringify(window.ytInitialPlayerResponse || null)')
ytInitialPlayerResponse has the exact same shape as the InnerTube /player POST response — so the parsing in steps 3–5 is unchanged. The captionTracks baseUrl you read from the watch page is signed for that browser session, so fetch it via browse eval's fetch() (same-origin) rather than from your own runtime:
TRACK_URL=$(node -e "console.log(JSON.parse(process.argv[1]).captions.playerCaptionsTracklistRenderer.captionTracks[0].baseUrl + '&fmt=json3')" "$PLAYER_RESPONSE")
browse eval --remote "await fetch('${TRACK_URL}').then(r => r.text())"
browse cloud sessions update "$SID" --status REQUEST_RELEASE
A residential-proxy session (--proxies --verified) is recommended for the browser fallback because YouTube's bot detection is more aggressive on the consent / /watch HTML path than on the InnerTube API — but the API path itself in step 3 routinely succeeds from datacenter IPs with no proxy. Don't pay for --proxies until you actually need it.
lengthSeconds is a string, not a number — JSON-parse coerces correctly but a naïve videoDetails.lengthSeconds + 1 will concatenate. Cast.captions field is entirely absent, not null, when the uploader has disabled captions. Distinguish "captions" in player vs captions.playerCaptionsTracklistRenderer.captionTracks.length === 0 — both indicate "no transcript", but the former is the uploader's choice and the latter is occasionally a transient API state. Retry once on the empty-array case before declaring captions_disabled.kind === "asr" is the canonical signal. vssId starting with a. is a redundant cross-check. Don't try to infer auto-generated from text quality / lowercasing / no-punctuation — modern ASR adds capitalization and punctuation; that heuristic is dead.key= query parameter is no longer required for the ANDROID and IOS clients as of late 2024 — those clients are validated by User-Agent + clientVersion instead. The WEB client still requires the key, which you harvest from https://www.youtube.com/embed/<id> HTML ("INNERTUBE_API_KEY":"..." — verified live as AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8 on 2026-05-18, rotates ~quarterly). Don't hardcode the key.captionTracks[].baseUrl is signed and time-limited. The signature embedded in the URL expires after ~6 hours. Fetch the track within minutes of getting the player response; don't store baseUrls in a long-lived cache.&fmt=json3 is mandatory for machine consumption. Default response is xml (TTML-style) with HTML entities, font tags, and inline <br> — much harder to parse cleanly than json3's events[].segs[].utf8.https://www.youtube.com/api/timedtext?v=<id>&lang=<code> GETs return HTTP 200 with empty body when no signature is supplied. Don't be fooled by the 200 — the body length is 0. Verified 2026-05-18: /api/timedtext?v=dQw4w9WgXcQ&lang=en&fmt=json3 → 200 OK Content-Length: 0. The signed baseUrl from the player response is the only working entry point.type=list on the timedtext endpoint is deprecated and also returns 200 + empty body. Use the InnerTube /player response's captionTracks array instead.playabilityStatus === "OK". Check videoDetails.isLiveContent and videoDetails.isLive; if either is true and captions is missing, report live_stream_no_transcript rather than captions_disabled. Once a live stream ends and is post-processed (typically within an hour), captions may appear./shorts/<id>) is just a regular video with portrait aspect ratio. The same InnerTube call works; the only difference is lengthSeconds is usually ≤60.embedded_player_response inside the embed page does NOT contain caption tracks when fetched without a valid Referer. The embed HTML returns previewPlayabilityStatus.errorCode: "PLAYABILITY_ERROR_CODE_EMBEDDER_IDENTITY_MISSING_REFERRER" and the captions field is absent. This is a common dead-end. Always use the InnerTube POST instead. (Confirmed 2026-05-18 against https://www.youtube.com/embed/dQw4w9WgXcQ — 128 KB HTML, INNERTUBE_API_KEY and clientVersion present, but no captionTracks anywhere in the document.)https://www.youtube.com/watch?v=<id> consistently returns > 1 MB of HTML (verified — exceeded the Browserbase Fetch 1 MB cap on www.youtube.com, m.youtube.com, music.youtube.com, /shorts/, and /watch_videos?video_ids=... variants on 2026-05-18). Don't try to fetch and regex it from a lightweight fetch endpoint; either use the InnerTube POST or open it in a real browser session and read window.ytInitialPlayerResponse.Origin: https://www.youtube.com header on the InnerTube POST is recommended even from the ANDROID client — it appeases the upstream WAF on rare 429-rate-limited paths. The User-Agent should match the clientVersion: com.google.android.youtube/<version> (Linux; U; Android 14) gzip.UNPLAYABLE with reason: "Video unavailable in your country". The ANDROID client doesn't bypass these any more than the WEB client does — both honor geofencing. Use a residential proxy in the relevant region if you need to access region-locked content.LOGIN_REQUIRED on cookieless InnerTube. There's no clean public bypass; the legacy EMBEDDED_PLAYER cipher trick stopped working in 2023. Report age_restricted and move on, or fall back to a logged-in browser session if the caller has cookies.captions.playerCaptionsTracklistRenderer.audioTracks populated but captionTracks: [] — these are videos with multi-language audio dubs but no subtitle tracks. Treat as captions_disabled.&tlang= are machine-translated by Google. They're not separate tracks in captionTracks; they're a per-baseUrl query parameter. Available target languages are listed in captions.playerCaptionsTracklistRenderer.translationLanguages[].segs[] per event on auto-generated tracks represent word-level timing for highlighting; concatenate them to get the line text. On human-authored tracks, multi-segs usually represents inline formatting (italics, color). Either way, concatenate utf8 fields and you get the human-readable line.segs events with aAppend: 1 are continuation markers for the previous event's last segment (used to extend the highlight window). Skip them — their text was already emitted.Six distinct outcome shapes. Always include the video_id and any metadata you successfully resolved, even on failure.
// (A) Success — human-authored captions
{
"success": true,
"video_id": "dQw4w9WgXcQ",
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
"channel": "Rick Astley",
"channel_url": "https://www.youtube.com/@RickAstleyYT",
"duration_seconds": 213,
"is_live": false,
"captions": {
"language": "en",
"language_name": "English",
"auto_generated": false,
"translated": false,
"segment_count": 56,
"segments": [
{ "start_seconds": 18.80, "duration_seconds": 4.04, "text": "We're no strangers to love" },
{ "start_seconds": 23.84, "duration_seconds": 3.00, "text": "You know the rules and so do I" }
]
},
"available_languages": [
{ "language_code": "en", "name": "English", "auto_generated": false },
{ "language_code": "es", "name": "Spanish (auto-generated)", "auto_generated": true }
],
"error_reasoning": null
}
// (B) Success — auto-generated only
{
"success": true,
"video_id": "...",
"title": "...", "channel": "...", "duration_seconds": 720,
"captions": { "language": "en", "auto_generated": true, "segment_count": 187, "segments": [...] },
"error_reasoning": null
}
// (C) Captions disabled by uploader
{
"success": false,
"video_id": "...", "title": "...", "channel": "...", "duration_seconds": 600,
"captions": null,
"error_reasoning": "captions_disabled"
}
// (D) Live stream — no transcript yet
{
"success": false,
"video_id": "...", "title": "...", "channel": "...", "duration_seconds": 0, "is_live": true,
"captions": null,
"error_reasoning": "live_stream_no_transcript"
}
// (E) Age-restricted / login-required
{
"success": false,
"video_id": "...", "title": null, "channel": null, "duration_seconds": null,
"captions": null,
"error_reasoning": "age_restricted",
"playability_status": "LOGIN_REQUIRED"
}
// (F) Video unavailable (private, deleted, region-blocked, copyright takedown)
{
"success": false,
"video_id": "...", "title": null, "channel": null, "duration_seconds": null,
"captions": null,
"error_reasoning": "video_unavailable",
"playability_status": "UNPLAYABLE",
"playability_reason_verbatim": "Video unavailable in your country"
}
AI-first code editor with Composer
Before installing skills in Cursor, ensure your development environment meets these requirements:
node --versionextract-transcriptExecute the skills CLI command in your project's root directory to begin installation:
browse install youtube.com/extract-transcript-loeudeFetches extract-transcript from youtube.com/extract-transcript-loeude and configures it for Cursor.
The CLI shows a list of agents. Use arrow keys and space to select Cursor:
Confirm successful installation by checking the skill directory location:
.cursor/skills/extract-transcript
Restart Cursor to activate extract-transcript. Access via /extract-transcriptin your agent's command palette.
Learn more:View source on GitHubSkills CLI docsAbout CursorWhat are agent skills?
Steps
Common Pitfalls
✓ Do
✗ Don't
💡 Pro Tips
✓ Use when
Use when skill capabilities match your task, clear ROI on time saved, and you can validate outputs. Best for repetitive tasks, learning, and quality improvement.
✗ Avoid when
Avoid when task requires deep expertise you can't validate, involves sensitive decisions, or when learning process is more valuable than speed of completion.
palmier-pro-shorts
whyashthakker/agent-skills-marketing · other · tag: video
ffmpeg-analyse-video
fabriqaai/ffmpeg-analyse-video-skill · Video · tag: video
ai-video-generation
inference-sh/skills · AI/ML · tag: video
add-to-cart
kmart.com.au/kmart-irwsr8 · shopping · tag: read-only
video-production
supercent-io/skills-template · Video · tag: video
video-prompting-guide
inferen-sh/skills · Frontend · tag: video
Keeps context tight: extract-transcript is the kind of skill you can hand to a new teammate without a long onboarding doc.
Registry listing for extract-transcript matched our evaluation — installs cleanly and behaves as described in the markdown.
Useful defaults in extract-transcript — fewer surprises than typical one-off scripts, and it plays nicely with `npx skills` flows.
extract-transcript fits our agent workflows well — practical, well scoped, and easy to wire into existing repos.
Keeps context tight: extract-transcript is the kind of skill you can hand to a new teammate without a long onboarding doc.
extract-transcript has been reliable in day-to-day use. Documentation quality is above average for community skills.
I recommend extract-transcript for anyone iterating fast on agent tooling; clear intent and a small, reviewable surface area.
extract-transcript reduced setup friction for our internal harness; good balance of opinion and flexibility.
Useful defaults in extract-transcript — fewer surprises than typical one-off scripts, and it plays nicely with `npx skills` flows.
Solid pick for teams standardizing on skills: extract-transcript is focused, and the summary matches what you get after install.
showing 1-10 of 25