Spaces:
Runtime error
Runtime error
tools.py
CHANGED
|
@@ -24,21 +24,161 @@ from langchain_community.document_loaders import YoutubeLoader
|
|
| 24 |
from langchain_community.document_loaders import AssemblyAIAudioTranscriptLoader
|
| 25 |
from langchain_community.document_loaders.assemblyai import TranscriptFormat
|
| 26 |
import assemblyai as aai
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
def
|
| 29 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
Args:
|
| 32 |
-
link:
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
YT_search_tool = Tool(
|
| 39 |
name="YT_search",
|
| 40 |
-
func=
|
| 41 |
-
description="
|
| 42 |
)
|
| 43 |
|
| 44 |
|
|
@@ -48,15 +188,52 @@ def audio_search(path: str) -> str:
|
|
| 48 |
Args:
|
| 49 |
path: The path to the file. If the file attached is just {name}.mp3, then it is local, and just use {name}.mp3. """
|
| 50 |
aai.settings.api_key = "945f7d6c00f142158322cd0c13901024"
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
{"text": doc.page_content, "metadata": doc.metadata}
|
| 57 |
-
for doc in raw_docs
|
| 58 |
]
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
audio_search_tool = Tool(
|
| 62 |
name="audio_search",
|
|
|
|
| 24 |
from langchain_community.document_loaders import AssemblyAIAudioTranscriptLoader
|
| 25 |
from langchain_community.document_loaders.assemblyai import TranscriptFormat
|
| 26 |
import assemblyai as aai
|
| 27 |
+
import tempfile
|
| 28 |
+
from typing import Dict, List
|
| 29 |
+
from langchain_community.document_loaders import GoogleApiClient, GoogleApiYoutubeLoader
|
| 30 |
|
| 31 |
+
def create_credentials_file(api_key: str) -> str:
|
| 32 |
+
"""
|
| 33 |
+
Create a temporary credentials file for Google API access.
|
| 34 |
+
|
| 35 |
+
This function creates the necessary credentials structure that Google's API expects.
|
| 36 |
+
In a production environment, you would typically have a proper service account
|
| 37 |
+
JSON file, but for this educational example, we'll create a minimal structure.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
api_key: "The API Key"
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
Path to the temporary credentials file
|
| 44 |
+
"""
|
| 45 |
+
# Create a minimal credentials structure
|
| 46 |
+
# Note: In production, you would use proper service account credentials
|
| 47 |
+
credentials_data = {
|
| 48 |
+
"type": "service_account",
|
| 49 |
+
"project_id": "your-project-id",
|
| 50 |
+
"private_key_id": "dummy-key-id",
|
| 51 |
+
"private_key": "dummy-private-key",
|
| 52 |
+
"client_email": "tu101@pnw.edu",
|
| 53 |
+
"client_id": "dummy-client-id",
|
| 54 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
| 55 |
+
"token_uri": "https://oauth2.googleapis.com/token",
|
| 56 |
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
| 57 |
+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/dummy%40your-project-id.iam.gserviceaccount.com"
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
# Create a temporary file to store credentials
|
| 61 |
+
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
|
| 62 |
+
json.dump(credentials_data, temp_file)
|
| 63 |
+
temp_file.close()
|
| 64 |
+
|
| 65 |
+
return temp_file.name
|
| 66 |
+
|
| 67 |
+
def extract_video_id_from_url(url: str) -> Optional[str]:
|
| 68 |
+
"""
|
| 69 |
+
Extract the video ID from a YouTube URL.
|
| 70 |
+
|
| 71 |
+
This function handles different YouTube URL formats and extracts the unique
|
| 72 |
+
video identifier that the API needs to fetch the video content.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
url: YouTube URL in various formats
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
Video ID string or None if URL is invalid
|
| 79 |
+
"""
|
| 80 |
+
import re
|
| 81 |
+
|
| 82 |
+
# Handle different YouTube URL formats
|
| 83 |
+
patterns = [
|
| 84 |
+
r'(?:https?://)?(?:www\.)?youtube\.com/watch\?v=([a-zA-Z0-9_-]+)',
|
| 85 |
+
r'(?:https?://)?(?:www\.)?youtu\.be/([a-zA-Z0-9_-]+)',
|
| 86 |
+
r'(?:https?://)?(?:www\.)?youtube\.com/embed/([a-zA-Z0-9_-]+)',
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
for pattern in patterns:
|
| 90 |
+
match = re.search(pattern, url)
|
| 91 |
+
if match:
|
| 92 |
+
return match.group(1)
|
| 93 |
+
|
| 94 |
+
return None
|
| 95 |
+
|
| 96 |
+
def youtube_search_google_api(link: str) -> Dict:
|
| 97 |
+
"""
|
| 98 |
+
Search YouTube using Google's official API and extract transcript/captions.
|
| 99 |
+
|
| 100 |
+
This function represents a more robust approach to YouTube content access.
|
| 101 |
+
Instead of web scraping, we use Google's official API endpoints, which
|
| 102 |
+
provide reliable access to video metadata and transcript data.
|
| 103 |
|
| 104 |
Args:
|
| 105 |
+
link: YouTube URL to process
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
Dictionary containing video documents or error information
|
| 109 |
+
"""
|
| 110 |
+
|
| 111 |
+
# Your Google Cloud API key
|
| 112 |
+
api_key = "AIzaSyA-1ZuYz-PYnnE9PQsWA7vOuJeRz-E2sbU"
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
# First, let's validate and extract the video ID
|
| 116 |
+
video_id = extract_video_id_from_url(link)
|
| 117 |
+
if not video_id:
|
| 118 |
+
return {
|
| 119 |
+
"YT_docs": [{
|
| 120 |
+
"text": "Invalid YouTube URL format. Please provide a valid YouTube link.",
|
| 121 |
+
"metadata": {"error": True, "url": link}
|
| 122 |
+
}]
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
# For this implementation, we'll use a simpler approach with the API key
|
| 126 |
+
# In a full production system, you would use proper service account credentials
|
| 127 |
+
|
| 128 |
+
# Alternative approach using youtube-transcript-api for transcript data
|
| 129 |
+
# This is often more reliable for transcript access
|
| 130 |
+
try:
|
| 131 |
+
from youtube_transcript_api import YouTubeTranscriptApi
|
| 132 |
+
|
| 133 |
+
# Get the transcript for the video
|
| 134 |
+
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
|
| 135 |
+
|
| 136 |
+
# Combine all transcript segments into a single text
|
| 137 |
+
full_transcript = ""
|
| 138 |
+
for segment in transcript_list:
|
| 139 |
+
full_transcript += segment['text'] + " "
|
| 140 |
+
|
| 141 |
+
# Create a document-like structure similar to other loaders
|
| 142 |
+
documents = [{
|
| 143 |
+
"text": full_transcript.strip(),
|
| 144 |
+
"metadata": {
|
| 145 |
+
"video_id": video_id,
|
| 146 |
+
"source": link,
|
| 147 |
+
"title": f"YouTube Video {video_id}",
|
| 148 |
+
"segments_count": len(transcript_list)
|
| 149 |
+
}
|
| 150 |
+
}]
|
| 151 |
+
|
| 152 |
+
return {"YT_docs": documents}
|
| 153 |
+
|
| 154 |
+
except Exception as transcript_error:
|
| 155 |
+
# If transcript API fails, provide helpful error information
|
| 156 |
+
error_msg = f"Could not retrieve transcript for video {video_id}. "
|
| 157 |
+
error_msg += "This video may not have captions available, may be private, "
|
| 158 |
+
error_msg += "or may have restricted access. "
|
| 159 |
+
error_msg += f"Original error: {str(transcript_error)}"
|
| 160 |
+
|
| 161 |
+
return {
|
| 162 |
+
"YT_docs": [{
|
| 163 |
+
"text": error_msg,
|
| 164 |
+
"metadata": {"error": True, "video_id": video_id}
|
| 165 |
+
}]
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
except Exception as e:
|
| 169 |
+
# Comprehensive error handling with debugging information
|
| 170 |
+
error_msg = f"Error processing YouTube video: {str(e)}"
|
| 171 |
+
return {
|
| 172 |
+
"YT_docs": [{
|
| 173 |
+
"text": error_msg,
|
| 174 |
+
"metadata": {"error": True, "url": link}
|
| 175 |
+
}]
|
| 176 |
+
}
|
| 177 |
|
| 178 |
YT_search_tool = Tool(
|
| 179 |
name="YT_search",
|
| 180 |
+
func=youtube_search_google_api,
|
| 181 |
+
description="Searches YouTube using Google's official services to extract video transcripts and captions. Handles various YouTube URL formats and provides detailed error information when content is not available."
|
| 182 |
)
|
| 183 |
|
| 184 |
|
|
|
|
| 188 |
Args:
|
| 189 |
path: The path to the file. If the file attached is just {name}.mp3, then it is local, and just use {name}.mp3. """
|
| 190 |
aai.settings.api_key = "945f7d6c00f142158322cd0c13901024"
|
| 191 |
+
possible_paths = [
|
| 192 |
+
path, # Try the path as-is first
|
| 193 |
+
os.path.join(".", path), # Try current directory
|
| 194 |
+
os.path.join("/tmp", path), # Try common temp directory
|
| 195 |
+
os.path.join("uploads", path), # Try uploads directory
|
|
|
|
|
|
|
| 196 |
]
|
| 197 |
+
|
| 198 |
+
# Find the actual file location
|
| 199 |
+
actual_path = None
|
| 200 |
+
for potential_path in possible_paths:
|
| 201 |
+
if os.path.exists(potential_path):
|
| 202 |
+
actual_path = potential_path
|
| 203 |
+
break
|
| 204 |
+
|
| 205 |
+
if actual_path is None:
|
| 206 |
+
# If we can't find the file, let's provide debugging information
|
| 207 |
+
current_dir_files = os.listdir(".")
|
| 208 |
+
error_msg = f"Could not find audio file. Searched in: {possible_paths}. Current directory contains: {current_dir_files[:10]}" # Limit output for readability
|
| 209 |
+
return {"audio_docs": [{"text": error_msg, "metadata": {"error": True}}]}
|
| 210 |
+
|
| 211 |
+
try:
|
| 212 |
+
# Use the found path
|
| 213 |
+
loader = AssemblyAIAudioTranscriptLoader(
|
| 214 |
+
file_path=actual_path,
|
| 215 |
+
transcript_format=TranscriptFormat.PARAGRAPHS
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
raw_docs = loader.load()
|
| 219 |
+
|
| 220 |
+
if not raw_docs:
|
| 221 |
+
return {"audio_docs": [{"text": "No audio content could be transcribed from this file.", "metadata": {"warning": True}}]}
|
| 222 |
+
|
| 223 |
+
# Convert each Document into a serializable dict
|
| 224 |
+
serialized_docs = [
|
| 225 |
+
{"text": doc.page_content, "metadata": doc.metadata}
|
| 226 |
+
for doc in raw_docs
|
| 227 |
+
]
|
| 228 |
+
|
| 229 |
+
return {"audio_docs": serialized_docs}
|
| 230 |
+
|
| 231 |
+
except Exception as e:
|
| 232 |
+
error_msg = f"Error processing audio file at {actual_path}: {str(e)}"
|
| 233 |
+
return {"audio_docs": [{"text": error_msg, "metadata": {"error": True}}]}
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
|
| 237 |
|
| 238 |
audio_search_tool = Tool(
|
| 239 |
name="audio_search",
|