import re from smolagents.tools import Tool class YouTubeTranscriptTool(Tool): name = "youtube_transcript" description = ( "Fetches the spoken transcript of a YouTube video. " "Use this for ANY question about the content of a YouTube video — " "what is said, shown, demonstrated, or described in it. " "Pass the full YouTube URL or just the video ID." ) inputs = { "url": { "type": "string", "description": ( "The YouTube video URL (e.g. https://www.youtube.com/watch?v=XXXXXXXXXXX) " "or bare video ID (e.g. XXXXXXXXXXX)." ), } } output_type = "string" def __init__(self, *args, **kwargs): self.is_initialized = False def forward(self, url: str) -> str: try: from youtube_transcript_api import YouTubeTranscriptApi from youtube_transcript_api._errors import ( TranscriptsDisabled, NoTranscriptFound, VideoUnavailable, CouldNotRetrieveTranscript, ) except ImportError: return ( "youtube-transcript-api is not installed. " "Add 'youtube-transcript-api' to requirements.txt and rebuild." ) video_id = self._extract_video_id(url.strip()) if not video_id: return f"Could not extract a YouTube video ID from: {url}" api = YouTubeTranscriptApi() # Try to fetch English first; fall back to any available language. # v1.x: api.fetch(video_id, languages=[...]) is the simplest path. # api.list(video_id).find_transcript([...]).fetch() gives language control. try: transcript_list = api.list(video_id) try: transcript = transcript_list.find_transcript( ["en", "en-US", "en-GB"] ) except NoTranscriptFound: # Fall back to whatever is available transcript = next(iter(transcript_list)) entries = transcript.fetch() # v1.x entries are FetchedTranscriptSnippet objects with .text attribute # but also support dict-style access for backwards compat try: text = " ".join(e.text for e in entries) except AttributeError: text = " ".join(e["text"] for e in entries) if len(text) > 12000: text = text[:12000] + "\n\n[Transcript truncated at 12,000 characters]" return text except TranscriptsDisabled: return ( f"Transcripts are disabled for video {video_id}. " "Try searching for the video title or description instead." ) except VideoUnavailable: return f"Video {video_id} is unavailable or does not exist." except NoTranscriptFound: return ( f"No transcript found for video {video_id}. " "The video may not have captions." ) except CouldNotRetrieveTranscript as e: return f"Could not retrieve transcript for {video_id}: {e}" except Exception as e: return f"Could not retrieve transcript for {video_id}: {e}" @staticmethod def _extract_video_id(url: str) -> str: """Return the 11-character YouTube video ID from a URL or bare ID.""" patterns = [ r"(?:v=|youtu\.be/|embed/|shorts/)([A-Za-z0-9_-]{11})", r"^([A-Za-z0-9_-]{11})$", ] for pattern in patterns: match = re.search(pattern, url) if match: return match.group(1) return ""