dadascribe 0.1.0__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.
dadascribe/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+
2
+ from .wrapper import ScribeAPIWrapper
dadascribe/cli.py ADDED
@@ -0,0 +1,127 @@
1
+
2
+ import argparse
3
+ import os
4
+ import sys
5
+ import json
6
+
7
+ from .internal_globals import ENV_API_NAME
8
+ from .wrapper import ScribeAPIWrapper, DownloadError
9
+ from .request_utils import InternalRequestError, InvalidFileError
10
+
11
+
12
+ def _check_api_key_presence(args) -> str:
13
+ api_key = args.api_key or os.getenv(ENV_API_NAME)
14
+ if not api_key:
15
+ print(
16
+ "Error: API key required. Provide --api-key " \
17
+ "or set DADASCRIBE_API_KEY env var.",
18
+ file=sys.stderr,
19
+ )
20
+ sys.exit(2)
21
+
22
+ return api_key
23
+
24
+
25
+ def parse_args() -> argparse.Namespace:
26
+ p = argparse.ArgumentParser(
27
+ description="Interact with the DaDaScribe API through the CLI."
28
+ )
29
+ p.add_argument(
30
+ "--api-key",
31
+ help="Dadascribe API key (or set DADASCRIBE_API_KEY env var).",
32
+ )
33
+ p.add_argument(
34
+ "--source",
35
+ default="https://www.youtube.com/watch?v=VIDEO_ID",
36
+ help="Source media URL (e.g. YouTube link) OR file path.",
37
+ )
38
+ p.add_argument(
39
+ "--source-language", default="en", help="Source language (e.g. 'en')."
40
+ )
41
+ p.add_argument(
42
+ "--destination-language",
43
+ default="",
44
+ help="Comma-separated destination languages (e.g. 'it,fr').",
45
+ )
46
+ p.add_argument(
47
+ "--diarization",
48
+ default="",
49
+ help="Comma-separated diarization labels (e.g. 'Alice,Bob').",
50
+ )
51
+ p.add_argument(
52
+ "--timeout", type=int, default=60, help="Request timeout in seconds."
53
+ )
54
+ p.add_argument(
55
+ "--dump-response",
56
+ help="File to save JSON response (optional). "
57
+ "If omitted, prints to stdout.",
58
+ )
59
+
60
+ p.add_argument(
61
+ "--status-id",
62
+ help="Check status for a previously submitted job ID (e.g. " \
63
+ "a1B2c3D4e5F6g7H8). If provided, transcribe will not run.",
64
+ )
65
+
66
+ # For downloading transcription files:
67
+
68
+ p.add_argument(
69
+ "--download",
70
+ help="Download generated files given a job ID " \
71
+ "& an optional output dir path" \
72
+ )
73
+
74
+ p.add_argument(
75
+ "--download-output-dir",
76
+ default=None,
77
+ help="Directory to save downloaded files (optional). " \
78
+ "If omitted, saves to current directory."
79
+ )
80
+
81
+ return p.parse_args()
82
+
83
+
84
+ def _handle_args(args: argparse.Namespace, api_key: str) -> None:
85
+ wrapper = ScribeAPIWrapper(api_key=api_key, req_timeout=args.timeout)
86
+ try:
87
+ if args.status_id:
88
+ # If a status ID is provided, perform a status
89
+ # check and ignore other transcribe options.
90
+ result = wrapper.retrieve_status(id=args.status_id)
91
+ elif args.download:
92
+ result = wrapper.download_transcription_output(
93
+ id=args.download,
94
+ output_dir=args.download_output_dir or os.getcwd(),
95
+ )
96
+ else:
97
+ result = wrapper.transcribe(
98
+ source=args.source,
99
+ source_language=args.source_language,
100
+ destination_language=args.destination_language,
101
+ diarization=args.diarization,
102
+ )
103
+ except (InternalRequestError, InvalidFileError, DownloadError) as e:
104
+ if isinstance(e, DownloadError) or isinstance(e, InvalidFileError):
105
+ print(f"Error: {e.message}", file=sys.stderr)
106
+ elif isinstance(e, InternalRequestError):
107
+ print(f"Error during request: {e.resp_body}", file=sys.stderr)
108
+
109
+ sys.exit(1)
110
+
111
+ if args.dump_response:
112
+ with open(args.dump_response, "w", encoding="utf-8") as f:
113
+ json.dump(result, f, ensure_ascii=False, indent=2)
114
+ print(f"Saved response to {args.dump_response}")
115
+ else:
116
+ # Pretty-print JSON or text
117
+ if isinstance(result, (dict, list)):
118
+ print(json.dumps(result, ensure_ascii=False, indent=2))
119
+ elif result is not None:
120
+ print(result)
121
+
122
+
123
+
124
+ def main() -> None:
125
+ parsed_args = parse_args()
126
+ api_key = _check_api_key_presence(parsed_args)
127
+ _handle_args(parsed_args, api_key)
@@ -0,0 +1,4 @@
1
+
2
+ # Stores important global constants used across the package.
3
+
4
+ ENV_API_NAME: str = "DADASCRIBE_API_KEY"
@@ -0,0 +1,173 @@
1
+ # Stores global constants & utils related to requests.
2
+ from enum import StrEnum
3
+ from io import BufferedReader
4
+ from typing import Optional, Any
5
+ from urllib.parse import urlparse
6
+ from pathlib import Path
7
+ from os import PathLike
8
+
9
+ import requests
10
+ import json
11
+
12
+ BASE_API_URL: str = "https://api.dadascribe.com/v1/"
13
+
14
+
15
+ class EndPoints(StrEnum):
16
+ """Common endpoint names for the API."""
17
+
18
+ STATUS = "status"
19
+ TRANSCRIBE = "transcribe"
20
+
21
+
22
+ class PayLoadKeys(StrEnum):
23
+ """Common payload keys for the API."""
24
+
25
+ ID = "id"
26
+ SOURCE = "source"
27
+ SOURCE_LANGUAGE = "source-language"
28
+ DEST_LANGUAGE = "destination-language"
29
+ DIARIZATION = "diarization"
30
+
31
+
32
+ class ResponseKeys(StrEnum):
33
+ """Common response keys for the API."""
34
+
35
+ STATUS = "status"
36
+ URLS = "urls"
37
+
38
+
39
+ class Status(StrEnum):
40
+ """Common status values for the API."""
41
+
42
+ COMPLETE = "complete"
43
+ PROCESSING = "processing"
44
+ ERROR = "error"
45
+
46
+
47
+ def extract_response_content(resp: requests.Response) -> str:
48
+ """Helper to extract the response content as JSON or raw text."""
49
+ try:
50
+ return resp.json()
51
+ except ValueError:
52
+ return resp.text
53
+
54
+
55
+ class InternalRequestError(Exception):
56
+ """Raised when a request fails with a non-200 status code.
57
+ Contains the response object for debugging and other useful
58
+ information about the failure."""
59
+
60
+ def __init__(self, response: requests.Response):
61
+ self._response = response
62
+ self._message = (
63
+ f"Request failed: {response.status_code} {response.reason}"
64
+ )
65
+
66
+ super().__init__(self.message)
67
+
68
+ @property
69
+ def response(self) -> requests.Response:
70
+ return self._response
71
+
72
+ @property
73
+ def message(self) -> str:
74
+ return self._message
75
+
76
+ @property
77
+ def resp_body(self) -> str:
78
+ return extract_response_content(self._response)
79
+
80
+
81
+ class InvalidFileError(Exception):
82
+ """Raised when an inputted file is not valid."""
83
+
84
+ def __init__(self, message: str):
85
+ self._message = message
86
+ super().__init__(self._message)
87
+
88
+ @property
89
+ def message(self) -> str:
90
+ return self._message
91
+
92
+
93
+ class RequestUtils:
94
+ """Utility class for making HTTP requests,
95
+ handling common request logic."""
96
+
97
+ def exec_request(
98
+ self,
99
+ api_key: str,
100
+ url: str,
101
+ payload: Optional[dict],
102
+ timeout: int = 0,
103
+ get: bool = False,
104
+ file: Optional[PathLike] | Optional[str] = None
105
+ ) -> str | None:
106
+ """Executes a POST/GET request to the given URL with the given headers
107
+ and payload, and returns the response as JSON or raw text."""
108
+ req_fn = requests.get if get else requests.post
109
+ if file is not None:
110
+ headers = self.construct_headers(api_key, include_content=False)
111
+ payload, file_obj = self.construct_file_payload(file, payload)
112
+ resp = req_fn(
113
+ url,
114
+ headers=headers,
115
+ files=payload, timeout=timeout
116
+ )
117
+ file_obj.close()
118
+ else:
119
+ headers = self.construct_headers(api_key)
120
+ resp = req_fn(
121
+ url,
122
+ headers=headers,
123
+ json=payload, timeout=timeout
124
+ )
125
+
126
+ try:
127
+ resp.raise_for_status()
128
+ except requests.HTTPError:
129
+ raise InternalRequestError(resp)
130
+
131
+ # Return JSON if possible, otherwise raw text
132
+ return extract_response_content(resp)
133
+
134
+
135
+ def construct_headers(
136
+ self,
137
+ api_key: str,
138
+ include_content: bool = True
139
+ ) -> dict:
140
+ """Contructs a commonly used headers dict with the given API key."""
141
+ headers = {
142
+ "Authorization": f"Bearer {api_key}",
143
+ }
144
+ if include_content:
145
+ headers["Content-Type"] = "application/json"
146
+ return headers
147
+
148
+ def is_link(self, object: Any) -> bool:
149
+ """Returns whether a given string is a link or not."""
150
+ try:
151
+ stringed = str(object)
152
+ result = urlparse(stringed)
153
+ return all([result.scheme, result.netloc])
154
+ except ValueError:
155
+ return False
156
+
157
+
158
+ def construct_file_payload(
159
+ self,
160
+ file: PathLike | str,
161
+ params: Optional[dict] = None
162
+ ) -> tuple[dict, BufferedReader]:
163
+ """Constructs a file payload dict from the given file path."""
164
+ file = Path(file)
165
+ if not file.is_file():
166
+ raise InvalidFileError(f"File not found or is not a file: {file}")
167
+
168
+ file_obj = open(file, "rb")
169
+ file_payload = {
170
+ "file": (file.name, file_obj),
171
+ "data": (None, json.dumps(params or {}), "application/json"),
172
+ }
173
+ return file_payload, file_obj
dadascribe/wrapper.py ADDED
@@ -0,0 +1,116 @@
1
+ import os
2
+ from os import PathLike
3
+ from typing import Any, Optional
4
+
5
+ from .request_utils import (
6
+ BASE_API_URL,
7
+ EndPoints,
8
+ PayLoadKeys,
9
+ RequestUtils,
10
+ ResponseKeys,
11
+ Status,
12
+ )
13
+
14
+
15
+ class DownloadError(Exception):
16
+ """Raised when download fails."""
17
+
18
+ def __init__(self, message: str):
19
+ self.message = message
20
+ super().__init__(self.message)
21
+
22
+
23
+ class ScribeAPIWrapper:
24
+ def __init__(self, api_key: str, req_timeout: int = 60):
25
+ self._request_utils = RequestUtils()
26
+ self._api_key = api_key
27
+ self._req_timeout = req_timeout
28
+
29
+ def transcribe(
30
+ self,
31
+ source: str | list[str] | PathLike,
32
+ source_language: str,
33
+ destination_language: str,
34
+ diarization: Optional[str] = None,
35
+ ) -> Any:
36
+ """Send a POST request to the Dadascribe /v1/transcribe
37
+ endpoint and return the parsed response.
38
+
39
+ Raises InvalidRequestError on failing requests.
40
+ """
41
+ url = BASE_API_URL + EndPoints.TRANSCRIBE
42
+ payload: dict[str, Any] = {
43
+ PayLoadKeys.SOURCE_LANGUAGE: source_language,
44
+ PayLoadKeys.DEST_LANGUAGE: destination_language,
45
+ }
46
+ if diarization is not None:
47
+ payload[PayLoadKeys.DIARIZATION] = diarization
48
+ file_source = None
49
+ if isinstance(source, list) or self._request_utils.is_link(source):
50
+ print("Source detected as LINK or LIST of LINKS.")
51
+ payload[PayLoadKeys.SOURCE] = source
52
+ else:
53
+ print("Source detected as FILE.")
54
+ # Construct files data:
55
+ file_source = source
56
+
57
+ return self._api_request(url, payload=payload, file=file_source)
58
+
59
+ def retrieve_status(self, id: str) -> Any:
60
+ """Send a POST request to the Dadascribe /v1/status endpoint
61
+ and return the parsed response.
62
+
63
+ Raises InvalidRequestError on failing requests.
64
+ """
65
+ url = BASE_API_URL + EndPoints.STATUS
66
+ payload = {PayLoadKeys.ID: id}
67
+
68
+ return self._api_request(url, payload=payload)
69
+
70
+ def download_transcription_output(
71
+ self,
72
+ id: str,
73
+ output_dir: str | PathLike,
74
+ ) -> None:
75
+ """Download the transcription output for the given job ID
76
+ to the specified directory. If no directory is specified,
77
+ saves to the current directory.
78
+ """
79
+ if not os.path.exists(output_dir):
80
+ raise DownloadError(
81
+ f'Output directory "{output_dir}" does not exist.'
82
+ )
83
+
84
+ status_info = self.retrieve_status(id)
85
+ if status_info[ResponseKeys.STATUS] != Status.COMPLETE:
86
+ raise DownloadError(
87
+ "Job is not completed yet. Cannot download output."
88
+ )
89
+
90
+ for file_url in status_info.get(ResponseKeys.URLS, []):
91
+ file_name = os.path.basename(file_url)
92
+ file_content = self._api_request(file_url, get=True)
93
+ with open(os.path.join(output_dir, file_name), "w") as f:
94
+ f.write(file_content)
95
+
96
+ def _api_request(
97
+ self,
98
+ url: str,
99
+ payload: Optional[dict] = None,
100
+ get: bool = False,
101
+ file: Optional[Any] = None,
102
+ ) -> Any:
103
+ """Send a POST request to the given URL with the given payload
104
+ and return the parsed response. Higher level version of exec_request
105
+ found in RequestUtils.
106
+
107
+ Raises InvalidRequestError on failing requests.
108
+ """
109
+ return self._request_utils.exec_request(
110
+ self._api_key,
111
+ url,
112
+ payload=payload,
113
+ timeout=self._req_timeout,
114
+ get=get,
115
+ file=file,
116
+ )
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: dadascribe
3
+ Version: 0.1.0
4
+ Summary: Python wrapper for the DaDaScribe API
5
+ Project-URL: Homepage, https://api.dadascribe.com/
6
+ Project-URL: Repository, https://github.com/PatzEdi/dadascribe-api-python/
7
+ Project-URL: Bug Tracker, https://github.com/PatzEdi/dadascribe-api-python/issues
8
+ Author-email: PatzEdi <patzedigithub@gmail.com>, Fabrizio Ferrari <fabriziovsm@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: diarization,transcriptions,whisper
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.13
16
+ Requires-Dist: requests
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Python wrapper for the DaDaScribe API
20
+
21
+ The official DaDaScribe API wrapper in Python.
22
+
23
+ ![DaDaScribe Logo](https://www.dadascribe.com/images/DaDaScribeLogo.svg)
24
+
25
+ ## CLI Interface
26
+ The wrapper offers a CLI interface.
27
+
28
+ To start a job, for example:
29
+ ```bash
30
+ $ dadascribe --source "link/to/youtube/or/path/to/file"
31
+ {
32
+ "status": "ok",
33
+ "id": "a7wuTaPrebOqheE0",
34
+ "count": 1
35
+ }
36
+ ```
37
+
38
+ Check a job status:
39
+ ```bash
40
+ $ dadascribe --status a7wuTaPrebOqheE0
41
+ ```
42
+
43
+ Easily download results of a completed job:
44
+ ```bash
45
+ $ dadadascribe --download a7wuTaPrebOqheE0
46
+ ```
47
+
48
+ ## Python API
49
+ The wrapper offers a Python API.
50
+
51
+ ```python
52
+ from dadascribe import ScribeAPIWrapper
53
+ api_key = ... # e.g. from an environment variable, as str
54
+ w = ScribeAPIWrapper(api_key)
55
+
56
+ # Example transcription:
57
+ trsc_result = w.transcribe(
58
+ source="youtube/link/or/path/to/file",
59
+ source_language="en",
60
+ destination_language="es,it",
61
+ )
62
+ ```
63
+
64
+
65
+ ## Development
66
+ ```bash
67
+ pip3 install -e .
68
+ ```
69
+
70
+ Make sure to run the tests as well via the run_tests.sh script:
71
+ ```bash
72
+ chmod +x run_tests.sh
73
+ ./run_tests.sh
74
+ ```
@@ -0,0 +1,10 @@
1
+ dadascribe/__init__.py,sha256=QJsTVPXQJ-3Wqfdl-bkXJd07boanNiJjFHfQlO1mYoc,38
2
+ dadascribe/cli.py,sha256=7Mg7cZbdp5A2_72wU4Fq_a-AtAU6pU1G1o7LS-v8Zyw,4013
3
+ dadascribe/internal_globals.py,sha256=Cs1gUEqdHHNAp9Bwu7ELVcjbTpLvCYlRdvM2GW0HIaM,104
4
+ dadascribe/request_utils.py,sha256=NQEx0VO_2n4uF6ZqYQuFDolFioR4PLflL4aYXp1beg4,4752
5
+ dadascribe/wrapper.py,sha256=_t8vqZH56ZTqL2dctVQCKk-4sYSMvzQ3gEChB131O-g,3674
6
+ dadascribe-0.1.0.dist-info/METADATA,sha256=yE6uyvLRWmpucyjkP_1CwxWlgII2lkVG1Qjejerh95A,1785
7
+ dadascribe-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
8
+ dadascribe-0.1.0.dist-info/entry_points.txt,sha256=y7OxHUdmxvz3IWFr6iWycgzJmVrLb4jR3CThx7Ta5wk,51
9
+ dadascribe-0.1.0.dist-info/licenses/LICENSE,sha256=v4gcvKFNHtlz30THehoOU7sWNZrnvnAYMPmUfD3bX9I,1081
10
+ dadascribe-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dadascribe = dadascribe.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PatzEdi, Fabrizio Ferrari
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.