Initial commit: /watch skill v0.1.0
Give Claude the ability to watch any video — yt-dlp download, ffmpeg frame extraction with auto-scaled fps, native-caption transcript with Whisper (Groq/OpenAI) fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Parse a WebVTT subtitle file into a clean, timestamped transcript.
|
||||
|
||||
YouTube auto-subs emit rolling-duplicate cues (each line appears 2-3 times as it
|
||||
scrolls). We dedupe consecutive identical cues and merge their time ranges.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
TS_RE = re.compile(
|
||||
r"(\d{2}):(\d{2}):(\d{2})[.,](\d{3})\s+-->\s+(\d{2}):(\d{2}):(\d{2})[.,](\d{3})"
|
||||
)
|
||||
TAG_RE = re.compile(r"<[^>]+>")
|
||||
|
||||
|
||||
def _to_seconds(h: str, m: str, s: str, ms: str) -> float:
|
||||
return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000.0
|
||||
|
||||
|
||||
def parse_vtt(path: str) -> list[dict]:
|
||||
text = Path(path).read_text(encoding="utf-8", errors="ignore")
|
||||
lines = text.splitlines()
|
||||
|
||||
segments: list[dict] = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
match = TS_RE.match(lines[i])
|
||||
if not match:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
start = _to_seconds(*match.groups()[:4])
|
||||
end = _to_seconds(*match.groups()[4:])
|
||||
i += 1
|
||||
|
||||
cue_lines: list[str] = []
|
||||
while i < len(lines) and lines[i].strip():
|
||||
cleaned = TAG_RE.sub("", lines[i]).strip()
|
||||
if cleaned:
|
||||
cue_lines.append(cleaned)
|
||||
i += 1
|
||||
|
||||
cue_text = " ".join(cue_lines).strip()
|
||||
if cue_text:
|
||||
segments.append({"start": round(start, 2), "end": round(end, 2), "text": cue_text})
|
||||
i += 1
|
||||
|
||||
return _dedupe(segments)
|
||||
|
||||
|
||||
def _dedupe(segments: list[dict]) -> list[dict]:
|
||||
"""Collapse rolling duplicates common in YouTube auto-subs."""
|
||||
out: list[dict] = []
|
||||
for seg in segments:
|
||||
if out and seg["text"] == out[-1]["text"]:
|
||||
out[-1]["end"] = seg["end"]
|
||||
continue
|
||||
if out and seg["text"].startswith(out[-1]["text"] + " "):
|
||||
out[-1]["text"] = seg["text"]
|
||||
out[-1]["end"] = seg["end"]
|
||||
continue
|
||||
out.append(seg)
|
||||
return out
|
||||
|
||||
|
||||
def filter_range(
|
||||
segments: list[dict],
|
||||
start_seconds: float | None,
|
||||
end_seconds: float | None,
|
||||
) -> list[dict]:
|
||||
"""Return segments whose time range overlaps [start, end]."""
|
||||
if start_seconds is None and end_seconds is None:
|
||||
return segments
|
||||
lo = start_seconds if start_seconds is not None else float("-inf")
|
||||
hi = end_seconds if end_seconds is not None else float("inf")
|
||||
return [seg for seg in segments if seg["end"] >= lo and seg["start"] <= hi]
|
||||
|
||||
|
||||
def format_transcript(segments: list[dict]) -> str:
|
||||
lines = []
|
||||
for seg in segments:
|
||||
start = int(seg["start"])
|
||||
stamp = f"[{start // 60:02d}:{start % 60:02d}]"
|
||||
lines.append(f"{stamp} {seg['text']}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: transcribe.py <vtt-path>", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
print(format_transcript(parse_vtt(sys.argv[1])))
|
||||
Reference in New Issue
Block a user