mirror of
https://github.com/hanzoai/video.git
synced 2026-10-30 18:18:27 +00:00
217 lines
7.1 KiB
Python
217 lines
7.1 KiB
Python
"""YouTube transcript fetcher tool wrapping youtube-transcript-api.
|
|
|
|
Extracts transcripts/captions from YouTube videos without downloading the video.
|
|
Instant, free, no API key needed. Falls back to yt-dlp subtitle download.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import time
|
|
from typing import Any
|
|
|
|
from tools.base_tool import (
|
|
BaseTool,
|
|
Determinism,
|
|
ExecutionMode,
|
|
ResourceProfile,
|
|
ToolResult,
|
|
ToolStability,
|
|
ToolStatus,
|
|
ToolTier,
|
|
ToolRuntime,
|
|
)
|
|
|
|
|
|
class TranscriptFetcher(BaseTool):
|
|
name = "transcript_fetcher"
|
|
version = "0.1.0"
|
|
tier = ToolTier.ANALYZE
|
|
capability = "analysis"
|
|
provider = "youtube-transcript-api"
|
|
stability = ToolStability.PRODUCTION
|
|
execution_mode = ExecutionMode.SYNC
|
|
determinism = Determinism.DETERMINISTIC
|
|
runtime = ToolRuntime.LOCAL
|
|
|
|
dependencies = ["python:youtube_transcript_api"]
|
|
install_instructions = (
|
|
"Install youtube-transcript-api: pip install youtube-transcript-api"
|
|
)
|
|
agent_skills = []
|
|
|
|
capabilities = [
|
|
"fetch_transcript",
|
|
"list_transcripts",
|
|
]
|
|
|
|
best_for = [
|
|
"fast YouTube transcript extraction",
|
|
"caption-based analysis without video download",
|
|
"getting timestamped text from YouTube videos",
|
|
]
|
|
|
|
not_good_for = [
|
|
"non-YouTube platforms (Instagram, TikTok)",
|
|
"videos without any captions",
|
|
"speaker diarization (use transcriber tool instead)",
|
|
]
|
|
|
|
input_schema = {
|
|
"type": "object",
|
|
"required": ["url_or_video_id"],
|
|
"properties": {
|
|
"url_or_video_id": {
|
|
"type": "string",
|
|
"description": "YouTube URL or video ID",
|
|
},
|
|
"languages": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"default": ["en"],
|
|
"description": "Preferred languages in priority order",
|
|
},
|
|
"include_auto_generated": {
|
|
"type": "boolean",
|
|
"default": True,
|
|
"description": "Whether to include auto-generated captions",
|
|
},
|
|
},
|
|
}
|
|
|
|
output_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"transcript": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"text": {"type": "string"},
|
|
"start": {"type": "number"},
|
|
"duration": {"type": "number"},
|
|
},
|
|
},
|
|
},
|
|
"full_text": {"type": "string"},
|
|
"language": {"type": "string"},
|
|
"is_auto_generated": {"type": "boolean"},
|
|
"word_count": {"type": "integer"},
|
|
"source": {"type": "string"},
|
|
},
|
|
}
|
|
|
|
resource_profile = ResourceProfile(
|
|
cpu_cores=1, ram_mb=256, vram_mb=0, disk_mb=10,
|
|
network_required=True,
|
|
)
|
|
idempotency_key_fields = ["url_or_video_id", "languages"]
|
|
side_effects = []
|
|
fallback = "transcriber"
|
|
user_visible_verification = [
|
|
"Spot-check transcript accuracy against video audio",
|
|
]
|
|
|
|
def _extract_video_id(self, url_or_id: str) -> str:
|
|
"""Extract YouTube video ID from URL or return as-is if already an ID."""
|
|
# Already a bare ID (11 chars, alphanumeric + - _)
|
|
if re.match(r"^[A-Za-z0-9_-]{11}$", url_or_id):
|
|
return url_or_id
|
|
|
|
# Standard YouTube URLs
|
|
patterns = [
|
|
r"(?:youtube\.com/watch\?.*v=|youtu\.be/|youtube\.com/embed/|youtube\.com/shorts/)([A-Za-z0-9_-]{11})",
|
|
]
|
|
for pattern in patterns:
|
|
match = re.search(pattern, url_or_id)
|
|
if match:
|
|
return match.group(1)
|
|
|
|
# If nothing matched, try using the whole string as ID
|
|
return url_or_id.strip()
|
|
|
|
def execute(self, inputs: dict[str, Any]) -> ToolResult:
|
|
video_id = self._extract_video_id(inputs["url_or_video_id"])
|
|
languages = inputs.get("languages", ["en"])
|
|
include_auto = inputs.get("include_auto_generated", True)
|
|
|
|
start = time.time()
|
|
|
|
try:
|
|
from youtube_transcript_api import YouTubeTranscriptApi
|
|
|
|
ytt = YouTubeTranscriptApi()
|
|
|
|
# Fetch transcript using the instance-based API (v1.0+)
|
|
transcript_result = ytt.fetch(video_id, languages=languages)
|
|
|
|
# Build segments and full text from snippets
|
|
segments = []
|
|
full_text_parts = []
|
|
for snippet in transcript_result.snippets:
|
|
segments.append({
|
|
"text": snippet.text,
|
|
"start": round(snippet.start, 3),
|
|
"duration": round(snippet.duration, 3),
|
|
})
|
|
full_text_parts.append(snippet.text)
|
|
|
|
full_text = " ".join(full_text_parts)
|
|
word_count = len(full_text.split())
|
|
|
|
# Get auto-generated status and language from the result
|
|
is_auto = getattr(transcript_result, "is_generated", False)
|
|
detected_lang = getattr(transcript_result, "language", languages[0])
|
|
# If language is an object, get the code
|
|
if hasattr(detected_lang, "code"):
|
|
detected_lang = detected_lang.code
|
|
elif not isinstance(detected_lang, str):
|
|
detected_lang = languages[0]
|
|
|
|
elapsed = time.time() - start
|
|
|
|
return ToolResult(
|
|
success=True,
|
|
data={
|
|
"transcript": segments,
|
|
"full_text": full_text,
|
|
"language": detected_lang,
|
|
"is_auto_generated": is_auto,
|
|
"word_count": word_count,
|
|
"source": "youtube_captions",
|
|
"video_id": video_id,
|
|
"segment_count": len(segments),
|
|
},
|
|
duration_seconds=round(elapsed, 2),
|
|
)
|
|
|
|
except ImportError:
|
|
return ToolResult(
|
|
success=False,
|
|
error="youtube-transcript-api not installed. Run: pip install youtube-transcript-api",
|
|
)
|
|
except Exception as e:
|
|
elapsed = time.time() - start
|
|
error_str = str(e)
|
|
|
|
# Provide helpful error messages
|
|
if "Could not retrieve" in error_str or "TranscriptsDisabled" in error_str:
|
|
return ToolResult(
|
|
success=False,
|
|
error=(
|
|
f"No captions available for video {video_id}. "
|
|
"This video may not have captions enabled. "
|
|
"Fallback: download the video and use the transcriber tool "
|
|
"with Whisper for local transcription."
|
|
),
|
|
data={"video_id": video_id, "fallback_suggested": "transcriber"},
|
|
duration_seconds=round(elapsed, 2),
|
|
)
|
|
|
|
return ToolResult(
|
|
success=False,
|
|
error=f"Transcript fetch failed: {error_str}",
|
|
data={"video_id": video_id},
|
|
duration_seconds=round(elapsed, 2),
|
|
)
|