webscout 6.3__py3-none-any.whl → 6.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of webscout might be problematic. Click here for more details.

Files changed (131) hide show
  1. webscout/AIauto.py +191 -176
  2. webscout/AIbase.py +0 -197
  3. webscout/AIutel.py +441 -1130
  4. webscout/DWEBS.py +189 -35
  5. webscout/{YTdownloader.py → Extra/YTToolkit/YTdownloader.py} +990 -1103
  6. webscout/Extra/YTToolkit/__init__.py +3 -0
  7. webscout/{transcriber.py → Extra/YTToolkit/transcriber.py} +479 -551
  8. webscout/Extra/YTToolkit/ytapi/__init__.py +6 -0
  9. webscout/Extra/YTToolkit/ytapi/channel.py +307 -0
  10. webscout/Extra/YTToolkit/ytapi/errors.py +13 -0
  11. webscout/Extra/YTToolkit/ytapi/extras.py +45 -0
  12. webscout/Extra/YTToolkit/ytapi/https.py +88 -0
  13. webscout/Extra/YTToolkit/ytapi/patterns.py +61 -0
  14. webscout/Extra/YTToolkit/ytapi/playlist.py +59 -0
  15. webscout/Extra/YTToolkit/ytapi/pool.py +8 -0
  16. webscout/Extra/YTToolkit/ytapi/query.py +37 -0
  17. webscout/Extra/YTToolkit/ytapi/stream.py +60 -0
  18. webscout/Extra/YTToolkit/ytapi/utils.py +62 -0
  19. webscout/Extra/YTToolkit/ytapi/video.py +102 -0
  20. webscout/Extra/__init__.py +3 -1
  21. webscout/Extra/autocoder/__init__.py +9 -0
  22. webscout/Extra/autocoder/autocoder_utiles.py +121 -0
  23. webscout/Extra/autocoder/rawdog.py +680 -0
  24. webscout/Extra/autollama.py +246 -195
  25. webscout/Extra/gguf.py +81 -56
  26. webscout/Extra/markdownlite/__init__.py +862 -0
  27. webscout/Extra/weather_ascii.py +2 -2
  28. webscout/LLM.py +206 -43
  29. webscout/Litlogger/__init__.py +681 -0
  30. webscout/Provider/DARKAI.py +1 -1
  31. webscout/Provider/EDITEE.py +1 -1
  32. webscout/Provider/NinjaChat.py +1 -1
  33. webscout/Provider/PI.py +120 -35
  34. webscout/Provider/Perplexity.py +590 -598
  35. webscout/Provider/Reka.py +0 -1
  36. webscout/Provider/RoboCoders.py +206 -0
  37. webscout/Provider/TTI/AiForce/__init__.py +22 -0
  38. webscout/Provider/TTI/AiForce/async_aiforce.py +257 -0
  39. webscout/Provider/TTI/AiForce/sync_aiforce.py +242 -0
  40. webscout/Provider/TTI/Nexra/__init__.py +22 -0
  41. webscout/Provider/TTI/Nexra/async_nexra.py +286 -0
  42. webscout/Provider/TTI/Nexra/sync_nexra.py +258 -0
  43. webscout/Provider/TTI/PollinationsAI/__init__.py +23 -0
  44. webscout/Provider/TTI/PollinationsAI/async_pollinations.py +330 -0
  45. webscout/Provider/TTI/PollinationsAI/sync_pollinations.py +285 -0
  46. webscout/Provider/TTI/__init__.py +2 -4
  47. webscout/Provider/TTI/artbit/__init__.py +22 -0
  48. webscout/Provider/TTI/artbit/async_artbit.py +184 -0
  49. webscout/Provider/TTI/artbit/sync_artbit.py +176 -0
  50. webscout/Provider/TTI/blackbox/__init__.py +4 -0
  51. webscout/Provider/TTI/blackbox/async_blackbox.py +212 -0
  52. webscout/Provider/TTI/{blackboximage.py → blackbox/sync_blackbox.py} +199 -153
  53. webscout/Provider/TTI/deepinfra/__init__.py +4 -0
  54. webscout/Provider/TTI/deepinfra/async_deepinfra.py +227 -0
  55. webscout/Provider/TTI/deepinfra/sync_deepinfra.py +199 -0
  56. webscout/Provider/TTI/huggingface/__init__.py +22 -0
  57. webscout/Provider/TTI/huggingface/async_huggingface.py +199 -0
  58. webscout/Provider/TTI/huggingface/sync_huggingface.py +195 -0
  59. webscout/Provider/TTI/imgninza/__init__.py +4 -0
  60. webscout/Provider/TTI/imgninza/async_ninza.py +214 -0
  61. webscout/Provider/TTI/{imgninza.py → imgninza/sync_ninza.py} +209 -136
  62. webscout/Provider/TTI/talkai/__init__.py +4 -0
  63. webscout/Provider/TTI/talkai/async_talkai.py +229 -0
  64. webscout/Provider/TTI/talkai/sync_talkai.py +207 -0
  65. webscout/Provider/TTS/__init__.py +5 -1
  66. webscout/Provider/TTS/deepgram.py +183 -0
  67. webscout/Provider/TTS/elevenlabs.py +137 -0
  68. webscout/Provider/TTS/gesserit.py +151 -0
  69. webscout/Provider/TTS/murfai.py +139 -0
  70. webscout/Provider/TTS/parler.py +134 -107
  71. webscout/Provider/TTS/streamElements.py +360 -275
  72. webscout/Provider/TTS/utils.py +280 -0
  73. webscout/Provider/TTS/voicepod.py +116 -116
  74. webscout/Provider/__init__.py +8 -1
  75. webscout/Provider/askmyai.py +2 -2
  76. webscout/Provider/cerebras.py +227 -219
  77. webscout/Provider/llama3mitril.py +0 -1
  78. webscout/Provider/meta.py +794 -779
  79. webscout/Provider/mhystical.py +176 -0
  80. webscout/Provider/perplexitylabs.py +265 -0
  81. webscout/Provider/twitterclone.py +251 -245
  82. webscout/Provider/typegpt.py +358 -0
  83. webscout/__init__.py +9 -8
  84. webscout/__main__.py +5 -5
  85. webscout/cli.py +252 -280
  86. webscout/conversation.py +227 -0
  87. webscout/exceptions.py +161 -29
  88. webscout/litagent/__init__.py +172 -0
  89. webscout/litprinter/__init__.py +832 -0
  90. webscout/optimizers.py +270 -0
  91. webscout/prompt_manager.py +279 -0
  92. webscout/scout/__init__.py +11 -0
  93. webscout/scout/core.py +884 -0
  94. webscout/scout/element.py +459 -0
  95. webscout/scout/parsers/__init__.py +69 -0
  96. webscout/scout/parsers/html5lib_parser.py +172 -0
  97. webscout/scout/parsers/html_parser.py +236 -0
  98. webscout/scout/parsers/lxml_parser.py +178 -0
  99. webscout/scout/utils.py +38 -0
  100. webscout/swiftcli/__init__.py +810 -0
  101. webscout/update_checker.py +125 -0
  102. webscout/version.py +1 -1
  103. webscout/zeroart/__init__.py +55 -0
  104. webscout/zeroart/base.py +61 -0
  105. webscout/zeroart/effects.py +99 -0
  106. webscout/zeroart/fonts.py +816 -0
  107. webscout/zerodir/__init__.py +225 -0
  108. {webscout-6.3.dist-info → webscout-6.5.dist-info}/METADATA +37 -112
  109. webscout-6.5.dist-info/RECORD +179 -0
  110. webscout/Agents/Onlinesearcher.py +0 -182
  111. webscout/Agents/__init__.py +0 -2
  112. webscout/Agents/functioncall.py +0 -248
  113. webscout/Bing_search.py +0 -154
  114. webscout/Provider/TTI/AIuncensoredimage.py +0 -103
  115. webscout/Provider/TTI/Nexra.py +0 -120
  116. webscout/Provider/TTI/PollinationsAI.py +0 -138
  117. webscout/Provider/TTI/WebSimAI.py +0 -142
  118. webscout/Provider/TTI/aiforce.py +0 -160
  119. webscout/Provider/TTI/artbit.py +0 -141
  120. webscout/Provider/TTI/deepinfra.py +0 -148
  121. webscout/Provider/TTI/huggingface.py +0 -155
  122. webscout/Provider/TTI/talkai.py +0 -116
  123. webscout/g4f.py +0 -666
  124. webscout/models.py +0 -23
  125. webscout/requestsHTMLfix.py +0 -775
  126. webscout/webai.py +0 -2590
  127. webscout-6.3.dist-info/RECORD +0 -124
  128. {webscout-6.3.dist-info → webscout-6.5.dist-info}/LICENSE.md +0 -0
  129. {webscout-6.3.dist-info → webscout-6.5.dist-info}/WHEEL +0 -0
  130. {webscout-6.3.dist-info → webscout-6.5.dist-info}/entry_points.txt +0 -0
  131. {webscout-6.3.dist-info → webscout-6.5.dist-info}/top_level.txt +0 -0
@@ -1,552 +1,480 @@
1
- import requests
2
- import http.cookiejar as cookiejar
3
- import json
4
- from xml.etree import ElementTree
5
- import re
6
- import html.parser
7
- from typing import List, Dict, Union, Optional
8
-
9
- html_parser = html.parser.HTMLParser()
10
-
11
-
12
- def unescape(string):
13
- return html.unescape(string)
14
-
15
-
16
- WATCH_URL = 'https://www.youtube.com/watch?v={video_id}'
17
-
18
-
19
- class TranscriptRetrievalError(Exception):
20
- """Base class for transcript retrieval errors."""
21
-
22
- def __init__(self, video_id, message):
23
- super().__init__(message.format(video_url=WATCH_URL.format(video_id=video_id)))
24
- self.video_id = video_id
25
-
26
-
27
- class YouTubeRequestFailedError(TranscriptRetrievalError):
28
- """Raised when a request to YouTube fails."""
29
-
30
- def __init__(self, video_id, http_error):
31
- message = 'Request to YouTube failed: {reason}'
32
- super().__init__(video_id, message.format(reason=str(http_error)))
33
-
34
-
35
- class VideoUnavailableError(TranscriptRetrievalError):
36
- """Raised when the video is unavailable."""
37
-
38
- def __init__(self, video_id):
39
- message = 'The video is no longer available'
40
- super().__init__(video_id, message)
41
-
42
-
43
- class InvalidVideoIdError(TranscriptRetrievalError):
44
- """Raised when an invalid video ID is provided."""
45
-
46
- def __init__(self, video_id):
47
- message = (
48
- 'You provided an invalid video id. Make sure you are using the video id and NOT the url!\n\n'
49
- 'Do NOT run: `YTTranscriber.get_transcript("https://www.youtube.com/watch?v=1234")`\n'
50
- 'Instead run: `YTTranscriber.get_transcript("1234")`'
51
- )
52
- super().__init__(video_id, message)
53
-
54
-
55
- class TooManyRequestsError(TranscriptRetrievalError):
56
- """Raised when YouTube rate limits the requests."""
57
-
58
- def __init__(self, video_id):
59
- message = (
60
- 'YouTube is receiving too many requests from this IP and now requires solving a captcha to continue. '
61
- 'One of the following things can be done to work around this:\n\
62
- - Manually solve the captcha in a browser and export the cookie. '
63
- '- Use a different IP address\n\
64
- - Wait until the ban on your IP has been lifted'
65
- )
66
- super().__init__(video_id, message)
67
-
68
-
69
- class TranscriptsDisabledError(TranscriptRetrievalError):
70
- """Raised when transcripts are disabled for the video."""
71
-
72
- def __init__(self, video_id):
73
- message = 'Subtitles are disabled for this video'
74
- super().__init__(video_id, message)
75
-
76
-
77
- class NoTranscriptAvailableError(TranscriptRetrievalError):
78
- """Raised when no transcripts are available for the video."""
79
-
80
- def __init__(self, video_id):
81
- message = 'No transcripts are available for this video'
82
- super().__init__(video_id, message)
83
-
84
-
85
- class NotTranslatableError(TranscriptRetrievalError):
86
- """Raised when the transcript is not translatable."""
87
-
88
- def __init__(self, video_id):
89
- message = 'The requested language is not translatable'
90
- super().__init__(video_id, message)
91
-
92
-
93
- class TranslationLanguageNotAvailableError(TranscriptRetrievalError):
94
- """Raised when the requested translation language is not available."""
95
-
96
- def __init__(self, video_id):
97
- message = 'The requested translation language is not available'
98
- super().__init__(video_id, message)
99
-
100
-
101
- class CookiePathInvalidError(TranscriptRetrievalError):
102
- """Raised when the cookie path is invalid."""
103
-
104
- def __init__(self, video_id):
105
- message = 'The provided cookie file was unable to be loaded'
106
- super().__init__(video_id, message)
107
-
108
-
109
- class CookiesInvalidError(TranscriptRetrievalError):
110
- """Raised when the provided cookies are invalid."""
111
-
112
- def __init__(self, video_id):
113
- message = 'The cookies provided are not valid (may have expired)'
114
- super().__init__(video_id, message)
115
-
116
-
117
- class FailedToCreateConsentCookieError(TranscriptRetrievalError):
118
- """Raised when consent cookie creation fails."""
119
-
120
- def __init__(self, video_id):
121
- message = 'Failed to automatically give consent to saving cookies'
122
- super().__init__(video_id, message)
123
-
124
-
125
- class NoTranscriptFoundError(TranscriptRetrievalError):
126
- """Raised when no transcript is found for the requested language codes."""
127
-
128
- def __init__(self, video_id, requested_language_codes, transcript_data):
129
- message = (
130
- 'No transcripts were found for any of the requested language codes: {requested_language_codes}\n\n'
131
- '{transcript_data}'
132
- )
133
- super().__init__(video_id, message.format(
134
- requested_language_codes=requested_language_codes,
135
- transcript_data=str(transcript_data)
136
- ))
137
-
138
-
139
- class YTTranscriber:
140
- """
141
- Main class for retrieving YouTube transcripts.
142
- """
143
-
144
- @staticmethod
145
- def get_transcript(video_url: str, languages: Optional[str] = 'en',
146
- proxies: Dict[str, str] = None,
147
- cookies: str = None,
148
- preserve_formatting: bool = False) -> List[Dict[str, Union[str, float]]]:
149
- """
150
- Retrieves the transcript for a given YouTube video URL.
151
-
152
- Args:
153
- video_url (str): YouTube video URL (supports various formats).
154
- languages (str, optional): Language code for the transcript.
155
- If None, fetches the auto-generated transcript.
156
- Defaults to 'en'.
157
- proxies (Dict[str, str], optional): Proxies to use for the request. Defaults to None.
158
- cookies (str, optional): Path to the cookie file. Defaults to None.
159
- preserve_formatting (bool, optional): Whether to preserve formatting tags. Defaults to False.
160
-
161
- Returns:
162
- List[Dict[str, Union[str, float]]]: A list of dictionaries, each containing:
163
- - 'text': The transcribed text.
164
- - 'start': The start time of the text segment (in seconds).
165
- - 'duration': The duration of the text segment (in seconds).
166
-
167
- Raises:
168
- TranscriptRetrievalError: If there's an error retrieving the transcript.
169
- """
170
- video_id = YTTranscriber._extract_video_id(video_url)
171
-
172
- with requests.Session() as http_client:
173
- if cookies:
174
- http_client.cookies = YTTranscriber._load_cookies(cookies, video_id)
175
- http_client.proxies = proxies if proxies else {}
176
- transcript_list_fetcher = TranscriptListFetcher(http_client)
177
- transcript_list = transcript_list_fetcher.fetch(video_id)
178
-
179
- if languages is None: # Get auto-generated transcript
180
- return transcript_list.find_generated_transcript(['any']).fetch(
181
- preserve_formatting=preserve_formatting)
182
- else:
183
- return transcript_list.find_transcript([languages]).fetch(preserve_formatting=preserve_formatting)
184
-
185
- @staticmethod
186
- def _extract_video_id(video_url: str) -> str:
187
- """Extracts the video ID from different YouTube URL formats."""
188
- if 'youtube.com/watch?v=' in video_url:
189
- video_id = video_url.split('youtube.com/watch?v=')[1].split('&')[0]
190
- elif 'youtu.be/' in video_url:
191
- video_id = video_url.split('youtu.be/')[1].split('?')[0]
192
- else:
193
- raise InvalidVideoIdError(video_url)
194
- return video_id
195
-
196
- @staticmethod
197
- def _load_cookies(cookies: str, video_id: str) -> cookiejar.MozillaCookieJar:
198
- """Loads cookies from a file."""
199
- try:
200
- cookie_jar = cookiejar.MozillaCookieJar()
201
- cookie_jar.load(cookies)
202
- if not cookie_jar:
203
- raise CookiesInvalidError(video_id)
204
- return cookie_jar
205
- except:
206
- raise CookiePathInvalidError(video_id)
207
-
208
-
209
- class TranscriptListFetcher:
210
- """Fetches the list of transcripts for a YouTube video."""
211
-
212
- def __init__(self, http_client: requests.Session):
213
- """Initializes TranscriptListFetcher."""
214
- self._http_client = http_client
215
-
216
- def fetch(self, video_id: str):
217
- """Fetches and returns a TranscriptList."""
218
- return TranscriptList.build(
219
- self._http_client,
220
- video_id,
221
- self._extract_captions_json(self._fetch_video_html(video_id), video_id),
222
- )
223
-
224
- def _extract_captions_json(self, html: str, video_id: str) -> dict:
225
- """Extracts the captions JSON data from the video's HTML."""
226
- splitted_html = html.split('"captions":')
227
-
228
- if len(splitted_html) <= 1:
229
- if video_id.startswith('http://') or video_id.startswith('https://'):
230
- raise InvalidVideoIdError(video_id)
231
- if 'class="g-recaptcha"' in html:
232
- raise TooManyRequestsError(video_id)
233
- if '"playabilityStatus":' not in html:
234
- raise VideoUnavailableError(video_id)
235
-
236
- raise TranscriptsDisabledError(video_id)
237
-
238
- captions_json = json.loads(
239
- splitted_html[1].split(',"videoDetails')[0].replace('\n', '')
240
- ).get('playerCaptionsTracklistRenderer')
241
- if captions_json is None:
242
- raise TranscriptsDisabledError(video_id)
243
-
244
- if 'captionTracks' not in captions_json:
245
- raise TranscriptsDisabledError(video_id)
246
-
247
- return captions_json
248
-
249
- def _create_consent_cookie(self, html, video_id):
250
- match = re.search('name="v" value="(.*?)"', html)
251
- if match is None:
252
- raise FailedToCreateConsentCookieError(video_id)
253
- self._http_client.cookies.set('CONSENT', 'YES+' + match.group(1), domain='.youtube.com')
254
-
255
- def _fetch_video_html(self, video_id):
256
- html = self._fetch_html(video_id)
257
- if 'action="https://consent.youtube.com/s"' in html:
258
- self._create_consent_cookie(html, video_id)
259
- html = self._fetch_html(video_id)
260
- if 'action="https://consent.youtube.com/s"' in html:
261
- raise FailedToCreateConsentCookieError(video_id)
262
- return html
263
-
264
- def _fetch_html(self, video_id):
265
- response = self._http_client.get(WATCH_URL.format(video_id=video_id), headers={'Accept-Language': 'en-US'})
266
- return unescape(_raise_http_errors(response, video_id).text)
267
-
268
-
269
- class TranscriptList:
270
- """Represents a list of available transcripts."""
271
-
272
- def __init__(self, video_id, manually_created_transcripts, generated_transcripts, translation_languages):
273
- """
274
- The constructor is only for internal use. Use the static build method instead.
275
-
276
- :param video_id: the id of the video this TranscriptList is for
277
- :type video_id: str
278
- :param manually_created_transcripts: dict mapping language codes to the manually created transcripts
279
- :type manually_created_transcripts: dict[str, Transcript]
280
- :param generated_transcripts: dict mapping language codes to the generated transcripts
281
- :type generated_transcripts: dict[str, Transcript]
282
- :param translation_languages: list of languages which can be used for translatable languages
283
- :type translation_languages: list[dict[str, str]]
284
- """
285
- self.video_id = video_id
286
- self._manually_created_transcripts = manually_created_transcripts
287
- self._generated_transcripts = generated_transcripts
288
- self._translation_languages = translation_languages
289
-
290
- @staticmethod
291
- def build(http_client, video_id, captions_json):
292
- """
293
- Factory method for TranscriptList.
294
-
295
- :param http_client: http client which is used to make the transcript retrieving http calls
296
- :type http_client: requests.Session
297
- :param video_id: the id of the video this TranscriptList is for
298
- :type video_id: str
299
- :param captions_json: the JSON parsed from the YouTube pages static HTML
300
- :type captions_json: dict
301
- :return: the created TranscriptList
302
- :rtype TranscriptList:
303
- """
304
- translation_languages = [
305
- {
306
- 'language': translation_language['languageName']['simpleText'],
307
- 'language_code': translation_language['languageCode'],
308
- } for translation_language in captions_json.get('translationLanguages', [])
309
- ]
310
-
311
- manually_created_transcripts = {}
312
- generated_transcripts = {}
313
-
314
- for caption in captions_json['captionTracks']:
315
- if caption.get('kind', '') == 'asr':
316
- transcript_dict = generated_transcripts
317
- else:
318
- transcript_dict = manually_created_transcripts
319
-
320
- transcript_dict[caption['languageCode']] = Transcript(
321
- http_client,
322
- video_id,
323
- caption['baseUrl'],
324
- caption['name']['simpleText'],
325
- caption['languageCode'],
326
- caption.get('kind', '') == 'asr',
327
- translation_languages if caption.get('isTranslatable', False) else [],
328
- )
329
-
330
- return TranscriptList(
331
- video_id,
332
- manually_created_transcripts,
333
- generated_transcripts,
334
- translation_languages,
335
- )
336
-
337
- def __iter__(self):
338
- return iter(list(self._manually_created_transcripts.values()) + list(self._generated_transcripts.values()))
339
-
340
- def find_transcript(self, language_codes):
341
- """
342
- Finds a transcript for a given language code. If no language is provided, it will
343
- return the auto-generated transcript.
344
-
345
- :param language_codes: A list of language codes in a descending priority.
346
- :type languages: list[str]
347
- :return: the found Transcript
348
- :rtype Transcript:
349
- :raises: NoTranscriptFound
350
- """
351
- if 'any' in language_codes:
352
- for transcript in self:
353
- return transcript
354
- return self._find_transcript(language_codes, [self._manually_created_transcripts, self._generated_transcripts])
355
-
356
- def find_generated_transcript(self, language_codes):
357
- """
358
- Finds an automatically generated transcript for a given language code.
359
-
360
- :param language_codes: A list of language codes in a descending priority. For example, if this is set to
361
- ['de', 'en'] it will first try to fetch the german transcript (de) and then fetch the english transcript (en) if
362
- it fails to do so.
363
- :type languages: list[str]
364
- :return: the found Transcript
365
- :rtype Transcript:
366
- :raises: NoTranscriptFound
367
- """
368
- if 'any' in language_codes:
369
- for transcript in self:
370
- if transcript.is_generated:
371
- return transcript
372
- return self._find_transcript(language_codes, [self._generated_transcripts])
373
-
374
- def find_manually_created_transcript(self, language_codes):
375
- """
376
- Finds a manually created transcript for a given language code.
377
-
378
- :param language_codes: A list of language codes in a descending priority. For example, if this is set to
379
- ['de', 'en'] it will first try to fetch the german transcript (de) and then fetch the english transcript (en) if
380
- it fails to do so.
381
- :type languages: list[str]
382
- :return: the found Transcript
383
- :rtype Transcript:
384
- :raises: NoTranscriptFound
385
- """
386
- return self._find_transcript(language_codes, [self._manually_created_transcripts])
387
-
388
- def _find_transcript(self, language_codes, transcript_dicts):
389
- for language_code in language_codes:
390
- for transcript_dict in transcript_dicts:
391
- if language_code in transcript_dict:
392
- return transcript_dict[language_code]
393
-
394
- raise NoTranscriptFoundError(
395
- self.video_id,
396
- language_codes,
397
- self
398
- )
399
-
400
- def __str__(self):
401
- return (
402
- 'For this video ({video_id}) transcripts are available in the following languages:\n\n'
403
- '(MANUALLY CREATED)\n'
404
- '{available_manually_created_transcript_languages}\n\n'
405
- '(GENERATED)\n'
406
- '{available_generated_transcripts}\n\n'
407
- '(TRANSLATION LANGUAGES)\n'
408
- '{available_translation_languages}'
409
- ).format(
410
- video_id=self.video_id,
411
- available_manually_created_transcript_languages=self._get_language_description(
412
- str(transcript) for transcript in self._manually_created_transcripts.values()
413
- ),
414
- available_generated_transcripts=self._get_language_description(
415
- str(transcript) for transcript in self._generated_transcripts.values()
416
- ),
417
- available_translation_languages=self._get_language_description(
418
- '{language_code} ("{language}")'.format(
419
- language=translation_language['language'],
420
- language_code=translation_language['language_code'],
421
- ) for translation_language in self._translation_languages
422
- )
423
- )
424
-
425
- def _get_language_description(self, transcript_strings):
426
- description = '\n'.join(' - {transcript}'.format(transcript=transcript) for transcript in transcript_strings)
427
- return description if description else 'None'
428
-
429
-
430
- class Transcript:
431
- """Represents a single transcript."""
432
-
433
- def __init__(self, http_client, video_id, url, language, language_code, is_generated, translation_languages):
434
- """
435
- You probably don't want to initialize this directly. Usually you'll access Transcript objects using a
436
- TranscriptList.
437
-
438
- :param http_client: http client which is used to make the transcript retrieving http calls
439
- :type http_client: requests.Session
440
- :param video_id: the id of the video this TranscriptList is for
441
- :type video_id: str
442
- :param url: the url which needs to be called to fetch the transcript
443
- :param language: the name of the language this transcript uses
444
- :param language_code:
445
- :param is_generated:
446
- :param translation_languages:
447
- """
448
- self._http_client = http_client
449
- self.video_id = video_id
450
- self._url = url
451
- self.language = language
452
- self.language_code = language_code
453
- self.is_generated = is_generated
454
- self.translation_languages = translation_languages
455
- self._translation_languages_dict = {
456
- translation_language['language_code']: translation_language['language']
457
- for translation_language in translation_languages
458
- }
459
-
460
- def fetch(self, preserve_formatting=False):
461
- """
462
- Loads the actual transcript data.
463
- :param preserve_formatting: whether to keep select HTML text formatting
464
- :type preserve_formatting: bool
465
- :return: a list of dictionaries containing the 'text', 'start' and 'duration' keys
466
- :rtype [{'text': str, 'start': float, 'end': float}]:
467
- """
468
- response = self._http_client.get(self._url, headers={'Accept-Language': 'en-US'})
469
- return TranscriptParser(preserve_formatting=preserve_formatting).parse(
470
- _raise_http_errors(response, self.video_id).text,
471
- )
472
-
473
- def __str__(self):
474
- return '{language_code} ("{language}"){translation_description}'.format(
475
- language=self.language,
476
- language_code=self.language_code,
477
- translation_description='[TRANSLATABLE]' if self.is_translatable else ''
478
- )
479
-
480
- @property
481
- def is_translatable(self):
482
- return len(self.translation_languages) > 0
483
-
484
- def translate(self, language_code):
485
- if not self.is_translatable:
486
- raise NotTranslatableError(self.video_id)
487
-
488
- if language_code not in self._translation_languages_dict:
489
- raise TranslationLanguageNotAvailableError(self.video_id)
490
-
491
- return Transcript(
492
- self._http_client,
493
- self.video_id,
494
- '{url}&tlang={language_code}'.format(url=self._url, language_code=language_code),
495
- self._translation_languages_dict[language_code],
496
- language_code,
497
- True,
498
- [],
499
- )
500
-
501
-
502
- class TranscriptParser:
503
- """Parses the transcript data from XML."""
504
- _FORMATTING_TAGS = [
505
- 'strong', # important
506
- 'em', # emphasized
507
- 'b', # bold
508
- 'i', # italic
509
- 'mark', # marked
510
- 'small', # smaller
511
- 'del', # deleted
512
- 'ins', # inserted
513
- 'sub', # subscript
514
- 'sup', # superscript
515
- ]
516
-
517
- def __init__(self, preserve_formatting=False):
518
- self._html_regex = self._get_html_regex(preserve_formatting)
519
-
520
- def _get_html_regex(self, preserve_formatting):
521
- if preserve_formatting:
522
- formats_regex = '|'.join(self._FORMATTING_TAGS)
523
- formats_regex = r'<\/?(?!\/?(' + formats_regex + r')\b).*?\b>'
524
- html_regex = re.compile(formats_regex, re.IGNORECASE)
525
- else:
526
- html_regex = re.compile(r'<[^>]*>', re.IGNORECASE)
527
- return html_regex
528
-
529
- def parse(self, plain_data):
530
- return [
531
- {
532
- 'text': re.sub(self._html_regex, '', unescape(xml_element.text)),
533
- 'start': float(xml_element.attrib['start']),
534
- 'duration': float(xml_element.attrib.get('dur', '0.0')),
535
- }
536
- for xml_element in ElementTree.fromstring(plain_data)
537
- if xml_element.text is not None
538
- ]
539
-
540
-
541
- def _raise_http_errors(response, video_id):
542
- try:
543
- response.raise_for_status()
544
- return response
545
- except requests.exceptions.HTTPError as error:
546
- raise YouTubeRequestFailedError(video_id, error)
547
-
548
- if __name__ == "__main__":
549
- from rich import print
550
- video_url = input("Enter the YouTube video URL: ")
551
- transcript = YTTranscriber.get_transcript(video_url, languages=None)
1
+ """Wassup fam! 🔥 This module is your go-to for getting those YouTube transcripts!
2
+
3
+ >>> from webscout import YTTranscriber
4
+ >>> transcript = YTTranscriber.get_transcript('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
5
+ >>> print(transcript)
6
+ {'text': 'Never gonna give you up', 'start': 0.0, 'duration': 4.5}
7
+
8
+ Built different by @HelpingAI 👑
9
+ """
10
+
11
+ import requests # For making those HTTP requests like a boss 🌐
12
+ import http.cookiejar as cookiejar # Handling cookies and stuff 🍪
13
+ import json # JSON parsing - keeping it clean! 📝
14
+ from xml.etree import ElementTree # XML parsing magic ✨
15
+ import re # Regex for pattern matching 🎯
16
+ import html # HTML stuff made easy 💪
17
+ from typing import List, Dict, Union, Optional # Type hints for that clean code 💯
18
+ from functools import lru_cache # Cache that data for speed! ⚡
19
+ from concurrent.futures import ThreadPoolExecutor # Parallel processing gang 🚀
20
+ import asyncio # Async/await swag 😎
21
+ from webscout.exceptions import * # All our custom exceptions 🛠️
22
+
23
+ WATCH_URL = 'https://www.youtube.com/watch?v={video_id}'
24
+ MAX_WORKERS = 4 # Keeping it optimal fam! 💪
25
+
26
+ class YTTranscriber:
27
+ """Your boy for getting those YouTube transcripts! 🎥
28
+
29
+ >>> transcript = YTTranscriber.get_transcript('https://youtu.be/dQw4w9WgXcQ')
30
+ >>> print(transcript[0]['text'])
31
+ 'Never gonna give you up'
32
+ """
33
+
34
+ _session = None
35
+ _executor = ThreadPoolExecutor(max_workers=MAX_WORKERS)
36
+
37
+ @classmethod
38
+ def _get_session(cls):
39
+ if cls._session is None:
40
+ cls._session = requests.Session()
41
+ cls._session.headers.update({
42
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
43
+ })
44
+ return cls._session
45
+
46
+ @classmethod
47
+ @lru_cache(maxsize=100)
48
+ def get_transcript(cls, video_url: str, languages: Optional[str] = 'en',
49
+ proxies: Dict[str, str] = None,
50
+ cookies: str = None,
51
+ preserve_formatting: bool = False) -> List[Dict[str, Union[str, float]]]:
52
+ """
53
+ Retrieves the transcript for a given YouTube video URL.
54
+
55
+ Args:
56
+ video_url (str): YouTube video URL (supports various formats).
57
+ languages (str, optional): Language code for the transcript.
58
+ If None, fetches the auto-generated transcript.
59
+ Defaults to 'en'.
60
+ proxies (Dict[str, str], optional): Proxies to use for the request. Defaults to None.
61
+ cookies (str, optional): Path to the cookie file. Defaults to None.
62
+ preserve_formatting (bool, optional): Whether to preserve formatting tags. Defaults to False.
63
+
64
+ Returns:
65
+ List[Dict[str, Union[str, float]]]: A list of dictionaries, each containing:
66
+ - 'text': The transcribed text.
67
+ - 'start': The start time of the text segment (in seconds).
68
+ - 'duration': The duration of the text segment (in seconds).
69
+
70
+ Raises:
71
+ TranscriptRetrievalError: If there's an error retrieving the transcript.
72
+ """
73
+ video_id = cls._extract_video_id(video_url)
74
+ http_client = cls._get_session()
75
+
76
+ if proxies:
77
+ http_client.proxies.update(proxies)
78
+
79
+ if cookies:
80
+ cls._load_cookies(cookies, video_id)
81
+
82
+ transcript_list = TranscriptListFetcher(http_client).fetch(video_id)
83
+ language_codes = [languages] if languages else None
84
+ transcript = transcript_list.find_transcript(language_codes)
85
+
86
+ return transcript.fetch(preserve_formatting)
87
+
88
+ @staticmethod
89
+ def _extract_video_id(video_url: str) -> str:
90
+ """Extracts the video ID from different YouTube URL formats."""
91
+ patterns = [
92
+ r'(?:v=|\/)([0-9A-Za-z_-]{11}).*',
93
+ r'youtu\.be\/([0-9A-Za-z_-]{11})',
94
+ r'youtube\.com\/embed\/([0-9A-Za-z_-]{11})'
95
+ ]
96
+
97
+ for pattern in patterns:
98
+ match = re.search(pattern, video_url)
99
+ if match:
100
+ return match.group(1)
101
+
102
+ if re.match(r'^[0-9A-Za-z_-]{11}$', video_url):
103
+ return video_url
104
+
105
+ raise InvalidVideoIdError(video_url)
106
+
107
+ @staticmethod
108
+ def _load_cookies(cookies: str, video_id: str) -> None:
109
+ """Loads cookies from a file."""
110
+ try:
111
+ cj = cookiejar.MozillaCookieJar(cookies)
112
+ cj.load()
113
+ return cj
114
+ except (cookiejar.LoadError, FileNotFoundError):
115
+ raise CookiePathInvalidError(video_id)
116
+
117
+ class TranscriptListFetcher:
118
+ """Fetches the list of transcripts for a YouTube video."""
119
+
120
+ def __init__(self, http_client: requests.Session):
121
+ """Initializes TranscriptListFetcher."""
122
+ self._http_client = http_client
123
+
124
+ def fetch(self, video_id: str):
125
+ """Fetches and returns a TranscriptList."""
126
+ return TranscriptList.build(
127
+ self._http_client,
128
+ video_id,
129
+ self._extract_captions_json(self._fetch_video_html(video_id), video_id),
130
+ )
131
+
132
+ def _extract_captions_json(self, html: str, video_id: str) -> dict:
133
+ """Extracts the captions JSON data from the video's HTML."""
134
+ splitted_html = html.split('"captions":')
135
+
136
+ if len(splitted_html) <= 1:
137
+ if video_id.startswith('http://') or video_id.startswith('https://'):
138
+ raise InvalidVideoIdError(video_id)
139
+ if 'class="g-recaptcha"' in html:
140
+ raise TooManyRequestsError(video_id)
141
+ if '"playabilityStatus":' not in html:
142
+ raise VideoUnavailableError(video_id)
143
+
144
+ raise TranscriptsDisabledError(video_id)
145
+
146
+ captions_json = json.loads(
147
+ splitted_html[1].split(',"videoDetails')[0].replace('\n', '')
148
+ ).get('playerCaptionsTracklistRenderer')
149
+ if captions_json is None:
150
+ raise TranscriptsDisabledError(video_id)
151
+
152
+ if 'captionTracks' not in captions_json:
153
+ raise TranscriptsDisabledError(video_id)
154
+
155
+ return captions_json
156
+
157
+ def _create_consent_cookie(self, html, video_id):
158
+ match = re.search('name="v" value="(.*?)"', html)
159
+ if match is None:
160
+ raise FailedToCreateConsentCookieError(video_id)
161
+ self._http_client.cookies.set('CONSENT', 'YES+' + match.group(1), domain='.youtube.com')
162
+
163
+ def _fetch_video_html(self, video_id):
164
+ html = self._fetch_html(video_id)
165
+ if 'action="https://consent.youtube.com/s"' in html:
166
+ self._create_consent_cookie(html, video_id)
167
+ html = self._fetch_html(video_id)
168
+ if 'action="https://consent.youtube.com/s"' in html:
169
+ raise FailedToCreateConsentCookieError(video_id)
170
+ return html
171
+
172
+ def _fetch_html(self, video_id):
173
+ response = self._http_client.get(WATCH_URL.format(video_id=video_id), headers={'Accept-Language': 'en-US'})
174
+ return html.unescape(_raise_http_errors(response, video_id).text)
175
+
176
+
177
+ class TranscriptList:
178
+ """Yo fam! This class is all about managing those YouTube transcript lists! 🎯
179
+
180
+ >>> transcript_list = TranscriptList.build(http_client, video_id, captions_json)
181
+ >>> transcript = transcript_list.find_transcript(['en'])
182
+ >>> print(transcript)
183
+ en ("English")[TRANSLATABLE]
184
+ """
185
+
186
+ def __init__(self, video_id, manually_created_transcripts, generated_transcripts, translation_languages):
187
+ """Init that transcript list with all the good stuff! 💯"""
188
+ self.video_id = video_id
189
+ self._manually_created_transcripts = manually_created_transcripts
190
+ self._generated_transcripts = generated_transcripts
191
+ self._translation_languages = translation_languages
192
+
193
+ @staticmethod
194
+ def build(http_client, video_id, captions_json):
195
+ """
196
+ Factory method for TranscriptList.
197
+
198
+ :param http_client: http client which is used to make the transcript retrieving http calls
199
+ :type http_client: requests.Session
200
+ :param video_id: the id of the video this TranscriptList is for
201
+ :type video_id: str
202
+ :param captions_json: the JSON parsed from the YouTube pages static HTML
203
+ :type captions_json: dict
204
+ :return: the created TranscriptList
205
+ :rtype TranscriptList:
206
+ """
207
+ translation_languages = [
208
+ {
209
+ 'language': translation_language['languageName']['simpleText'],
210
+ 'language_code': translation_language['languageCode'],
211
+ } for translation_language in captions_json.get('translationLanguages', [])
212
+ ]
213
+
214
+ manually_created_transcripts = {}
215
+ generated_transcripts = {}
216
+
217
+ for caption in captions_json['captionTracks']:
218
+ if caption.get('kind', '') == 'asr':
219
+ transcript_dict = generated_transcripts
220
+ else:
221
+ transcript_dict = manually_created_transcripts
222
+
223
+ transcript_dict[caption['languageCode']] = Transcript(
224
+ http_client,
225
+ video_id,
226
+ caption['baseUrl'],
227
+ caption['name']['simpleText'],
228
+ caption['languageCode'],
229
+ caption.get('kind', '') == 'asr',
230
+ translation_languages if caption.get('isTranslatable', False) else [],
231
+ )
232
+
233
+ return TranscriptList(
234
+ video_id,
235
+ manually_created_transcripts,
236
+ generated_transcripts,
237
+ translation_languages,
238
+ )
239
+
240
+ def __iter__(self):
241
+ return iter(list(self._manually_created_transcripts.values()) + list(self._generated_transcripts.values()))
242
+
243
+ def find_transcript(self, language_codes):
244
+ """
245
+ Finds a transcript for a given language code. If no language is provided, it will
246
+ return the auto-generated transcript.
247
+
248
+ :param language_codes: A list of language codes in a descending priority.
249
+ :type languages: list[str]
250
+ :return: the found Transcript
251
+ :rtype Transcript:
252
+ :raises: NoTranscriptFound
253
+ """
254
+ if 'any' in language_codes:
255
+ for transcript in self:
256
+ return transcript
257
+ return self._find_transcript(language_codes, [self._manually_created_transcripts, self._generated_transcripts])
258
+
259
+ def find_generated_transcript(self, language_codes):
260
+ """
261
+ Finds an automatically generated transcript for a given language code.
262
+
263
+ :param language_codes: A list of language codes in a descending priority. For example, if this is set to
264
+ ['de', 'en'] it will first try to fetch the german transcript (de) and then fetch the english transcript (en) if
265
+ it fails to do so.
266
+ :type languages: list[str]
267
+ :return: the found Transcript
268
+ :rtype Transcript:
269
+ :raises: NoTranscriptFound
270
+ """
271
+ if 'any' in language_codes:
272
+ for transcript in self:
273
+ if transcript.is_generated:
274
+ return transcript
275
+ return self._find_transcript(language_codes, [self._generated_transcripts])
276
+
277
+ def find_manually_created_transcript(self, language_codes):
278
+ """
279
+ Finds a manually created transcript for a given language code.
280
+
281
+ :param language_codes: A list of language codes in a descending priority. For example, if this is set to
282
+ ['de', 'en'] it will first try to fetch the german transcript (de) and then fetch the english transcript (en) if
283
+ it fails to do so.
284
+ :type languages: list[str]
285
+ :return: the found Transcript
286
+ :rtype Transcript:
287
+ :raises: NoTranscriptFound
288
+ """
289
+ return self._find_transcript(language_codes, [self._manually_created_transcripts])
290
+
291
+ def _find_transcript(self, language_codes, transcript_dicts):
292
+ for language_code in language_codes:
293
+ for transcript_dict in transcript_dicts:
294
+ if language_code in transcript_dict:
295
+ return transcript_dict[language_code]
296
+
297
+ raise NoTranscriptFoundError(
298
+ self.video_id,
299
+ language_codes,
300
+ self
301
+ )
302
+
303
+ def __str__(self):
304
+ return (
305
+ 'For this video ({video_id}) transcripts are available in the following languages:\n\n'
306
+ '(MANUALLY CREATED)\n'
307
+ '{available_manually_created_transcript_languages}\n\n'
308
+ '(GENERATED)\n'
309
+ '{available_generated_transcripts}\n\n'
310
+ '(TRANSLATION LANGUAGES)\n'
311
+ '{available_translation_languages}'
312
+ ).format(
313
+ video_id=self.video_id,
314
+ available_manually_created_transcript_languages=self._get_language_description(
315
+ str(transcript) for transcript in self._manually_created_transcripts.values()
316
+ ),
317
+ available_generated_transcripts=self._get_language_description(
318
+ str(transcript) for transcript in self._generated_transcripts.values()
319
+ ),
320
+ available_translation_languages=self._get_language_description(
321
+ '{language_code} ("{language}")'.format(
322
+ language=translation_language['language'],
323
+ language_code=translation_language['language_code'],
324
+ ) for translation_language in self._translation_languages
325
+ )
326
+ )
327
+
328
+ def _get_language_description(self, transcript_strings):
329
+ description = '\n'.join(' - {transcript}'.format(transcript=transcript) for transcript in transcript_strings)
330
+ return description if description else 'None'
331
+
332
+
333
+ class Transcript:
334
+ """Your personal transcript handler! 🎭
335
+
336
+ >>> transcript = transcript_list.find_transcript(['en'])
337
+ >>> print(transcript.language)
338
+ 'English'
339
+ >>> if transcript.is_translatable:
340
+ ... es_transcript = transcript.translate('es')
341
+ ... print(es_transcript.language)
342
+ 'Spanish'
343
+ """
344
+
345
+ def __init__(self, http_client, video_id, url, language, language_code, is_generated, translation_languages):
346
+ """Initialize with all the goodies! 🎁"""
347
+ self._http_client = http_client
348
+ self.video_id = video_id
349
+ self._url = url
350
+ self.language = language
351
+ self.language_code = language_code
352
+ self.is_generated = is_generated
353
+ self.translation_languages = translation_languages
354
+ self._translation_languages_dict = {
355
+ translation_language['language_code']: translation_language['language']
356
+ for translation_language in translation_languages
357
+ }
358
+
359
+ def fetch(self, preserve_formatting=False):
360
+ """Get that transcript data! 🎯
361
+
362
+ Args:
363
+ preserve_formatting (bool): Keep HTML formatting? Default is nah fam.
364
+
365
+ Returns:
366
+ list: That sweet transcript data with text, start time, and duration! 📝
367
+ """
368
+ response = self._http_client.get(self._url, headers={'Accept-Language': 'en-US'})
369
+ return TranscriptParser(preserve_formatting=preserve_formatting).parse(
370
+ _raise_http_errors(response, self.video_id).text,
371
+ )
372
+
373
+ def __str__(self):
374
+ """String representation looking clean! 💅"""
375
+ return '{language_code} ("{language}"){translation_description}'.format(
376
+ language=self.language,
377
+ language_code=self.language_code,
378
+ translation_description='[TRANSLATABLE]' if self.is_translatable else ''
379
+ )
380
+
381
+ @property
382
+ def is_translatable(self):
383
+ """Can we translate this? 🌍"""
384
+ return len(self.translation_languages) > 0
385
+
386
+ def translate(self, language_code):
387
+ """Translate to another language! 🌎
388
+
389
+ Args:
390
+ language_code (str): Which language you want fam?
391
+
392
+ Returns:
393
+ Transcript: A fresh transcript in your requested language! 🔄
394
+
395
+ Raises:
396
+ NotTranslatableError: If we can't translate this one 😢
397
+ TranslationLanguageNotAvailableError: If that language isn't available 🚫
398
+ """
399
+ if not self.is_translatable:
400
+ raise NotTranslatableError(self.video_id)
401
+
402
+ if language_code not in self._translation_languages_dict:
403
+ raise TranslationLanguageNotAvailableError(self.video_id)
404
+
405
+ return Transcript(
406
+ self._http_client,
407
+ self.video_id,
408
+ '{url}&tlang={language_code}'.format(url=self._url, language_code=language_code),
409
+ self._translation_languages_dict[language_code],
410
+ language_code,
411
+ True,
412
+ [],
413
+ )
414
+
415
+
416
+ class TranscriptParser:
417
+ """Parsing those transcripts like a pro! 🎯
418
+
419
+ >>> parser = TranscriptParser(preserve_formatting=True)
420
+ >>> data = parser.parse(xml_data)
421
+ >>> print(data[0])
422
+ {'text': 'Never gonna give you up', 'start': 0.0, 'duration': 4.5}
423
+ """
424
+
425
+ _FORMATTING_TAGS = [
426
+ 'strong', # For that extra emphasis 💪
427
+ 'em', # When you need that italic swag 🎨
428
+ 'b', # Bold and beautiful 💯
429
+ 'i', # More italic vibes ✨
430
+ 'mark', # Highlight that text 🌟
431
+ 'small', # Keep it lowkey 🤫
432
+ 'del', # Strike it out ⚡
433
+ 'ins', # Insert new stuff 🆕
434
+ 'sub', # Subscript gang 📉
435
+ 'sup', # Superscript squad 📈
436
+ ]
437
+
438
+ def __init__(self, preserve_formatting=False):
439
+ """Get ready to parse with style! 🎨"""
440
+ self._html_regex = self._get_html_regex(preserve_formatting)
441
+
442
+ def _get_html_regex(self, preserve_formatting):
443
+ """Get that regex pattern ready! 🎯"""
444
+ if preserve_formatting:
445
+ formats_regex = '|'.join(self._FORMATTING_TAGS)
446
+ formats_regex = r'<\/?(?!\/?(' + formats_regex + r')\b).*?\b>'
447
+ html_regex = re.compile(formats_regex, re.IGNORECASE)
448
+ else:
449
+ html_regex = re.compile(r'<[^>]*>', re.IGNORECASE)
450
+ return html_regex
451
+
452
+ def parse(self, plain_data):
453
+ """Parse that XML data into something beautiful! ✨"""
454
+ return [
455
+ {
456
+ 'text': re.sub(self._html_regex, '', html.unescape(xml_element.text)),
457
+ 'start': float(xml_element.attrib['start']),
458
+ 'duration': float(xml_element.attrib.get('dur', '0.0')),
459
+ }
460
+ for xml_element in ElementTree.fromstring(plain_data)
461
+ if xml_element.text is not None
462
+ ]
463
+
464
+
465
+ def _raise_http_errors(response, video_id):
466
+ """Handle those HTTP errors with style! 🛠️"""
467
+ try:
468
+ response.raise_for_status()
469
+ return response
470
+ except requests.exceptions.HTTPError as error:
471
+ raise YouTubeRequestFailedError(video_id, error)
472
+
473
+
474
+ if __name__ == "__main__":
475
+ # Let's get this party started! 🎉
476
+ from rich import print
477
+ video_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
478
+ transcript = YTTranscriber.get_transcript(video_url, languages=None)
479
+ print("Here's what we got! 🔥")
552
480
  print(transcript)