Claude Skill

youtube

Extract transcripts from YouTube videos via the YouTube caption system

LLM Mart · 0 points · 15 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download axoviq-ai-synthadoc-synthadoc_skills_youtube-8dee0ee.zip · 4 KB
Part of axoviq-ai/synthadoc — 10 skills

Install

skills CLI npx skills add https://github.com/axoviq-ai/synthadoc/tree/main/synthadoc/skills/youtube
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install axoviq-ai-synthadoc@llmmart
Git 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/, 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:

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.

No comments yet.

Reviews (0)

No reviews yet.

Related