clipscribe 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Omar Taoufik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: clipscribe
3
+ Version: 0.1.0
4
+ Summary: Extract transcripts from YouTube, TikTok, and Instagram URLs.
5
+ Author-email: Omar Taoufik <omartaoufik26@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/darkknight127/clipscribe
8
+ Project-URL: Repository, https://github.com/darkknight127/clipscribe
9
+ Project-URL: Issues, https://github.com/darkknight127/clipscribe/issues
10
+ Keywords: transcript,youtube,tiktok,instagram,whisper,captions
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Multimedia :: Video
20
+ Classifier: Topic :: Text Processing
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: youtube
25
+ Requires-Dist: youtube-transcript-api>=1.1.0; extra == "youtube"
26
+ Provides-Extra: tiktok
27
+ Requires-Dist: yt-dlp>=2024.8.6; extra == "tiktok"
28
+ Requires-Dist: faster-whisper>=1.0.0; extra == "tiktok"
29
+ Requires-Dist: imageio-ffmpeg>=0.5.0; extra == "tiktok"
30
+ Provides-Extra: instagram
31
+ Requires-Dist: yt-dlp>=2024.8.6; extra == "instagram"
32
+ Requires-Dist: faster-whisper>=1.0.0; extra == "instagram"
33
+ Requires-Dist: imageio-ffmpeg>=0.5.0; extra == "instagram"
34
+ Provides-Extra: all
35
+ Requires-Dist: youtube-transcript-api>=1.1.0; extra == "all"
36
+ Requires-Dist: yt-dlp>=2024.8.6; extra == "all"
37
+ Requires-Dist: faster-whisper>=1.0.0; extra == "all"
38
+ Requires-Dist: imageio-ffmpeg>=0.5.0; extra == "all"
39
+ Provides-Extra: dev
40
+ Requires-Dist: pytest>=8.0; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # clipscribe
44
+
45
+ Extract transcripts from YouTube, TikTok, and Instagram URLs.
46
+
47
+ YouTube uses `youtube-transcript-api`. TikTok and Instagram use yt-dlp + faster-whisper.
48
+
49
+ This is a **0.1.0** library. Site extractors break when platforms change pages. Keep `yt-dlp` updated if you use TikTok or Instagram.
50
+
51
+ Source: [github.com/darkknight127/clipscribe](https://github.com/darkknight127/clipscribe)
52
+
53
+ ## Install
54
+
55
+ Pick the platforms you need, or install all of them:
56
+
57
+ ```bash
58
+ pip install "clipscribe[youtube]"
59
+ pip install "clipscribe[tiktok]"
60
+ pip install "clipscribe[instagram]"
61
+ pip install "clipscribe[youtube,tiktok]"
62
+ pip install "clipscribe[all]"
63
+ ```
64
+
65
+ TikTok and Instagram download Whisper models on first use. ffmpeg comes from `imageio-ffmpeg`; no system install is required.
66
+
67
+ From this repo:
68
+
69
+ ```bash
70
+ pip install -e ".[all,dev]"
71
+ ```
72
+
73
+ If TikTok/Instagram downloads start failing, upgrade the extractor stack:
74
+
75
+ ```bash
76
+ pip install -U yt-dlp imageio-ffmpeg
77
+ ```
78
+
79
+ ## Usage
80
+
81
+ ```python
82
+ from clipscribe import TranscriptExtractor
83
+
84
+ api = TranscriptExtractor()
85
+ result = api.extract("https://www.youtube.com/watch?v=VIDEO_ID")
86
+
87
+ print(result.full_text)
88
+ print(result.to_dict())
89
+ ```
90
+
91
+ Batch (failed URLs are skipped and logged):
92
+
93
+ ```python
94
+ results = api.extract_many([url1, url2, url3])
95
+ ```
96
+
97
+ Save files under `output/` relative to the current working directory (`json` by default). Choose from `json`, `txt`, `srt`:
98
+
99
+ ```python
100
+ result = api.extract(url, save=True)
101
+
102
+ api = TranscriptExtractor(allowed_outputs=["json", "srt"])
103
+ result = api.extract(url, save=True)
104
+
105
+ result = api.extract(url, save=True, allowed_outputs=["srt"])
106
+ ```
107
+
108
+ Retry flaky TikTok downloads:
109
+
110
+ ```python
111
+ result = api.extract(tiktok_url, max_retries=2)
112
+ ```
113
+
114
+ TikTok often needs cookies when the site blocks anonymous requests:
115
+
116
+ ```python
117
+ api = TranscriptExtractor(tiktok_cookies_from_browser="firefox")
118
+ ```
119
+
120
+ Or a Netscape cookies file:
121
+
122
+ ```python
123
+ api = TranscriptExtractor(tiktok_cookies="tiktok_cookies.txt")
124
+ ```
125
+
126
+ YouTube proxy (optional):
127
+
128
+ ```python
129
+ from clipscribe import GenericProxyConfig, TranscriptExtractor
130
+
131
+ api = TranscriptExtractor(
132
+ youtube_proxy_config=GenericProxyConfig(
133
+ http_url="http://user:pass@host:port",
134
+ https_url="http://user:pass@host:port",
135
+ )
136
+ )
137
+ ```
138
+
139
+ Instagram needs auth. Browser cookies are the most reliable. This also requires the `instagram` extra:
140
+
141
+ ```python
142
+ api = TranscriptExtractor(instagram_cookies_from_browser="firefox")
143
+ result = api.extract("https://www.instagram.com/reel/REEL_ID/")
144
+ ```
145
+
146
+ Or a Netscape cookies file:
147
+
148
+ ```python
149
+ api = TranscriptExtractor(instagram_cookies="instagram_cookies.txt")
150
+ ```
151
+
152
+ TikTok and Instagram videos longer than 15 minutes are rejected unless you raise or disable the cap:
153
+
154
+ ```python
155
+ api = TranscriptExtractor(max_duration_s=None)
156
+ ```
157
+
158
+ ## Notes
159
+
160
+ - First TikTok/Instagram run may download a Whisper model and ffmpeg. That needs disk and network.
161
+ - Default Whisper model is `tiny` (speed over accuracy).
162
+ - `YouTubeTranscriptApi` is not thread-safe. Use one `TranscriptExtractor` per thread.
163
+ - `register()` is per instance, not global.
164
+ - You are responsible for complying with YouTube, TikTok, and Instagram terms of use.
@@ -0,0 +1,122 @@
1
+ # clipscribe
2
+
3
+ Extract transcripts from YouTube, TikTok, and Instagram URLs.
4
+
5
+ YouTube uses `youtube-transcript-api`. TikTok and Instagram use yt-dlp + faster-whisper.
6
+
7
+ This is a **0.1.0** library. Site extractors break when platforms change pages. Keep `yt-dlp` updated if you use TikTok or Instagram.
8
+
9
+ Source: [github.com/darkknight127/clipscribe](https://github.com/darkknight127/clipscribe)
10
+
11
+ ## Install
12
+
13
+ Pick the platforms you need, or install all of them:
14
+
15
+ ```bash
16
+ pip install "clipscribe[youtube]"
17
+ pip install "clipscribe[tiktok]"
18
+ pip install "clipscribe[instagram]"
19
+ pip install "clipscribe[youtube,tiktok]"
20
+ pip install "clipscribe[all]"
21
+ ```
22
+
23
+ TikTok and Instagram download Whisper models on first use. ffmpeg comes from `imageio-ffmpeg`; no system install is required.
24
+
25
+ From this repo:
26
+
27
+ ```bash
28
+ pip install -e ".[all,dev]"
29
+ ```
30
+
31
+ If TikTok/Instagram downloads start failing, upgrade the extractor stack:
32
+
33
+ ```bash
34
+ pip install -U yt-dlp imageio-ffmpeg
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ from clipscribe import TranscriptExtractor
41
+
42
+ api = TranscriptExtractor()
43
+ result = api.extract("https://www.youtube.com/watch?v=VIDEO_ID")
44
+
45
+ print(result.full_text)
46
+ print(result.to_dict())
47
+ ```
48
+
49
+ Batch (failed URLs are skipped and logged):
50
+
51
+ ```python
52
+ results = api.extract_many([url1, url2, url3])
53
+ ```
54
+
55
+ Save files under `output/` relative to the current working directory (`json` by default). Choose from `json`, `txt`, `srt`:
56
+
57
+ ```python
58
+ result = api.extract(url, save=True)
59
+
60
+ api = TranscriptExtractor(allowed_outputs=["json", "srt"])
61
+ result = api.extract(url, save=True)
62
+
63
+ result = api.extract(url, save=True, allowed_outputs=["srt"])
64
+ ```
65
+
66
+ Retry flaky TikTok downloads:
67
+
68
+ ```python
69
+ result = api.extract(tiktok_url, max_retries=2)
70
+ ```
71
+
72
+ TikTok often needs cookies when the site blocks anonymous requests:
73
+
74
+ ```python
75
+ api = TranscriptExtractor(tiktok_cookies_from_browser="firefox")
76
+ ```
77
+
78
+ Or a Netscape cookies file:
79
+
80
+ ```python
81
+ api = TranscriptExtractor(tiktok_cookies="tiktok_cookies.txt")
82
+ ```
83
+
84
+ YouTube proxy (optional):
85
+
86
+ ```python
87
+ from clipscribe import GenericProxyConfig, TranscriptExtractor
88
+
89
+ api = TranscriptExtractor(
90
+ youtube_proxy_config=GenericProxyConfig(
91
+ http_url="http://user:pass@host:port",
92
+ https_url="http://user:pass@host:port",
93
+ )
94
+ )
95
+ ```
96
+
97
+ Instagram needs auth. Browser cookies are the most reliable. This also requires the `instagram` extra:
98
+
99
+ ```python
100
+ api = TranscriptExtractor(instagram_cookies_from_browser="firefox")
101
+ result = api.extract("https://www.instagram.com/reel/REEL_ID/")
102
+ ```
103
+
104
+ Or a Netscape cookies file:
105
+
106
+ ```python
107
+ api = TranscriptExtractor(instagram_cookies="instagram_cookies.txt")
108
+ ```
109
+
110
+ TikTok and Instagram videos longer than 15 minutes are rejected unless you raise or disable the cap:
111
+
112
+ ```python
113
+ api = TranscriptExtractor(max_duration_s=None)
114
+ ```
115
+
116
+ ## Notes
117
+
118
+ - First TikTok/Instagram run may download a Whisper model and ffmpeg. That needs disk and network.
119
+ - Default Whisper model is `tiny` (speed over accuracy).
120
+ - `YouTubeTranscriptApi` is not thread-safe. Use one `TranscriptExtractor` per thread.
121
+ - `register()` is per instance, not global.
122
+ - You are responsible for complying with YouTube, TikTok, and Instagram terms of use.
@@ -0,0 +1,52 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from .extractor import ExtractionResponse, TranscriptExtractor
4
+ from .utils.extras import missing_extra
5
+ from .utils.transcript_utils import (
6
+ TranscriptResult,
7
+ TranscriptSaver,
8
+ TranscriptSegment,
9
+ TranscriptSource,
10
+ )
11
+
12
+ try:
13
+ __version__ = version("clipscribe")
14
+ except PackageNotFoundError:
15
+ __version__ = "0.1.0"
16
+
17
+ __all__ = [
18
+ "ExtractionResponse",
19
+ "GenericProxyConfig",
20
+ "InstagramTranscriptExtractor",
21
+ "TikTokTranscriptExtractor",
22
+ "TranscriptExtractor",
23
+ "TranscriptResult",
24
+ "TranscriptSaver",
25
+ "TranscriptSegment",
26
+ "TranscriptSource",
27
+ "YouTubeTranscriptExtractor",
28
+ "__version__",
29
+ ]
30
+
31
+
32
+ def __dir__():
33
+ return sorted(set(globals()) | set(__all__))
34
+
35
+
36
+ def __getattr__(name: str):
37
+ if name == "GenericProxyConfig":
38
+ try:
39
+ from youtube_transcript_api.proxies import GenericProxyConfig
40
+ except ImportError as exc:
41
+ raise missing_extra("youtube", "YouTube proxy config") from exc
42
+ return GenericProxyConfig
43
+ if name == "YouTubeTranscriptExtractor":
44
+ from .utils.youtube_utils import YouTubeTranscriptExtractor
45
+ return YouTubeTranscriptExtractor
46
+ if name == "TikTokTranscriptExtractor":
47
+ from .utils.tiktok_utils import TikTokTranscriptExtractor
48
+ return TikTokTranscriptExtractor
49
+ if name == "InstagramTranscriptExtractor":
50
+ from .utils.instagram_utils import InstagramTranscriptExtractor
51
+ return InstagramTranscriptExtractor
52
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,295 @@
1
+ """Unified transcript extraction API for YouTube, TikTok, and Instagram."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ import time
8
+ from typing import Callable
9
+
10
+ from .utils.transcript_utils import PlatformExtractor, TranscriptResult, TranscriptSaver
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+ _NON_RETRYABLE = (ValueError, FileNotFoundError, ImportError)
15
+
16
+
17
+ class ExtractionResponse:
18
+ """
19
+ Wraps a TranscriptResult alongside optional save paths.
20
+ This is what TranscriptExtractor.extract() always returns.
21
+
22
+ Attributes:
23
+ result: The raw TranscriptResult (segments, metadata, full_text).
24
+ saved: Dict of format -> Path if save=True, else None.
25
+ url: The original URL that was extracted.
26
+ platform: Detected TranscriptSource.
27
+ """
28
+
29
+ def __init__(self, result: TranscriptResult, saved: dict | None = None):
30
+ self.result = result
31
+ self.saved = saved
32
+ self.url = result.url
33
+ self.platform = result.source
34
+
35
+ @property
36
+ def full_text(self) -> str:
37
+ return self.result.full_text
38
+
39
+ @property
40
+ def segments(self):
41
+ return self.result.segments
42
+
43
+ @property
44
+ def metadata(self):
45
+ return self.result.metadata
46
+
47
+ def to_dict(self) -> dict:
48
+ data = self.result.to_dict()
49
+ if self.saved:
50
+ data["saved_files"] = {k: str(v) for k, v in self.saved.items()}
51
+ return data
52
+
53
+ def __repr__(self) -> str:
54
+ return (
55
+ f"<ExtractionResponse platform={self.platform.value!r} "
56
+ f"segments={len(self.segments)} url={self.url!r}>"
57
+ )
58
+
59
+
60
+ class TranscriptExtractor:
61
+ """
62
+ Unified transcript extraction API.
63
+
64
+ Built-in extractors and extra platforms added via ``register()`` are
65
+ per-instance.
66
+
67
+ Args:
68
+ language: Preferred transcript language for YouTube (default: "en").
69
+ whisper_model: faster-whisper model size for TikTok/Instagram (default: "tiny").
70
+ whisper_device: "cpu" or "cuda" (default: "cpu").
71
+ whisper_compute: Quantization type for faster-whisper (default: "int8").
72
+ output_dir: Directory for saved transcripts (default: "output", relative to CWD).
73
+ allowed_outputs: File formats to write when save=True.
74
+ Choose from "json", "txt", "srt" (default: json).
75
+ on_error: Optional callback(url, exc) called on per-URL failures
76
+ in extract_many(). Defaults to logging the error.
77
+ youtube_proxy_config: Optional youtube-transcript-api ProxyConfig.
78
+ tiktok_cookies_from_browser: Browser name for TikTok cookies.
79
+ tiktok_cookies: Netscape cookies.txt path for TikTok.
80
+ instagram_cookies_from_browser: Browser name for Instagram cookies.
81
+ instagram_cookies: Netscape cookies.txt path for Instagram.
82
+ max_duration_s: Reject TikTok/Instagram videos longer than this
83
+ (default: 900). Pass None for no limit.
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ language: str = "en",
89
+ whisper_model: str = "tiny",
90
+ whisper_device: str = "cpu",
91
+ whisper_compute: str = "int8",
92
+ output_dir: str = "output",
93
+ allowed_outputs: list[str] | None = None,
94
+ on_error: Callable[[str, Exception], None] | None = None,
95
+ instagram_cookies_from_browser: str | None = None,
96
+ instagram_cookies: str | None = None,
97
+ tiktok_cookies_from_browser: str | None = None,
98
+ tiktok_cookies: str | None = None,
99
+ youtube_proxy_config=None,
100
+ max_duration_s: int | None = 900,
101
+ ):
102
+ self._saver = TranscriptSaver(output_dir=output_dir, allowed_outputs=allowed_outputs)
103
+ self._on_error = on_error or self._default_error_handler
104
+ self._registry: list[tuple[re.Pattern, PlatformExtractor]] = []
105
+ self._extra: list[tuple[re.Pattern, PlatformExtractor]] = []
106
+
107
+ self._register_defaults(
108
+ language=language,
109
+ whisper_model=whisper_model,
110
+ whisper_device=whisper_device,
111
+ whisper_compute=whisper_compute,
112
+ instagram_cookies_from_browser=instagram_cookies_from_browser,
113
+ instagram_cookies=instagram_cookies,
114
+ tiktok_cookies_from_browser=tiktok_cookies_from_browser,
115
+ tiktok_cookies=tiktok_cookies,
116
+ youtube_proxy_config=youtube_proxy_config,
117
+ max_duration_s=max_duration_s,
118
+ )
119
+
120
+ def extract(
121
+ self,
122
+ url: str,
123
+ save: bool = False,
124
+ max_retries: int = 0,
125
+ allowed_outputs: list[str] | None = None,
126
+ ) -> ExtractionResponse:
127
+ """
128
+ Extract transcript from a single URL.
129
+
130
+ Args:
131
+ url: A YouTube, TikTok, or Instagram URL (or any registered platform).
132
+ save: If True, saves files to the configured output_dir.
133
+ max_retries: Extra attempts after a failed extract (default: 0).
134
+ allowed_outputs: Formats for this call only. Choose from
135
+ "json", "txt", "srt". Defaults to the instance list.
136
+
137
+ Returns:
138
+ ExtractionResponse with .full_text, .segments, .metadata, .to_dict()
139
+
140
+ Raises:
141
+ ValueError: If the URL doesn't match any registered platform.
142
+ RuntimeError: If extraction fails.
143
+ ImportError: If the matching platform extra is not installed.
144
+ """
145
+ extractor = self._resolve(url)
146
+ if allowed_outputs is not None and not save:
147
+ log.warning("allowed_outputs is ignored because save=False")
148
+ max_retries = max(0, max_retries)
149
+ result: TranscriptResult | None = None
150
+ for attempt in range(max_retries + 1):
151
+ try:
152
+ result = extractor.extract(url)
153
+ break
154
+ except _NON_RETRYABLE:
155
+ raise
156
+ except Exception:
157
+ if attempt >= max_retries:
158
+ raise
159
+ time.sleep(min(8, 2 ** attempt))
160
+ if result is None:
161
+ raise RuntimeError(f"Extraction failed for {url}")
162
+ saved = (
163
+ self._saver.save(result, self._slug(result), allowed_outputs=allowed_outputs)
164
+ if save
165
+ else None
166
+ )
167
+ return ExtractionResponse(result=result, saved=saved)
168
+
169
+ def extract_many(
170
+ self,
171
+ urls: list[str],
172
+ save: bool = False,
173
+ max_retries: int = 0,
174
+ allowed_outputs: list[str] | None = None,
175
+ ) -> list[ExtractionResponse]:
176
+ """
177
+ Extract transcripts from multiple URLs.
178
+ Failures are caught, passed to on_error, and skipped — processing continues.
179
+ """
180
+ if allowed_outputs is not None and not save:
181
+ log.warning("allowed_outputs is ignored because save=False")
182
+ allowed_outputs = None
183
+ responses = []
184
+ for url in urls:
185
+ try:
186
+ responses.append(
187
+ self.extract(
188
+ url,
189
+ save=save,
190
+ max_retries=max_retries,
191
+ allowed_outputs=allowed_outputs,
192
+ )
193
+ )
194
+ except Exception as exc:
195
+ self._on_error(url, exc)
196
+ return responses
197
+
198
+ def supports(self, url: str) -> bool:
199
+ """Return True if the URL matches a registered platform."""
200
+ try:
201
+ self._resolve(url)
202
+ return True
203
+ except ValueError:
204
+ return False
205
+
206
+ def register(self, url_pattern: str, extractor: PlatformExtractor) -> None:
207
+ """
208
+ Register a new platform extractor on this instance.
209
+
210
+ Args:
211
+ url_pattern: A regex string matched against the full URL.
212
+ extractor: Any object with an .extract(url) -> TranscriptResult method.
213
+ """
214
+ self._extra.append((re.compile(url_pattern, re.IGNORECASE), extractor))
215
+
216
+ def _register_defaults(
217
+ self,
218
+ language: str,
219
+ whisper_model: str,
220
+ whisper_device: str,
221
+ whisper_compute: str,
222
+ instagram_cookies_from_browser: str | None = None,
223
+ instagram_cookies: str | None = None,
224
+ tiktok_cookies_from_browser: str | None = None,
225
+ tiktok_cookies: str | None = None,
226
+ youtube_proxy_config=None,
227
+ max_duration_s: int | None = 900,
228
+ ) -> None:
229
+ from .utils.youtube_utils import YouTubeTranscriptExtractor
230
+ from .utils.tiktok_utils import TikTokTranscriptExtractor
231
+
232
+ self._registry = [
233
+ (
234
+ re.compile(r"youtube\.com|youtu\.be", re.IGNORECASE),
235
+ YouTubeTranscriptExtractor(
236
+ language=language,
237
+ proxy_config=youtube_proxy_config,
238
+ ),
239
+ ),
240
+ (
241
+ re.compile(r"tiktok\.com", re.IGNORECASE),
242
+ TikTokTranscriptExtractor(
243
+ whisper_model=whisper_model,
244
+ device=whisper_device,
245
+ compute_type=whisper_compute,
246
+ cookies_from_browser=tiktok_cookies_from_browser,
247
+ cookies_file=tiktok_cookies,
248
+ max_duration_s=max_duration_s,
249
+ ),
250
+ ),
251
+ ]
252
+
253
+ if instagram_cookies_from_browser or instagram_cookies:
254
+ from .utils.instagram_utils import InstagramTranscriptExtractor
255
+
256
+ self._registry.append((
257
+ re.compile(r"instagram\.com", re.IGNORECASE),
258
+ InstagramTranscriptExtractor(
259
+ cookies_from_browser=instagram_cookies_from_browser,
260
+ cookies_file=instagram_cookies,
261
+ whisper_model=whisper_model,
262
+ device=whisper_device,
263
+ compute_type=whisper_compute,
264
+ max_duration_s=max_duration_s,
265
+ ),
266
+ ))
267
+
268
+ def _resolve(self, url: str) -> PlatformExtractor:
269
+ for pattern, extractor in (*self._registry, *self._extra):
270
+ if pattern.search(url):
271
+ return extractor
272
+ if "instagram.com" in url.lower():
273
+ raise ValueError(
274
+ "Instagram URLs require authentication. "
275
+ "Pass instagram_cookies_from_browser or instagram_cookies."
276
+ )
277
+ raise ValueError(
278
+ f"No extractor registered for URL: {url}\n"
279
+ f"Registered platforms: {self._registered_platforms()}"
280
+ )
281
+
282
+ def _slug(self, result: TranscriptResult) -> str:
283
+ media_id = (
284
+ result.metadata.get("video_id")
285
+ or result.metadata.get("post_id")
286
+ or "unknown"
287
+ )
288
+ return re.sub(r'[\\/*?:"<>|]', "_", f"{result.source.value}_{media_id}").strip()
289
+
290
+ def _registered_platforms(self) -> list[str]:
291
+ return [pattern.pattern for pattern, _ in (*self._registry, *self._extra)]
292
+
293
+ @staticmethod
294
+ def _default_error_handler(url: str, exc: Exception) -> None:
295
+ log.error("Failed (%s): %s", url, exc)
File without changes
File without changes
@@ -0,0 +1,10 @@
1
+ """Install-extra hints for optional platform dependencies."""
2
+
3
+ def extra_install(platform: str) -> str:
4
+ return f'pip install "clipscribe[{platform}]" or "clipscribe[all]"'
5
+
6
+
7
+ def missing_extra(platform: str, what: str) -> ImportError:
8
+ return ImportError(
9
+ f"{what} requires extra dependencies. Install with: {extra_install(platform)}"
10
+ )