capslane 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.
- capslane-0.1.0/.github/workflows/publish.yml +23 -0
- capslane-0.1.0/.github/workflows/quality.yml +13 -0
- capslane-0.1.0/.gitignore +5 -0
- capslane-0.1.0/LICENSE +9 -0
- capslane-0.1.0/PKG-INFO +117 -0
- capslane-0.1.0/README.md +99 -0
- capslane-0.1.0/pyproject.toml +27 -0
- capslane-0.1.0/src/capslane/__init__.py +3 -0
- capslane-0.1.0/src/capslane/client.py +91 -0
- capslane-0.1.0/src/capslane/py.typed +1 -0
- capslane-0.1.0/tests/test_client.py +35 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- 'v*.*.*'
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
|
|
17
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
|
18
|
+
with:
|
|
19
|
+
python-version: '3.13'
|
|
20
|
+
- run: python -m pip install build
|
|
21
|
+
- run: python -m unittest discover tests
|
|
22
|
+
- run: python -m build
|
|
23
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
|
capslane-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luca Deguin, Webba Creative Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
capslane-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: capslane
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for retrieving YouTube transcripts through the Capslane API.
|
|
5
|
+
Project-URL: Documentation, https://capslane.com/docs
|
|
6
|
+
Project-URL: Repository, https://github.com/Webba-Creative-Technologies/capslane-python
|
|
7
|
+
Author-email: Webba Creative Technologies <luca.deguin@webba-creative.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: api,captions,transcript,video-to-text,youtube
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Capslane Python SDK
|
|
20
|
+
|
|
21
|
+
The Capslane Python SDK retrieves YouTube transcripts from Python applications. It returns existing captions when available and can generate a transcript when a video has no usable caption track. The package uses the Python standard library and has no runtime dependency.
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Python 3.10 or later
|
|
26
|
+
- A Capslane API key from the [dashboard](https://capslane.com/api-keys)
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install capslane
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Retrieve a transcript
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import os
|
|
38
|
+
|
|
39
|
+
from capslane import CapslaneClient
|
|
40
|
+
|
|
41
|
+
capslane = CapslaneClient(os.environ["CAPSLANE_API_KEY"])
|
|
42
|
+
|
|
43
|
+
result = capslane.transcript(
|
|
44
|
+
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
45
|
+
mode="auto",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
transcript = (
|
|
49
|
+
capslane.wait_for_transcript(result)
|
|
50
|
+
if "jobId" in result
|
|
51
|
+
else result
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
print(transcript["content"])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Check a transcript job
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
job = capslane.transcript_job(
|
|
61
|
+
"job_00000000-0000-0000-0000-000000000000"
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`wait_for_transcript` polls an accepted job until it completes, fails or reaches the timeout.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
transcript = capslane.wait_for_transcript(
|
|
69
|
+
job,
|
|
70
|
+
interval=2.0,
|
|
71
|
+
timeout=20 * 60,
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Request options
|
|
76
|
+
|
|
77
|
+
| Option | Type | Description |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `url` | `str` | Public YouTube URL or 11-character video ID. |
|
|
80
|
+
| `lang` | `str` | Optional preferred language code. |
|
|
81
|
+
| `mode` | `native`, `auto` or `generate` | Selects how Capslane obtains the transcript. |
|
|
82
|
+
| `text` | `bool` | Returns one text string instead of timestamped segments. |
|
|
83
|
+
| `chunk_size` | `int` | Groups transcript segments into larger chunks. |
|
|
84
|
+
|
|
85
|
+
## Modes
|
|
86
|
+
|
|
87
|
+
- `native` returns existing captions and never starts speech transcription.
|
|
88
|
+
- `auto` uses existing captions first and generates a transcript only when needed.
|
|
89
|
+
- `generate` creates a transcript from the video audio.
|
|
90
|
+
|
|
91
|
+
## Errors
|
|
92
|
+
|
|
93
|
+
Failed requests raise `CapslaneError`. The error includes the HTTP status, Capslane error code and request ID when available.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from capslane import CapslaneError
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
capslane.transcript("invalid")
|
|
100
|
+
except CapslaneError as error:
|
|
101
|
+
print(error.status, error.code, error.request_id)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Security
|
|
105
|
+
|
|
106
|
+
Use Capslane from a trusted server. Do not expose API keys in browser code, public repositories or client-side environment variables.
|
|
107
|
+
|
|
108
|
+
## Links
|
|
109
|
+
|
|
110
|
+
- [Documentation](https://capslane.com/docs)
|
|
111
|
+
- [API reference](https://capslane.com/api-reference)
|
|
112
|
+
- [Dashboard](https://capslane.com/dashboard)
|
|
113
|
+
- [GitHub](https://github.com/Webba-Creative-Technologies/capslane-python)
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
capslane-0.1.0/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Capslane Python SDK
|
|
2
|
+
|
|
3
|
+
The Capslane Python SDK retrieves YouTube transcripts from Python applications. It returns existing captions when available and can generate a transcript when a video has no usable caption track. The package uses the Python standard library and has no runtime dependency.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Python 3.10 or later
|
|
8
|
+
- A Capslane API key from the [dashboard](https://capslane.com/api-keys)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install capslane
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Retrieve a transcript
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
from capslane import CapslaneClient
|
|
22
|
+
|
|
23
|
+
capslane = CapslaneClient(os.environ["CAPSLANE_API_KEY"])
|
|
24
|
+
|
|
25
|
+
result = capslane.transcript(
|
|
26
|
+
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
27
|
+
mode="auto",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
transcript = (
|
|
31
|
+
capslane.wait_for_transcript(result)
|
|
32
|
+
if "jobId" in result
|
|
33
|
+
else result
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
print(transcript["content"])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Check a transcript job
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
job = capslane.transcript_job(
|
|
43
|
+
"job_00000000-0000-0000-0000-000000000000"
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`wait_for_transcript` polls an accepted job until it completes, fails or reaches the timeout.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
transcript = capslane.wait_for_transcript(
|
|
51
|
+
job,
|
|
52
|
+
interval=2.0,
|
|
53
|
+
timeout=20 * 60,
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Request options
|
|
58
|
+
|
|
59
|
+
| Option | Type | Description |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `url` | `str` | Public YouTube URL or 11-character video ID. |
|
|
62
|
+
| `lang` | `str` | Optional preferred language code. |
|
|
63
|
+
| `mode` | `native`, `auto` or `generate` | Selects how Capslane obtains the transcript. |
|
|
64
|
+
| `text` | `bool` | Returns one text string instead of timestamped segments. |
|
|
65
|
+
| `chunk_size` | `int` | Groups transcript segments into larger chunks. |
|
|
66
|
+
|
|
67
|
+
## Modes
|
|
68
|
+
|
|
69
|
+
- `native` returns existing captions and never starts speech transcription.
|
|
70
|
+
- `auto` uses existing captions first and generates a transcript only when needed.
|
|
71
|
+
- `generate` creates a transcript from the video audio.
|
|
72
|
+
|
|
73
|
+
## Errors
|
|
74
|
+
|
|
75
|
+
Failed requests raise `CapslaneError`. The error includes the HTTP status, Capslane error code and request ID when available.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from capslane import CapslaneError
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
capslane.transcript("invalid")
|
|
82
|
+
except CapslaneError as error:
|
|
83
|
+
print(error.status, error.code, error.request_id)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Security
|
|
87
|
+
|
|
88
|
+
Use Capslane from a trusted server. Do not expose API keys in browser code, public repositories or client-side environment variables.
|
|
89
|
+
|
|
90
|
+
## Links
|
|
91
|
+
|
|
92
|
+
- [Documentation](https://capslane.com/docs)
|
|
93
|
+
- [API reference](https://capslane.com/api-reference)
|
|
94
|
+
- [Dashboard](https://capslane.com/dashboard)
|
|
95
|
+
- [GitHub](https://github.com/Webba-Creative-Technologies/capslane-python)
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "capslane"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client for retrieving YouTube transcripts through the Capslane API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Webba Creative Technologies", email = "luca.deguin@webba-creative.com" }]
|
|
13
|
+
keywords = ["youtube", "transcript", "captions", "video-to-text", "api"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Documentation = "https://capslane.com/docs"
|
|
24
|
+
Repository = "https://github.com/Webba-Creative-Technologies/capslane-python"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/capslane"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.error import HTTPError, URLError
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
from urllib.request import Request, urlopen
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CapslaneError(Exception):
|
|
14
|
+
status: int
|
|
15
|
+
code: str
|
|
16
|
+
message: str
|
|
17
|
+
request_id: str | None = None
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
return f"{self.code}: {self.message}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CapslaneClient:
|
|
24
|
+
def __init__(self, api_key: str, base_url: str = "https://capslane.com", timeout: float = 20.0) -> None:
|
|
25
|
+
if not api_key.strip():
|
|
26
|
+
raise ValueError("api_key is required")
|
|
27
|
+
self.api_key = api_key
|
|
28
|
+
self.base_url = base_url.rstrip("/")
|
|
29
|
+
self.timeout = timeout
|
|
30
|
+
|
|
31
|
+
def transcript(
|
|
32
|
+
self,
|
|
33
|
+
url: str,
|
|
34
|
+
*,
|
|
35
|
+
lang: str | None = None,
|
|
36
|
+
text: bool | None = None,
|
|
37
|
+
chunk_size: int | None = None,
|
|
38
|
+
mode: str | None = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
query: dict[str, str] = {"url": url}
|
|
41
|
+
if lang:
|
|
42
|
+
query["lang"] = lang
|
|
43
|
+
if text is not None:
|
|
44
|
+
query["text"] = str(text).lower()
|
|
45
|
+
if chunk_size is not None:
|
|
46
|
+
query["chunkSize"] = str(chunk_size)
|
|
47
|
+
if mode:
|
|
48
|
+
query["mode"] = mode
|
|
49
|
+
return self._get(f"/v1/transcript?{urlencode(query)}")
|
|
50
|
+
|
|
51
|
+
def transcript_job(self, job_id: str) -> dict[str, Any]:
|
|
52
|
+
return self._get(f"/v1/transcript/{job_id}")
|
|
53
|
+
|
|
54
|
+
def wait_for_transcript(
|
|
55
|
+
self,
|
|
56
|
+
job: dict[str, Any] | str,
|
|
57
|
+
*,
|
|
58
|
+
interval: float = 2.0,
|
|
59
|
+
timeout: float = 20 * 60,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
job_id = job if isinstance(job, str) else str(job["jobId"])
|
|
62
|
+
deadline = time.monotonic() + timeout
|
|
63
|
+
while time.monotonic() < deadline:
|
|
64
|
+
time.sleep(interval)
|
|
65
|
+
result = self.transcript_job(job_id)
|
|
66
|
+
if "content" in result:
|
|
67
|
+
return result
|
|
68
|
+
if result.get("status") in {"failed", "cancelled"}:
|
|
69
|
+
raise CapslaneError(422, str(result.get("error", result["status"])), f"Transcript job {result['status']}", result.get("requestId"))
|
|
70
|
+
raise CapslaneError(504, "processing_timeout", "Transcript job deadline exceeded")
|
|
71
|
+
|
|
72
|
+
def _get(self, path: str) -> dict[str, Any]:
|
|
73
|
+
request = Request(
|
|
74
|
+
f"{self.base_url}{path}",
|
|
75
|
+
headers={"x-api-key": self.api_key, "accept": "application/json"},
|
|
76
|
+
)
|
|
77
|
+
try:
|
|
78
|
+
with urlopen(request, timeout=self.timeout) as response:
|
|
79
|
+
return json.loads(response.read().decode("utf-8"))
|
|
80
|
+
except HTTPError as error:
|
|
81
|
+
body = self._error_body(error)
|
|
82
|
+
raise CapslaneError(error.code, str(body.get("error", "request_failed")), str(body.get("message", "Capslane request failed")), body.get("requestId")) from error
|
|
83
|
+
except URLError as error:
|
|
84
|
+
raise CapslaneError(0, "network_error", str(error.reason)) from error
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _error_body(error: HTTPError) -> dict[str, Any]:
|
|
88
|
+
try:
|
|
89
|
+
return json.loads(error.read().decode("utf-8"))
|
|
90
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
91
|
+
return {"error": "request_failed", "message": "Capslane request failed"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
|
8
|
+
|
|
9
|
+
from capslane import CapslaneClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FakeResponse:
|
|
13
|
+
def __enter__(self):
|
|
14
|
+
return self
|
|
15
|
+
|
|
16
|
+
def __exit__(self, *_args):
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
def read(self):
|
|
20
|
+
return json.dumps({"content": [], "lang": "en", "requestId": "req_test"}).encode()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CapslaneClientTest(unittest.TestCase):
|
|
24
|
+
@patch("capslane.client.urlopen", return_value=FakeResponse())
|
|
25
|
+
def test_api_key_stays_in_header(self, mocked_urlopen):
|
|
26
|
+
client = CapslaneClient("vxl_test_secret")
|
|
27
|
+
result = client.transcript("dQw4w9WgXcQ", mode="native")
|
|
28
|
+
request = mocked_urlopen.call_args.args[0]
|
|
29
|
+
self.assertEqual(result["requestId"], "req_test")
|
|
30
|
+
self.assertNotIn("vxl_test_secret", request.full_url)
|
|
31
|
+
self.assertEqual(request.headers["X-api-key"], "vxl_test_secret")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
unittest.main()
|