youtube
Extract transcripts from YouTube videos via the YouTube caption system
Install
npx skills add https://github.com/axoviq-ai/synthadoc/tree/main/synthadoc/skills/youtube
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install axoviq-ai-synthadoc@llmmart
git clone https://github.com/axoviq-ai/synthadoc.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole axoviq-ai/synthadoc collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
YouTube Skill
Extracts the transcript (captions) from a YouTube video using the YouTube caption system — no API key or audio download required. Optionally uses a vision-capable LLM to produce a structured summary (Overview, Topics, Key takeaways) before the raw transcript.
Setup
pip install youtube-transcript-api
Standalone usage
Transcript only (no LLM required):
import asyncio
from synthadoc.skills.youtube.scripts.main import YoutubeSkill
skill = YoutubeSkill() # no provider — returns raw timestamped transcript
async def main():
result = await skill.extract("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
print(result.text) # "[0:00] text [0:04] text ..."
print(result.metadata) # {"video_id": "...", "title": "...", "url": "..."}
asyncio.run(main())
With LLM summarization (pass any provider that implements complete()):
skill = YoutubeSkill(provider=my_provider)
result = await skill.extract(url)
# result.text contains:
# ## Executive Summary
# <Overview / Topics / Key takeaways>
#
# ## Transcript
# [0:00] ...
The provider must implement:
async def complete(messages, system=None, temperature=0.0, max_tokens=4096)
-> object with .text (str), .input_tokens (int), .output_tokens (int)
Message (used to build the messages list) is importable from
synthadoc.skills.base:
from synthadoc.skills.base import Message
When this skill is used
- Source starts with
https://www.youtube.com/,https://youtu.be/, orhttps://www.youtubekids.com/
To search YouTube by topic instead of ingesting a specific URL, use the web search skill — it filters Tavily results to YouTube domains automatically:
synthadoc ingest "youtube Moore's Law"
synthadoc ingest "youtube kids: Sesame Street"
synthadoc ingest "search for youtube: history of computing"
Limitations
- Only works for videos that have captions (auto-generated or manually added). If no captions are available the source is skipped with a warning.
- Private or deleted videos are skipped gracefully.
Files (synthadoc)
-
scripts
-
main.py 6.4 KB
# SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (C) 2026 William Johnason / axoviq.com from __future__ import annotations import asyncio import logging from urllib.parse import urlparse, parse_qs from synthadoc.skills.base import BaseSkill, ExtractedContent, SkillMeta import re _CJK_RE = re.compile( r'[一-鿿-ゟ゠-ヿ가-]' ) _YOUTUBE_SUMMARY_PROMPT = ( "Summarise this YouTube video transcript for a knowledge wiki.\n" "Write no more than {limit} words total across all sections.\n\n" "Structure (use these exact headings):\n" "**Overview** — 2–3 sentences: what the video is, who made it, what it covers at a high level.\n" "**Topics covered** — bullet list of the main subjects discussed (aim for 8–15 bullets, each 1–2 sentences).\n" "**Key takeaways** — 3–5 bullet points: the most important facts, conclusions, or insights a reader should remember.\n\n" "Markdown only. No filler phrases. No meta-commentary about the video being introductory or educational.\n\n" "Transcript:\n{transcript}" ) def _is_cjk_dominant(text: str) -> bool: if not text: return False return len(_CJK_RE.findall(text)) / len(text) > 0.10 logger = logging.getLogger(__name__) async def _fetch_video_title(video_id: str) -> str | None: """Fetch video title from YouTube oEmbed — free, no API key required. httpx is used here but is not a hard requirement of the skill — the transcript extraction works without it. If httpx is absent the title is simply omitted from the result metadata. """ try: import httpx except ImportError: return None url = ( f"https://www.youtube.com/oembed" f"?url=https://www.youtube.com/watch?v={video_id}&format=json" ) try: async with httpx.AsyncClient(timeout=5) as client: resp = await client.get(url) if resp.status_code == 200: return resp.json().get("title") or None except Exception: pass return None def _format_timestamp(seconds: float) -> str: """Convert seconds to MM:SS for embedding in transcript text.""" mins, secs = divmod(int(seconds), 60) return f"{mins}:{secs:02d}" def _extract_video_id(url: str) -> str | None: """Return the YouTube video ID from any recognised URL form, or None.""" parsed = urlparse(url) # youtu.be/<id> if parsed.hostname in ("youtu.be",): vid = parsed.path.lstrip("/").split("/")[0] return vid or None # youtube.com/watch?v=<id> qs = parse_qs(parsed.query) if "v" in qs: return qs["v"][0] or None # youtube.com/embed/<id>, youtube.com/shorts/<id>, youtube.com/live/<id>, youtube.com/v/<id> parts = parsed.path.split("/") for segment in ("embed", "shorts", "live", "v"): try: idx = parts.index(segment) vid = parts[idx + 1] if idx + 1 < len(parts) else "" return vid or None except ValueError: pass return None class YoutubeSkill(BaseSkill): meta = SkillMeta( name="youtube", description="Extract transcripts from YouTube videos via the YouTube caption system", extensions=["https://www.youtube.com/", "https://youtu.be/"], ) def __init__(self, provider=None) -> None: super().__init__() self._provider = provider async def _summarise(self, transcript_text: str) -> str: limit = 2000 if _is_cjk_dominant(transcript_text) else 1000 prompt = _YOUTUBE_SUMMARY_PROMPT.format( limit=limit, transcript=transcript_text[:6000], ) from synthadoc.skills.base import Message resp = await self._provider.complete( messages=[Message(role="user", content=prompt)], temperature=0.3, max_tokens=2048, ) return resp.text.strip() async def extract(self, source: str) -> ExtractedContent: from youtube_transcript_api import ( YouTubeTranscriptApi, NoTranscriptFound, TranscriptsDisabled, VideoUnavailable, ) video_id = _extract_video_id(source) if not video_id: logger.warning("youtube: could not parse video ID from %s — skipping", source) return ExtractedContent(text="", source_path=source, metadata={"url": source}) api = YouTubeTranscriptApi() try: fetched = await asyncio.to_thread(api.fetch, video_id) except (NoTranscriptFound, TranscriptsDisabled): logger.warning( "youtube: no captions available for %s — " "subtitles are disabled or unavailable for this video", source, ) return ExtractedContent( text="", source_path=source, metadata={"url": source, "video_id": video_id, "no_transcript": True}, ) except VideoUnavailable: logger.warning( "youtube: video unavailable (private or deleted): %s — skipping", source ) return ExtractedContent( text="", source_path=source, metadata={"url": source} ) transcript_text = " ".join( f"[{_format_timestamp(snippet.start)}] {snippet.text}" for snippet in fetched ) video_title = await _fetch_video_title(video_id) base_meta: dict = { "url": source, "video_id": video_id, "suggested_slug": f"youtube-{video_id}", } if video_title: base_meta["title"] = video_title if self._provider is not None: try: summary = await self._summarise(transcript_text) structured = ( f"## Executive Summary\n\n{summary}\n\n" f"## Transcript\n\n{transcript_text}" ) return ExtractedContent( text=structured, source_path=source, metadata={**base_meta, "has_summary": True}, ) except Exception: logger.warning( "youtube: summary LLM call failed for %s — returning raw transcript", source ) return ExtractedContent( text=transcript_text, source_path=source, metadata=base_meta, ) -
__init__.py 0 B
-
-
requirements.txt 104 B
youtube-transcript-api # httpx is optional — used only for video title lookup; skill works without it -
SKILL.md 2.5 KB
--- name: youtube version: "1.0" description: Extract transcripts from YouTube videos via the YouTube caption system entry: script: scripts/main.py class: YoutubeSkill triggers: extensions: - "https://www.youtube.com/" - "https://youtu.be/" - "https://www.youtubekids.com/" intents: [] requires: - youtube-transcript-api author: axoviq.com license: AGPL-3.0-or-later --- # YouTube Skill Extracts the transcript (captions) from a YouTube video using the YouTube caption system — no API key or audio download required. Optionally uses a vision-capable LLM to produce a structured summary (Overview, Topics, Key takeaways) before the raw transcript. ## Setup ```bash pip install youtube-transcript-api ``` ## Standalone usage **Transcript only** (no LLM required): ```python import asyncio from synthadoc.skills.youtube.scripts.main import YoutubeSkill skill = YoutubeSkill() # no provider — returns raw timestamped transcript async def main(): result = await skill.extract("https://www.youtube.com/watch?v=dQw4w9WgXcQ") print(result.text) # "[0:00] text [0:04] text ..." print(result.metadata) # {"video_id": "...", "title": "...", "url": "..."} asyncio.run(main()) ``` **With LLM summarization** (pass any provider that implements `complete()`): ```python skill = YoutubeSkill(provider=my_provider) result = await skill.extract(url) # result.text contains: # ## Executive Summary # <Overview / Topics / Key takeaways> # # ## Transcript # [0:00] ... ``` The provider must implement: ```python async def complete(messages, system=None, temperature=0.0, max_tokens=4096) -> object with .text (str), .input_tokens (int), .output_tokens (int) ``` `Message` (used to build the `messages` list) is importable from `synthadoc.skills.base`: ```python from synthadoc.skills.base import Message ``` ## When this skill is used - Source starts with `https://www.youtube.com/`, `https://youtu.be/`, or `https://www.youtubekids.com/` To search YouTube by topic instead of ingesting a specific URL, use the web search skill — it filters Tavily results to YouTube domains automatically: ```bash synthadoc ingest "youtube Moore's Law" synthadoc ingest "youtube kids: Sesame Street" synthadoc ingest "search for youtube: history of computing" ``` ## Limitations - Only works for videos that have captions (auto-generated or manually added). If no captions are available the source is skipped with a warning. - Private or deleted videos are skipped gracefully.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.