ztoolx commited on
Commit
f5f6f29
·
verified ·
1 Parent(s): a4c9342
Files changed (1) hide show
  1. main.py +80 -35
main.py CHANGED
@@ -1,6 +1,6 @@
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
- from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled, NoTranscriptFound, VideoUnavailable
4
  import re
5
  from pydantic import BaseModel
6
 
@@ -15,7 +15,6 @@ app.add_middleware(
15
  allow_headers=["*"],
16
  )
17
 
18
- # Pydantic model to ensure we receive proper JSON data
19
  class TranscriptRequest(BaseModel):
20
  url: str
21
 
@@ -26,6 +25,17 @@ def extract_video_id(url: str) -> str:
26
  return match.group(1)
27
  return None
28
 
 
 
 
 
 
 
 
 
 
 
 
29
  @app.post("/api/transcript")
30
  async def get_transcript(req: TranscriptRequest):
31
  url = req.url
@@ -35,52 +45,87 @@ async def get_transcript(req: TranscriptRequest):
35
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
36
 
37
  try:
38
- print(f"Attempting to fetch transcript for video_id: {video_id}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- transcript_data = None
 
41
 
42
- # 1. Try fetching English transcript first
43
- try:
44
- print("Trying English transcript...")
45
- transcript_data = YouTubeTranscriptApi.get_transcript(video_id, languages=['en'])
46
- print("Found English transcript.")
47
- except NoTranscriptFound:
48
- print("No English transcript found. Trying any available language...")
49
- # 2. If no English, fetch the first available language automatically
50
- try:
51
- transcript_data = YouTubeTranscriptApi.get_transcript(video_id)
52
- print("Found fallback language transcript.")
53
- except Exception as e:
54
- print(f"Fallback failed: {e}")
55
- pass
56
 
57
- if not transcript_data:
 
 
 
 
 
 
 
 
58
  raise HTTPException(status_code=404, detail="No captions found for this video.")
59
-
60
- # Combine all text lines safely
61
- full_text = " ".join([line.get('text', '') for line in transcript_data])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
- # Clean up HTML entities
64
- full_text = full_text.replace("'", "'").replace(""", '"').replace("&", "&").replace("'", "'")
 
 
 
65
 
 
 
 
66
  print(f"Successfully fetched transcript. Length: {len(full_text)} chars")
67
  return {"text": full_text}
68
 
69
  except HTTPException:
70
- raise # Re-raise our custom HTTP exceptions
71
- except TranscriptsDisabled:
72
- raise HTTPException(status_code=403, detail="Captions are disabled for this video.")
73
- except VideoUnavailable:
74
- raise HTTPException(status_code=404, detail="Video is unavailable.")
75
  except Exception as e:
76
- # Catch-all: Log the exact error to Hugging Face logs so we can see what went wrong!
77
  error_name = type(e).__name__
78
  print(f"UNHANDLED ERROR for {video_id}: {error_name} - {str(e)}")
79
-
80
- # Check if it's a rate limit error dynamically
81
- if "TooManyRequests" in error_name or "429" in str(e):
82
- raise HTTPException(status_code=429, detail="YouTube is rate-limiting requests. Try again in a minute.")
83
-
84
  raise HTTPException(status_code=500, detail=f"Failed to fetch transcript: {error_name} - {str(e)}")
85
 
86
  # KEEP-ALIVE ENDPOINT
 
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
+ import requests
4
  import re
5
  from pydantic import BaseModel
6
 
 
15
  allow_headers=["*"],
16
  )
17
 
 
18
  class TranscriptRequest(BaseModel):
19
  url: str
20
 
 
25
  return match.group(1)
26
  return None
27
 
28
+ def parse_caption_xml(xml_string: str) -> str:
29
+ # Extract text content from the XML safely using regex
30
+ text_matches = re.findall(r'<text[^>]*>([^<]*)</text>', xml_string)
31
+ if not text_matches:
32
+ return ""
33
+
34
+ full_text = " ".join(text_matches)
35
+ # Clean up HTML entities
36
+ full_text = full_text.replace("&amp;#39;", "'").replace("&amp;quot;", '"').replace("&amp;amp;", "&").replace("&#39;", "'").replace("&lt;", "<").replace("&gt;", ">")
37
+ return full_text
38
+
39
  @app.post("/api/transcript")
40
  async def get_transcript(req: TranscriptRequest):
41
  url = req.url
 
45
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
46
 
47
  try:
48
+ print(f"Fetching InnerTube data for video_id: {video_id}")
49
+
50
+ # YouTube's internal API endpoint
51
+ innertube_url = "https://www.youtube.com/youtubei/v1/player?prettyPrint=false"
52
+
53
+ payload = {
54
+ "context": {
55
+ "client": {
56
+ "clientName": "WEB",
57
+ "clientVersion": "2.20241210.00.00",
58
+ "hl": "en",
59
+ "gl": "US"
60
+ }
61
+ },
62
+ "videoId": video_id
63
+ }
64
+
65
+ headers = {
66
+ "Content-Type": "application/json",
67
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
68
+ "Origin": "https://www.youtube.com",
69
+ "Referer": "https://www.youtube.com/"
70
+ }
71
 
72
+ # 1. Fetch the video player data
73
+ response = requests.post(innertube_url, json=payload, headers=headers, timeout=5)
74
 
75
+ if response.status_code != 200:
76
+ raise HTTPException(status_code=500, detail=f"YouTube API returned status {response.status_code}")
77
+
78
+ data = response.json()
 
 
 
 
 
 
 
 
 
 
79
 
80
+ # Check if video is playable
81
+ playability = data.get("playabilityStatus", {})
82
+ if playability.get("status") == "ERROR":
83
+ raise HTTPException(status_code=404, detail=playability.get("reason", "Video unavailable"))
84
+
85
+ # 2. Extract caption tracks
86
+ caption_tracks = data.get("captions", {}).get("playerCaptionsTracklistRenderer", {}).get("captionTracks", [])
87
+
88
+ if not caption_tracks:
89
  raise HTTPException(status_code=404, detail="No captions found for this video.")
90
+
91
+ # Prefer English, fallback to first available
92
+ selected_track = None
93
+ for track in caption_tracks:
94
+ if track.get("languageCode") == "en":
95
+ selected_track = track
96
+ break
97
+
98
+ if not selected_track:
99
+ selected_track = caption_tracks[0]
100
+
101
+ caption_url = selected_track.get("baseUrl")
102
+ if not caption_url:
103
+ raise HTTPException(status_code=404, detail="Caption URL not found.")
104
+
105
+ print(f"Fetching captions from URL...")
106
+
107
+ # 3. Fetch the actual caption XML
108
+ caption_response = requests.get(caption_url, headers={"User-Agent": headers["User-Agent"]}, timeout=5)
109
 
110
+ if caption_response.status_code != 200:
111
+ raise HTTPException(status_code=500, detail=f"Caption fetch failed with status {caption_response.status_code}")
112
+
113
+ # 4. Parse the XML into plain text
114
+ full_text = parse_caption_xml(caption_response.text)
115
 
116
+ if not full_text.strip():
117
+ raise HTTPException(status_code=404, detail="Caption XML was empty.")
118
+
119
  print(f"Successfully fetched transcript. Length: {len(full_text)} chars")
120
  return {"text": full_text}
121
 
122
  except HTTPException:
123
+ raise
124
+ except requests.exceptions.Timeout:
125
+ raise HTTPException(status_code=504, detail="Request to YouTube timed out.")
 
 
126
  except Exception as e:
 
127
  error_name = type(e).__name__
128
  print(f"UNHANDLED ERROR for {video_id}: {error_name} - {str(e)}")
 
 
 
 
 
129
  raise HTTPException(status_code=500, detail=f"Failed to fetch transcript: {error_name} - {str(e)}")
130
 
131
  # KEEP-ALIVE ENDPOINT