vivekchakraverty Claude Opus 4.8 commited on
Commit
ffdac8f
·
1 Parent(s): f07812e

Transcript: detect IP-block (SSL/EOF) + retry; expose module-level demo

Browse files

- youtube-transcript-api hits www.youtube.com, which datacenter IPs get blocked from
(TLS reset / SSL EOF). Retry transient connection failures and, on a clear block, the
error now tells the user to set the YT_PROXY (residential) secret. Fixed a substring
bug where "transcripts" matched the "ip" block keyword.
- app.py exposes module-level `demo` so HF's SSR launcher stops warning
"Launching demo not found in __main__".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +6 -1
  2. pipeline/transcribe.py +63 -39
app.py CHANGED
@@ -285,5 +285,10 @@ def build_ui():
285
  return demo
286
 
287
 
 
 
 
 
 
288
  if __name__ == "__main__":
289
- build_ui().queue().launch()
 
285
  return demo
286
 
287
 
288
+ # Expose a module-level `demo` so HF Spaces' SSR launcher finds it (avoids the
289
+ # "Launching demo not found in __main__" fallback warning).
290
+ demo = build_ui()
291
+ demo.queue()
292
+
293
  if __name__ == "__main__":
294
+ demo.launch()
pipeline/transcribe.py CHANGED
@@ -1,56 +1,77 @@
1
  """Stage 4: fetch a timestamped transcript via youtube-transcript-api (no download).
2
 
3
- youtube-transcript-api returns caption snippets that already carry ``start`` (and
4
- ``duration``) timestamps, so we just normalize them into ``{start, end, text}`` segments
5
- for the rest of the pipeline. No Whisper, no audio, no video.
 
 
6
  """
7
  from __future__ import annotations
8
 
 
 
9
 
10
  class TranscriptError(RuntimeError):
11
  """Raised when no usable transcript can be fetched (disabled, blocked, none)."""
12
 
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  def get_segments(video_id: str, languages=("en", "en-US", "en-GB"),
15
- proxy: str | None = None) -> list[dict]:
16
  """Return timestamped segments ``[{start, end, text}]`` for ``video_id``.
17
 
18
- Works across youtube-transcript-api 1.x (instance ``.fetch``) and older 0.6.x
19
- (classmethod ``.get_transcript``). ``proxy`` is an optional http(s) proxy URL used as
20
- a thin fallback when the Space's IP is blocked.
21
  """
22
- from youtube_transcript_api import YouTubeTranscriptApi
23
-
24
  langs = list(languages)
25
- raw = None
26
- try:
27
- # --- 1.x instance API ---
28
- api = None
29
- if proxy:
30
- try:
31
- from youtube_transcript_api.proxies import GenericProxyConfig
32
- api = YouTubeTranscriptApi(
33
- proxy_config=GenericProxyConfig(http_url=proxy, https_url=proxy))
34
- except Exception:
35
- api = YouTubeTranscriptApi()
36
- else:
37
- api = YouTubeTranscriptApi()
38
-
39
- if hasattr(api, "fetch"):
40
- fetched = api.fetch(video_id, languages=langs)
41
- raw = [{"text": s.text, "start": s.start, "duration": s.duration} for s in fetched]
42
- else:
43
- # --- 0.6.x classmethod API ---
44
- kwargs = {"languages": langs}
45
- if proxy:
46
- kwargs["proxies"] = {"http": proxy, "https": proxy}
47
- data = YouTubeTranscriptApi.get_transcript(video_id, **kwargs)
48
- raw = [{"text": d["text"], "start": d["start"], "duration": d.get("duration", 0)} for d in data]
49
- except Exception as exc:
50
- raise TranscriptError(_hint(exc)) from exc
51
 
52
  segs = []
53
- for r in raw or []:
54
  text = (r.get("text") or "").strip()
55
  if not text:
56
  continue
@@ -75,14 +96,17 @@ def _hint(exc: Exception) -> str:
75
  name = type(exc).__name__
76
  msg = str(exc)
77
  low = (name + " " + msg).lower()
 
78
  if "disabled" in low or "transcriptsdisabled" in low:
79
  return "This video has transcripts/captions disabled — pick another video."
80
  if "notranscript" in low or "no transcript" in low:
81
  return "No transcript is available for this video in the requested languages."
82
  if "unavailable" in low or "videounavailable" in low:
83
  return "The video is unavailable (private/removed/region-locked)."
84
- if "block" in low or "ip" in low or "too many" in low or "429" in low:
85
- return ("YouTube blocked the transcript request (common from datacenter IPs). "
86
- "Set a YT_PROXY Space secret (residential proxy) and retry. "
 
 
87
  f"[{name}]")
88
  return f"Could not fetch transcript: {name}: {msg[:200]}"
 
1
  """Stage 4: fetch a timestamped transcript via youtube-transcript-api (no download).
2
 
3
+ youtube-transcript-api scrapes ``www.youtube.com``, which datacenter IPs (like a Space)
4
+ often get blocked from usually as a TLS reset / SSL EOF rather than a clean 4xx. We
5
+ retry a few times for transient failures and, when it's clearly a block, point the user at
6
+ the ``YT_PROXY`` fallback. The returned snippets already carry ``start``/``duration``
7
+ timestamps, which we normalize into ``{start, end, text}`` segments.
8
  """
9
  from __future__ import annotations
10
 
11
+ import time
12
+
13
 
14
  class TranscriptError(RuntimeError):
15
  """Raised when no usable transcript can be fetched (disabled, blocked, none)."""
16
 
17
 
18
+ def _fetch_raw(video_id: str, langs: list[str], proxy: str | None) -> list[dict]:
19
+ from youtube_transcript_api import YouTubeTranscriptApi
20
+
21
+ # --- 1.x instance API ---
22
+ api = None
23
+ if proxy:
24
+ try:
25
+ from youtube_transcript_api.proxies import GenericProxyConfig
26
+ api = YouTubeTranscriptApi(
27
+ proxy_config=GenericProxyConfig(http_url=proxy, https_url=proxy))
28
+ except Exception:
29
+ api = YouTubeTranscriptApi()
30
+ else:
31
+ api = YouTubeTranscriptApi()
32
+
33
+ if hasattr(api, "fetch"):
34
+ fetched = api.fetch(video_id, languages=langs)
35
+ return [{"text": s.text, "start": s.start, "duration": s.duration} for s in fetched]
36
+
37
+ # --- 0.6.x classmethod API ---
38
+ kwargs = {"languages": langs}
39
+ if proxy:
40
+ kwargs["proxies"] = {"http": proxy, "https": proxy}
41
+ data = YouTubeTranscriptApi.get_transcript(video_id, **kwargs)
42
+ return [{"text": d["text"], "start": d["start"], "duration": d.get("duration", 0)} for d in data]
43
+
44
+
45
+ def _is_transient(exc: Exception) -> bool:
46
+ s = (type(exc).__name__ + " " + str(exc)).lower()
47
+ return any(k in s for k in ("ssl", "eof", "timed out", "timeout", "reset",
48
+ "connection", "max retries", "temporarily"))
49
+
50
+
51
  def get_segments(video_id: str, languages=("en", "en-US", "en-GB"),
52
+ proxy: str | None = None, attempts: int = 3) -> list[dict]:
53
  """Return timestamped segments ``[{start, end, text}]`` for ``video_id``.
54
 
55
+ Retries transient connection/TLS failures; raises TranscriptError otherwise.
56
+ ``proxy`` (the YT_PROXY secret) routes around datacenter-IP blocks.
 
57
  """
 
 
58
  langs = list(languages)
59
+ raw, last = None, None
60
+ for attempt in range(attempts):
61
+ try:
62
+ raw = _fetch_raw(video_id, langs, proxy)
63
+ break
64
+ except Exception as exc:
65
+ last = exc
66
+ if _is_transient(exc) and attempt < attempts - 1:
67
+ time.sleep(1.5 * (attempt + 1))
68
+ continue
69
+ raise TranscriptError(_hint(exc)) from exc
70
+ if raw is None:
71
+ raise TranscriptError(_hint(last) if last else "Unknown transcript error.")
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  segs = []
74
+ for r in raw:
75
  text = (r.get("text") or "").strip()
76
  if not text:
77
  continue
 
96
  name = type(exc).__name__
97
  msg = str(exc)
98
  low = (name + " " + msg).lower()
99
+ # Check specific transcript states first (note: "transcripts" contains "ip").
100
  if "disabled" in low or "transcriptsdisabled" in low:
101
  return "This video has transcripts/captions disabled — pick another video."
102
  if "notranscript" in low or "no transcript" in low:
103
  return "No transcript is available for this video in the requested languages."
104
  if "unavailable" in low or "videounavailable" in low:
105
  return "The video is unavailable (private/removed/region-locked)."
106
+ if any(k in low for k in ("ssl", "eof", "reset", "connection", "max retries",
107
+ "timed out", "blocked", "forbidden", "too many", "429")):
108
+ return ("YouTube blocked the transcript request from this Space's IP "
109
+ "(datacenter IPs are commonly blocked — seen here as a TLS/SSL reset). "
110
+ "Set a residential proxy as the YT_PROXY Space secret and retry. "
111
  f"[{name}]")
112
  return f"Could not fetch transcript: {name}: {msg[:200]}"