Claude Skill

video-transcript

Extract video transcripts: yt-dlp subtitles to clean paragraphs.

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

Full trust report

Download notque-vexjoy-agent-skills_research_video-transcript-8ad6845.zip · 2 KB
Part of notque/vexjoy-agent — 69 skills

Install

skills CLI npx skills add https://github.com/notque/vexjoy-agent/tree/main/skills/research/video-transcript
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install notque-vexjoy-agent@llmmart
Git git clone https://github.com/notque/vexjoy-agent.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole notque/vexjoy-agent collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Video Transcript

Pull a video's transcript as readable paragraphs. Two paths, in order:

Path 1 — uploader subtitles (accurate, prefer when present):

yt-dlp --skip-download --write-subs --sub-langs en --sub-format vtt \
  -o '<work-dir>/%(id)s' '<URL>'

Path 2 — auto-generated captions (fallback when path 1 writes no file):

yt-dlp --skip-download --write-auto-subs --sub-langs en --sub-format vtt \
  -o '<work-dir>/%(id)s' '<URL>'

Then clean the VTT into paragraphs:

python3 skills/research/video-transcript/scripts/vtt_to_paragraph.py <work-dir>/<id>.en.vtt

Default output is plain paragraph text with [Music]-style cues stripped and the rolling duplicates of auto-captions deduplicated. Use --timestamps for [mm:ss] markers, --keep-brackets to keep cue tags, -o FILE to write to a file.

For other languages, change --sub-langs (e.g. de, en.*). List what a video offers with yt-dlp --list-subs '<URL>'.

Error handling

Both paths write no .vtt file

Cause: video has no subtitles or captions in the requested language. Solution: run yt-dlp --list-subs '<URL>' and pick an available language; if none exist, report that and offer audio transcription via the markdown-converter skill on a downloaded audio file.

HTTP 429 / "Sign in to confirm"

Cause: platform rate-limiting the host. Solution: wait and retry with --sleep-requests 2; keep request volume low.

Cleaner output repeats lines

Cause: VTT came from a third path (e.g. translated captions) with cue formats the dedupe misses. Solution: rerun the cleaner; if repeats remain, file the sample VTT alongside a fix to vtt_to_paragraph.py.

Files (vexjoy-agent)
  • scripts
    • vtt_to_paragraph.py 3.9 KB
      #!/usr/bin/env python3
      """Clean a WebVTT subtitle file into readable paragraph text.
      
      Handles both uploader subtitles and YouTube auto-captions (rolling
      duplicate lines, inline <c>/timing tags, [Music]-style cues).
      Stdlib only.
      """
      
      import argparse
      import re
      import sys
      from pathlib import Path
      
      TIMESTAMP_LINE = re.compile(r"^(\d{2}:)?\d{2}:\d{2}\.\d{3}\s+-->\s+(\d{2}:)?\d{2}:\d{2}\.\d{3}")
      INLINE_TAG = re.compile(r"<[^>]+>")
      BRACKET_CUE = re.compile(r"\[[^\]]*\]|\([^)]*\)")
      HEADER_PREFIXES = ("WEBVTT", "Kind:", "Language:", "NOTE", "STYLE", "REGION")
      PARAGRAPH_GAP_SECONDS = 4.0
      SENTENCES_PER_PARAGRAPH = 5
      
      
      def parse_start_seconds(line: str) -> float:
          """Return the cue start time in seconds."""
          start = line.split("-->")[0].strip()
          parts = start.split(":")
          seconds = 0.0
          for part in parts:
              seconds = seconds * 60 + float(part)
          return seconds
      
      
      def extract_cues(text: str, keep_brackets: bool) -> list[tuple[float, str]]:
          """Return (start_seconds, text) cues, deduplicated and tag-free."""
          cues: list[tuple[float, str]] = []
          current_start = 0.0
          seen_tail: str = ""
          for raw in text.splitlines():
              line = raw.strip()
              if not line or line.isdigit() or line.startswith(HEADER_PREFIXES):
                  continue
              if TIMESTAMP_LINE.match(line):
                  current_start = parse_start_seconds(line)
                  continue
              line = INLINE_TAG.sub("", line)
              if not keep_brackets:
                  line = BRACKET_CUE.sub("", line)
              line = re.sub(r"\s+", " ", line).strip()
              # Auto-captions roll: each cue repeats the previous line, then adds one.
              if not line or line == seen_tail:
                  continue
              seen_tail = line
              cues.append((current_start, line))
          return cues
      
      
      def format_timestamp(seconds: float) -> str:
          minutes, secs = divmod(int(seconds), 60)
          hours, minutes = divmod(minutes, 60)
          if hours:
              return f"[{hours}:{minutes:02d}:{secs:02d}]"
          return f"[{minutes}:{secs:02d}]"
      
      
      def build_paragraphs(cues: list[tuple[float, str]], timestamps: bool) -> str:
          """Join cues into paragraphs, breaking on silence gaps or sentence count."""
          paragraphs: list[str] = []
          chunk: list[str] = []
          sentence_count = 0
          previous_start = None
          for start, line in cues:
              gap = previous_start is not None and start - previous_start > PARAGRAPH_GAP_SECONDS
              if chunk and (gap or sentence_count >= SENTENCES_PER_PARAGRAPH):
                  paragraphs.append(" ".join(chunk))
                  chunk = []
                  sentence_count = 0
              if timestamps and not chunk:
                  chunk.append(format_timestamp(start))
              chunk.append(line)
              sentence_count += len(re.findall(r"[.!?](?:\s|$)", line))
              previous_start = start
          if chunk:
              paragraphs.append(" ".join(chunk))
          return "\n\n".join(paragraphs) + "\n"
      
      
      def main() -> int:
          parser = argparse.ArgumentParser(description=__doc__)
          parser.add_argument("vtt_file", type=Path, help="input .vtt file")
          parser.add_argument("-o", "--output", type=Path, help="write result to file")
          parser.add_argument("--timestamps", action="store_true", help="prefix paragraphs with [mm:ss]")
          parser.add_argument("--keep-brackets", action="store_true", help="keep [Music]-style cues")
          args = parser.parse_args()
      
          try:
              text = args.vtt_file.read_text(encoding="utf-8", errors="replace")
          except OSError as exc:
              print(f"error: {exc}", file=sys.stderr)
              return 1
      
          cues = extract_cues(text, keep_brackets=args.keep_brackets)
          if not cues:
              print("error: no subtitle text found in file", file=sys.stderr)
              return 1
      
          result = build_paragraphs(cues, timestamps=args.timestamps)
          if args.output:
              args.output.write_text(result, encoding="utf-8")
          else:
              sys.stdout.write(result)
          return 0
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
  • SKILL.md 2.3 KB
    ---
    name: video-transcript
    promoted_to: video-editing
    description: "Extract video transcripts: yt-dlp subtitles to clean paragraphs."
    user_invocable: false  # default -- router-dispatched, not user-typed
    agent: python-general-engineer
    allowed-tools:
      - Bash
      - Read
    routing:
      triggers:
        - "video transcript"
        - "youtube transcript"
        - "extract transcript"
        - "download subtitles"
        - "what does this video say"
        - "get transcript"
        - "transcribe video"
        - "transcribe this video"
        - "pull subtitles"
        - "captions from video"
      category: research
      pairs_with:
        - research
    ---
    
    # Video Transcript
    
    Pull a video's transcript as readable paragraphs. Two paths, in order:
    
    **Path 1 — uploader subtitles** (accurate, prefer when present):
    
    ```bash
    yt-dlp --skip-download --write-subs --sub-langs en --sub-format vtt \
      -o '<work-dir>/%(id)s' '<URL>'
    ```
    
    **Path 2 — auto-generated captions** (fallback when path 1 writes no file):
    
    ```bash
    yt-dlp --skip-download --write-auto-subs --sub-langs en --sub-format vtt \
      -o '<work-dir>/%(id)s' '<URL>'
    ```
    
    Then clean the VTT into paragraphs:
    
    ```bash
    python3 skills/research/video-transcript/scripts/vtt_to_paragraph.py <work-dir>/<id>.en.vtt
    ```
    
    Default output is plain paragraph text with `[Music]`-style cues stripped and the rolling duplicates of auto-captions deduplicated. Use `--timestamps` for `[mm:ss]` markers, `--keep-brackets` to keep cue tags, `-o FILE` to write to a file.
    
    For other languages, change `--sub-langs` (e.g. `de`, `en.*`). List what a video offers with `yt-dlp --list-subs '<URL>'`.
    
    ## Error handling
    
    ### Both paths write no .vtt file
    Cause: video has no subtitles or captions in the requested language.
    Solution: run `yt-dlp --list-subs '<URL>'` and pick an available language; if none exist, report that and offer audio transcription via the `markdown-converter` skill on a downloaded audio file.
    
    ### HTTP 429 / "Sign in to confirm"
    Cause: platform rate-limiting the host.
    Solution: wait and retry with `--sleep-requests 2`; keep request volume low.
    
    ### Cleaner output repeats lines
    Cause: VTT came from a third path (e.g. translated captions) with cue formats the dedupe misses.
    Solution: rerun the cleaner; if repeats remain, file the sample VTT alongside a fix to `vtt_to_paragraph.py`.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related