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.
- clipscribe-0.1.0/LICENSE +21 -0
- clipscribe-0.1.0/PKG-INFO +164 -0
- clipscribe-0.1.0/README.md +122 -0
- clipscribe-0.1.0/clipscribe/__init__.py +52 -0
- clipscribe-0.1.0/clipscribe/extractor.py +295 -0
- clipscribe-0.1.0/clipscribe/py.typed +0 -0
- clipscribe-0.1.0/clipscribe/utils/__init__.py +0 -0
- clipscribe-0.1.0/clipscribe/utils/extras.py +10 -0
- clipscribe-0.1.0/clipscribe/utils/instagram_utils.py +164 -0
- clipscribe-0.1.0/clipscribe/utils/tiktok_utils.py +142 -0
- clipscribe-0.1.0/clipscribe/utils/transcript_utils.py +217 -0
- clipscribe-0.1.0/clipscribe/utils/whisper_backend.py +44 -0
- clipscribe-0.1.0/clipscribe/utils/ydl_audio.py +131 -0
- clipscribe-0.1.0/clipscribe/utils/youtube_utils.py +173 -0
- clipscribe-0.1.0/clipscribe.egg-info/PKG-INFO +164 -0
- clipscribe-0.1.0/clipscribe.egg-info/SOURCES.txt +25 -0
- clipscribe-0.1.0/clipscribe.egg-info/dependency_links.txt +1 -0
- clipscribe-0.1.0/clipscribe.egg-info/requires.txt +22 -0
- clipscribe-0.1.0/clipscribe.egg-info/top_level.txt +1 -0
- clipscribe-0.1.0/pyproject.toml +67 -0
- clipscribe-0.1.0/setup.cfg +4 -0
- clipscribe-0.1.0/tests/test_duration_filter.py +9 -0
- clipscribe-0.1.0/tests/test_extractor.py +81 -0
- clipscribe-0.1.0/tests/test_instagram_urls.py +35 -0
- clipscribe-0.1.0/tests/test_saver.py +42 -0
- clipscribe-0.1.0/tests/test_tiktok_urls.py +31 -0
- clipscribe-0.1.0/tests/test_youtube_urls.py +37 -0
clipscribe-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
)
|