Spaces:
				
			
			
	
			
			
		Runtime error
		
	
	
	
			
			
	
	
	
	
		
		
		Runtime error
		
	
		Yago Bolivar
		
	commited on
		
		
					Commit 
							
							·
						
						baa65ee
	
1
								Parent(s):
							
							afa93b5
								
feat: enhance YouTube video processing with improved error handling and logging
Browse files- src/video_processing_tool.py +85 -14
 
    	
        src/video_processing_tool.py
    CHANGED
    
    | 
         @@ -8,14 +8,20 @@ import re 
     | 
|
| 8 | 
         
             
            import shutil
         
     | 
| 9 | 
         
             
            import time
         
     | 
| 10 | 
         
             
            from smolagents.tools import Tool
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 11 | 
         | 
| 12 | 
         
             
            class VideoProcessingTool(Tool):
         
     | 
| 13 | 
         
             
                """
         
     | 
| 14 | 
         
             
                Analyzes video content, extracting information such as frames, audio, or metadata.
         
     | 
| 15 | 
         
             
                Useful for tasks like video summarization, frame extraction, transcript analysis, or content analysis.
         
     | 
| 
         | 
|
| 16 | 
         
             
                """
         
     | 
| 17 | 
         
             
                name = "video_processor"
         
     | 
| 18 | 
         
            -
                description = "Analyzes video content from a file path or YouTube URL. Can extract frames, detect objects, get transcripts, and provide video metadata."
         
     | 
| 19 | 
         
             
                inputs = {
         
     | 
| 20 | 
         
             
                    "file_path": {"type": "string", "description": "Path to the video file or YouTube URL.", "nullable": True},
         
     | 
| 21 | 
         
             
                    "task": {"type": "string", "description": "Specific task to perform (e.g., 'extract_frames', 'get_transcript', 'detect_objects', 'get_metadata').", "nullable": True},
         
     | 
| 
         @@ -75,24 +81,80 @@ class VideoProcessingTool(Tool): 
     | 
|
| 75 | 
         
             
                    if task_parameters is None:
         
     | 
| 76 | 
         
             
                        task_parameters = {}
         
     | 
| 77 | 
         | 
| 
         | 
|
| 78 | 
         
             
                    is_youtube_url = file_path and ("youtube.com/" in file_path or "youtu.be/" in file_path)
         
     | 
| 79 | 
         
             
                    video_source_path = file_path
         
     | 
| 80 | 
         | 
| 
         | 
|
| 81 | 
         
             
                    if is_youtube_url:
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 82 | 
         
             
                        download_resolution = task_parameters.get("resolution", "360p")
         
     | 
| 83 | 
         
             
                        download_result = self.download_video(file_path, resolution=download_resolution)
         
     | 
| 
         | 
|
| 84 | 
         
             
                        if download_result.get("error"):
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 85 | 
         
             
                            return download_result
         
     | 
| 
         | 
|
| 86 | 
         
             
                        video_source_path = download_result.get("file_path")
         
     | 
| 87 | 
         
             
                        if not video_source_path or not os.path.exists(video_source_path):
         
     | 
| 88 | 
         
            -
             
     | 
| 89 | 
         | 
| 90 | 
         
             
                    elif file_path and not os.path.exists(file_path):
         
     | 
| 91 | 
         
             
                        return {"error": f"Video file not found: {file_path}"}
         
     | 
| 92 | 
         
             
                    elif not file_path and task not in ['get_transcript']: # transcript can work with URL directly
         
     | 
| 93 | 
         
            -
             
     | 
| 94 | 
         
            -
             
     | 
| 95 | 
         | 
| 
         | 
|
| 96 | 
         
             
                    if task == "get_metadata":
         
     | 
| 97 | 
         
             
                        return self.get_video_metadata(video_source_path)
         
     | 
| 98 | 
         
             
                    elif task == "extract_frames":
         
     | 
| 
         @@ -108,7 +170,6 @@ class VideoProcessingTool(Tool): 
     | 
|
| 108 | 
         
             
                        confidence_threshold = task_parameters.get("confidence_threshold", 0.5)
         
     | 
| 109 | 
         
             
                        frames_to_process = task_parameters.get("frames_to_process", 5) # Process N frames
         
     | 
| 110 | 
         
             
                        return self.detect_objects_in_video(video_source_path, confidence_threshold=confidence_threshold, num_frames_to_sample=frames_to_process)
         
     | 
| 111 | 
         
            -
                    # Add more tasks as needed, e.g., extract_audio
         
     | 
| 112 | 
         
             
                    else:
         
     | 
| 113 | 
         
             
                        return {"error": f"Unsupported task: {task}"}
         
     | 
| 114 | 
         | 
| 
         @@ -120,7 +181,7 @@ class VideoProcessingTool(Tool): 
     | 
|
| 120 | 
         
             
                    return None
         
     | 
| 121 | 
         | 
| 122 | 
         
             
                def download_video(self, youtube_url, resolution="360p"):
         
     | 
| 123 | 
         
            -
                    """Download YouTube video for processing."""
         
     | 
| 124 | 
         
             
                    video_id = self._extract_video_id(youtube_url)
         
     | 
| 125 | 
         
             
                    if not video_id:
         
     | 
| 126 | 
         
             
                        return {"error": "Invalid YouTube URL or could not extract video ID."}
         
     | 
| 
         @@ -132,6 +193,7 @@ class VideoProcessingTool(Tool): 
     | 
|
| 132 | 
         
             
                        return {"success": True, "file_path": output_file_path, "message": "Video already downloaded."}
         
     | 
| 133 | 
         | 
| 134 | 
         
             
                    try:
         
     | 
| 
         | 
|
| 135 | 
         
             
                        ydl_opts = {
         
     | 
| 136 | 
         
             
                            'format': f'bestvideo[height<={resolution[:-1]}][ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
         
     | 
| 137 | 
         
             
                            'outtmpl': output_file_path,
         
     | 
| 
         @@ -139,11 +201,15 @@ class VideoProcessingTool(Tool): 
     | 
|
| 139 | 
         
             
                            'quiet': True,
         
     | 
| 140 | 
         
             
                            'no_warnings': True,
         
     | 
| 141 | 
         
             
                        }
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 142 | 
         
             
                        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
         
     | 
| 143 | 
         
             
                            ydl.download([youtube_url])
         
     | 
| 144 | 
         | 
| 145 | 
         
             
                        if not os.path.exists(output_file_path): # Check if download actually created the file
         
     | 
| 146 | 
         
            -
             
     | 
| 
         | 
|
| 147 | 
         
             
                            ydl_opts['format'] = f'best[height<={resolution[:-1]}]' # more generic
         
     | 
| 148 | 
         
             
                            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
         
     | 
| 149 | 
         
             
                                info_dict = ydl.extract_info(youtube_url, download=True)
         
     | 
| 
         @@ -152,12 +218,10 @@ class VideoProcessingTool(Tool): 
     | 
|
| 152 | 
         
             
                                if downloaded_files:
         
     | 
| 153 | 
         
             
                                    actual_file_path = os.path.join(self.temp_dir, downloaded_files[0])
         
     | 
| 154 | 
         
             
                                    if actual_file_path != output_file_path and actual_file_path.endswith(('.mkv', '.webm', '.flv')):
         
     | 
| 155 | 
         
            -
                                        #  
     | 
| 156 | 
         
            -
                                         
     | 
| 157 | 
         
            -
             
     | 
| 158 | 
         
            -
             
     | 
| 159 | 
         
            -
                                         return {"error": f"Downloaded video is not in a directly usable format: {downloaded_files[0]}"}
         
     | 
| 160 | 
         
            -
             
     | 
| 161 | 
         | 
| 162 | 
         
             
                        if os.path.exists(output_file_path):
         
     | 
| 163 | 
         
             
                            return {"success": True, "file_path": output_file_path}
         
     | 
| 
         @@ -165,7 +229,14 @@ class VideoProcessingTool(Tool): 
     | 
|
| 165 | 
         
             
                            return {"error": "Video download failed, file not found after attempt."}
         
     | 
| 166 | 
         | 
| 167 | 
         
             
                    except yt_dlp.utils.DownloadError as e:
         
     | 
| 168 | 
         
            -
                         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 169 | 
         
             
                    except Exception as e:
         
     | 
| 170 | 
         
             
                        return {"error": f"Failed to download video: {str(e)}"}
         
     | 
| 171 | 
         | 
| 
         | 
|
| 8 | 
         
             
            import shutil
         
     | 
| 9 | 
         
             
            import time
         
     | 
| 10 | 
         
             
            from smolagents.tools import Tool
         
     | 
| 11 | 
         
            +
            import logging
         
     | 
| 12 | 
         
            +
             
     | 
| 13 | 
         
            +
            # Set up logging
         
     | 
| 14 | 
         
            +
            logging.basicConfig(level=logging.INFO)
         
     | 
| 15 | 
         
            +
            logger = logging.getLogger(__name__)
         
     | 
| 16 | 
         | 
| 17 | 
         
             
            class VideoProcessingTool(Tool):
         
     | 
| 18 | 
         
             
                """
         
     | 
| 19 | 
         
             
                Analyzes video content, extracting information such as frames, audio, or metadata.
         
     | 
| 20 | 
         
             
                Useful for tasks like video summarization, frame extraction, transcript analysis, or content analysis.
         
     | 
| 21 | 
         
            +
                Has limitations with YouTube content due to platform restrictions.
         
     | 
| 22 | 
         
             
                """
         
     | 
| 23 | 
         
             
                name = "video_processor"
         
     | 
| 24 | 
         
            +
                description = "Analyzes video content from a file path or YouTube URL. Can extract frames, detect objects, get transcripts, and provide video metadata. Note: Has limitations with YouTube content due to platform restrictions."
         
     | 
| 25 | 
         
             
                inputs = {
         
     | 
| 26 | 
         
             
                    "file_path": {"type": "string", "description": "Path to the video file or YouTube URL.", "nullable": True},
         
     | 
| 27 | 
         
             
                    "task": {"type": "string", "description": "Specific task to perform (e.g., 'extract_frames', 'get_transcript', 'detect_objects', 'get_metadata').", "nullable": True},
         
     | 
| 
         | 
|
| 81 | 
         
             
                    if task_parameters is None:
         
     | 
| 82 | 
         
             
                        task_parameters = {}
         
     | 
| 83 | 
         | 
| 84 | 
         
            +
                    # Check for YouTube URL and provide appropriate warnings
         
     | 
| 85 | 
         
             
                    is_youtube_url = file_path and ("youtube.com/" in file_path or "youtu.be/" in file_path)
         
     | 
| 86 | 
         
             
                    video_source_path = file_path
         
     | 
| 87 | 
         | 
| 88 | 
         
            +
                    # Special case for YouTube - check for likely restrictions before attempting download
         
     | 
| 89 | 
         
             
                    if is_youtube_url:
         
     | 
| 90 | 
         
            +
                        # For transcript tasks, try direct API first without downloading
         
     | 
| 91 | 
         
            +
                        if task == "get_transcript":
         
     | 
| 92 | 
         
            +
                            transcript_result = self.get_youtube_transcript(file_path)
         
     | 
| 93 | 
         
            +
                            if not transcript_result.get("error"):
         
     | 
| 94 | 
         
            +
                                return transcript_result
         
     | 
| 95 | 
         
            +
                            
         
     | 
| 96 | 
         
            +
                            # If transcript API fails with certain errors, provide more helpful response
         
     | 
| 97 | 
         
            +
                            error_msg = transcript_result.get("error", "")
         
     | 
| 98 | 
         
            +
                            if "Transcripts are disabled" in error_msg:
         
     | 
| 99 | 
         
            +
                                return {
         
     | 
| 100 | 
         
            +
                                    "error": "This YouTube video has disabled transcripts. Consider these alternatives:",
         
     | 
| 101 | 
         
            +
                                    "alternatives": [
         
     | 
| 102 | 
         
            +
                                        "Please provide a different video with transcripts enabled",
         
     | 
| 103 | 
         
            +
                                        "Upload a local video file that you have permission to use",
         
     | 
| 104 | 
         
            +
                                        "Provide a text summary of the video content manually"
         
     | 
| 105 | 
         
            +
                                    ]
         
     | 
| 106 | 
         
            +
                                }
         
     | 
| 107 | 
         
            +
             
     | 
| 108 | 
         
            +
                        # For other tasks that require downloading
         
     | 
| 109 | 
         
            +
                        logger.info(f"YouTube URL detected: {file_path}. Attempting to access content...")
         
     | 
| 110 | 
         
            +
                        
         
     | 
| 111 | 
         
            +
                        # Try to get metadata about the video before downloading (title, etc.)
         
     | 
| 112 | 
         
            +
                        try:
         
     | 
| 113 | 
         
            +
                            with yt_dlp.YoutubeDL({'quiet': True, 'no_warnings': True}) as ydl:
         
     | 
| 114 | 
         
            +
                                info = ydl.extract_info(file_path, download=False)
         
     | 
| 115 | 
         
            +
                                video_title = info.get('title', 'Unknown')
         
     | 
| 116 | 
         
            +
                                logger.info(f"Video title: {video_title}")
         
     | 
| 117 | 
         
            +
                        except Exception as e:
         
     | 
| 118 | 
         
            +
                            # YouTube is likely blocking access
         
     | 
| 119 | 
         
            +
                            error_text = str(e).lower()
         
     | 
| 120 | 
         
            +
                            if any(term in error_text for term in ["forbidden", "403", "blocked", "bot", "captcha", "cookie"]):
         
     | 
| 121 | 
         
            +
                                return {
         
     | 
| 122 | 
         
            +
                                    "error": "YouTube access restricted. This agent cannot access this content due to platform restrictions.",
         
     | 
| 123 | 
         
            +
                                    "alternatives": [
         
     | 
| 124 | 
         
            +
                                        "Please upload a local video file instead",
         
     | 
| 125 | 
         
            +
                                        "For transcripts, try providing a text summary manually",
         
     | 
| 126 | 
         
            +
                                        "For visual analysis, consider uploading screenshots from the video"
         
     | 
| 127 | 
         
            +
                                    ]
         
     | 
| 128 | 
         
            +
                                }
         
     | 
| 129 | 
         
            +
                            return {"error": f"Failed to access video info: {str(e)}"}
         
     | 
| 130 | 
         
            +
                        
         
     | 
| 131 | 
         
            +
                        # Proceed with download attempt but with better handling
         
     | 
| 132 | 
         
             
                        download_resolution = task_parameters.get("resolution", "360p")
         
     | 
| 133 | 
         
             
                        download_result = self.download_video(file_path, resolution=download_resolution)
         
     | 
| 134 | 
         
            +
                        
         
     | 
| 135 | 
         
             
                        if download_result.get("error"):
         
     | 
| 136 | 
         
            +
                            error_text = download_result.get("error", "").lower()
         
     | 
| 137 | 
         
            +
                            if any(term in error_text for term in ["forbidden", "403", "blocked", "bot", "captcha", "cookie"]):
         
     | 
| 138 | 
         
            +
                                return {
         
     | 
| 139 | 
         
            +
                                    "error": "YouTube download restricted. This agent cannot download this content due to platform restrictions.",
         
     | 
| 140 | 
         
            +
                                    "alternatives": [
         
     | 
| 141 | 
         
            +
                                        "Please upload a local video file instead",
         
     | 
| 142 | 
         
            +
                                        "For transcripts, try obtaining them separately or summarizing manually",
         
     | 
| 143 | 
         
            +
                                        "For visual analysis, consider uploading key frames as images"
         
     | 
| 144 | 
         
            +
                                    ]
         
     | 
| 145 | 
         
            +
                                }
         
     | 
| 146 | 
         
             
                            return download_result
         
     | 
| 147 | 
         
            +
                            
         
     | 
| 148 | 
         
             
                        video_source_path = download_result.get("file_path")
         
     | 
| 149 | 
         
             
                        if not video_source_path or not os.path.exists(video_source_path):
         
     | 
| 150 | 
         
            +
                            return {"error": f"Failed to download or locate video from URL: {file_path}"}
         
     | 
| 151 | 
         | 
| 152 | 
         
             
                    elif file_path and not os.path.exists(file_path):
         
     | 
| 153 | 
         
             
                        return {"error": f"Video file not found: {file_path}"}
         
     | 
| 154 | 
         
             
                    elif not file_path and task not in ['get_transcript']: # transcript can work with URL directly
         
     | 
| 155 | 
         
            +
                        return {"error": "File path is required for this task."}
         
     | 
| 
         | 
|
| 156 | 
         | 
| 157 | 
         
            +
                    # Execute the appropriate task based on the request
         
     | 
| 158 | 
         
             
                    if task == "get_metadata":
         
     | 
| 159 | 
         
             
                        return self.get_video_metadata(video_source_path)
         
     | 
| 160 | 
         
             
                    elif task == "extract_frames":
         
     | 
| 
         | 
|
| 170 | 
         
             
                        confidence_threshold = task_parameters.get("confidence_threshold", 0.5)
         
     | 
| 171 | 
         
             
                        frames_to_process = task_parameters.get("frames_to_process", 5) # Process N frames
         
     | 
| 172 | 
         
             
                        return self.detect_objects_in_video(video_source_path, confidence_threshold=confidence_threshold, num_frames_to_sample=frames_to_process)
         
     | 
| 
         | 
|
| 173 | 
         
             
                    else:
         
     | 
| 174 | 
         
             
                        return {"error": f"Unsupported task: {task}"}
         
     | 
| 175 | 
         | 
| 
         | 
|
| 181 | 
         
             
                    return None
         
     | 
| 182 | 
         | 
| 183 | 
         
             
                def download_video(self, youtube_url, resolution="360p"):
         
     | 
| 184 | 
         
            +
                    """Download YouTube video for processing with improved error handling."""
         
     | 
| 185 | 
         
             
                    video_id = self._extract_video_id(youtube_url)
         
     | 
| 186 | 
         
             
                    if not video_id:
         
     | 
| 187 | 
         
             
                        return {"error": "Invalid YouTube URL or could not extract video ID."}
         
     | 
| 
         | 
|
| 193 | 
         
             
                        return {"success": True, "file_path": output_file_path, "message": "Video already downloaded."}
         
     | 
| 194 | 
         | 
| 195 | 
         
             
                    try:
         
     | 
| 196 | 
         
            +
                        # First try with default options
         
     | 
| 197 | 
         
             
                        ydl_opts = {
         
     | 
| 198 | 
         
             
                            'format': f'bestvideo[height<={resolution[:-1]}][ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
         
     | 
| 199 | 
         
             
                            'outtmpl': output_file_path,
         
     | 
| 
         | 
|
| 201 | 
         
             
                            'quiet': True,
         
     | 
| 202 | 
         
             
                            'no_warnings': True,
         
     | 
| 203 | 
         
             
                        }
         
     | 
| 204 | 
         
            +
                        
         
     | 
| 205 | 
         
            +
                        logger.info(f"Attempting to download YouTube video {video_id} at {resolution}...")
         
     | 
| 206 | 
         
            +
                        
         
     | 
| 207 | 
         
             
                        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
         
     | 
| 208 | 
         
             
                            ydl.download([youtube_url])
         
     | 
| 209 | 
         | 
| 210 | 
         
             
                        if not os.path.exists(output_file_path): # Check if download actually created the file
         
     | 
| 211 | 
         
            +
                            # Fallback for some formats if mp4 direct is not available
         
     | 
| 212 | 
         
            +
                            logger.info("Primary download method failed, trying alternative format...")
         
     | 
| 213 | 
         
             
                            ydl_opts['format'] = f'best[height<={resolution[:-1]}]' # more generic
         
     | 
| 214 | 
         
             
                            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
         
     | 
| 215 | 
         
             
                                info_dict = ydl.extract_info(youtube_url, download=True)
         
     | 
| 
         | 
|
| 218 | 
         
             
                                if downloaded_files:
         
     | 
| 219 | 
         
             
                                    actual_file_path = os.path.join(self.temp_dir, downloaded_files[0])
         
     | 
| 220 | 
         
             
                                    if actual_file_path != output_file_path and actual_file_path.endswith(('.mkv', '.webm', '.flv')):
         
     | 
| 221 | 
         
            +
                                        # Use the actual downloaded file
         
     | 
| 222 | 
         
            +
                                        output_file_path = actual_file_path
         
     | 
| 223 | 
         
            +
                                    elif not actual_file_path.endswith('.mp4'):
         
     | 
| 224 | 
         
            +
                                        return {"error": f"Downloaded video is not in a directly usable format: {downloaded_files[0]}"}
         
     | 
| 
         | 
|
| 
         | 
|
| 225 | 
         | 
| 226 | 
         
             
                        if os.path.exists(output_file_path):
         
     | 
| 227 | 
         
             
                            return {"success": True, "file_path": output_file_path}
         
     | 
| 
         | 
|
| 229 | 
         
             
                            return {"error": "Video download failed, file not found after attempt."}
         
     | 
| 230 | 
         | 
| 231 | 
         
             
                    except yt_dlp.utils.DownloadError as e:
         
     | 
| 232 | 
         
            +
                        error_msg = str(e)
         
     | 
| 233 | 
         
            +
                        if "Sign in to confirm your age" in error_msg:
         
     | 
| 234 | 
         
            +
                            return {"error": "Age-restricted video. Cannot download due to platform restrictions."}
         
     | 
| 235 | 
         
            +
                        elif "This video is private" in error_msg:
         
     | 
| 236 | 
         
            +
                            return {"error": "This video is private and cannot be accessed."}
         
     | 
| 237 | 
         
            +
                        elif any(term in error_msg.lower() for term in ["captcha", "bot", "cookie", "forbidden"]):
         
     | 
| 238 | 
         
            +
                            return {"error": f"YouTube access restricted due to bot detection. Consider uploading a local video file instead."}
         
     | 
| 239 | 
         
            +
                        return {"error": f"yt-dlp download error: {error_msg}"}
         
     | 
| 240 | 
         
             
                    except Exception as e:
         
     | 
| 241 | 
         
             
                        return {"error": f"Failed to download video: {str(e)}"}
         
     | 
| 242 | 
         |