dadascribe 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.
- dadascribe-0.1.0/.github/workflows/publish.yml +46 -0
- dadascribe-0.1.0/.gitignore +7 -0
- dadascribe-0.1.0/LICENSE +21 -0
- dadascribe-0.1.0/PKG-INFO +74 -0
- dadascribe-0.1.0/README.md +56 -0
- dadascribe-0.1.0/pyproject.toml +35 -0
- dadascribe-0.1.0/run_tests.sh +2 -0
- dadascribe-0.1.0/src/dadascribe/__init__.py +2 -0
- dadascribe-0.1.0/src/dadascribe/cli.py +127 -0
- dadascribe-0.1.0/src/dadascribe/internal_globals.py +4 -0
- dadascribe-0.1.0/src/dadascribe/request_utils.py +173 -0
- dadascribe-0.1.0/src/dadascribe/wrapper.py +116 -0
- dadascribe-0.1.0/tests/test_internal_globals.py +14 -0
- dadascribe-0.1.0/tests/test_request_utils.py +175 -0
- dadascribe-0.1.0/tests/test_wrapper.py +203 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Publish Python Package to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release-build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.x"
|
|
18
|
+
- name: Install build dependencies
|
|
19
|
+
run: |
|
|
20
|
+
python -m pip install --upgrade pip
|
|
21
|
+
python -m pip install build
|
|
22
|
+
- name: Build release distributions
|
|
23
|
+
run: python -m build
|
|
24
|
+
- name: Upload distributions
|
|
25
|
+
uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: release-dists
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
pypi-publish:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
needs:
|
|
33
|
+
- release-build
|
|
34
|
+
permissions:
|
|
35
|
+
id-token: write
|
|
36
|
+
environment:
|
|
37
|
+
name: pypi
|
|
38
|
+
url: https://pypi.org/p/dadascribe
|
|
39
|
+
steps:
|
|
40
|
+
- name: Retrieve release distributions
|
|
41
|
+
uses: actions/download-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: release-dists
|
|
44
|
+
path: dist/
|
|
45
|
+
- name: Publish to PyPI
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
dadascribe-0.1.0/LICENSE
ADDED
|
@@ -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.
|
|
@@ -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
|
+

|
|
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,56 @@
|
|
|
1
|
+
# Python wrapper for the DaDaScribe API
|
|
2
|
+
|
|
3
|
+
The official DaDaScribe API wrapper in Python.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## CLI Interface
|
|
8
|
+
The wrapper offers a CLI interface.
|
|
9
|
+
|
|
10
|
+
To start a job, for example:
|
|
11
|
+
```bash
|
|
12
|
+
$ dadascribe --source "link/to/youtube/or/path/to/file"
|
|
13
|
+
{
|
|
14
|
+
"status": "ok",
|
|
15
|
+
"id": "a7wuTaPrebOqheE0",
|
|
16
|
+
"count": 1
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Check a job status:
|
|
21
|
+
```bash
|
|
22
|
+
$ dadascribe --status a7wuTaPrebOqheE0
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Easily download results of a completed job:
|
|
26
|
+
```bash
|
|
27
|
+
$ dadadascribe --download a7wuTaPrebOqheE0
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Python API
|
|
31
|
+
The wrapper offers a Python API.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from dadascribe import ScribeAPIWrapper
|
|
35
|
+
api_key = ... # e.g. from an environment variable, as str
|
|
36
|
+
w = ScribeAPIWrapper(api_key)
|
|
37
|
+
|
|
38
|
+
# Example transcription:
|
|
39
|
+
trsc_result = w.transcribe(
|
|
40
|
+
source="youtube/link/or/path/to/file",
|
|
41
|
+
source_language="en",
|
|
42
|
+
destination_language="es,it",
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Development
|
|
48
|
+
```bash
|
|
49
|
+
pip3 install -e .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Make sure to run the tests as well via the run_tests.sh script:
|
|
53
|
+
```bash
|
|
54
|
+
chmod +x run_tests.sh
|
|
55
|
+
./run_tests.sh
|
|
56
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dadascribe"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "PatzEdi", email = "patzedigithub@gmail.com" },
|
|
10
|
+
{ name = "Fabrizio Ferrari", email = "fabriziovsm@gmail.com" }
|
|
11
|
+
]
|
|
12
|
+
description = "Python wrapper for the DaDaScribe API"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
keywords = ["transcriptions", "diarization", "whisper"]
|
|
15
|
+
requires-python = ">=3.13"
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
license="MIT"
|
|
22
|
+
license-files=["LICENSE"]
|
|
23
|
+
|
|
24
|
+
dependencies = ["requests"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
dadascribe = "dadascribe.cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/dadascribe"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
"Homepage" = "https://api.dadascribe.com/"
|
|
34
|
+
"Repository" = "https://github.com/PatzEdi/dadascribe-api-python/"
|
|
35
|
+
"Bug Tracker" = "https://github.com/PatzEdi/dadascribe-api-python/issues"
|
|
@@ -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,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
|
|
@@ -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,14 @@
|
|
|
1
|
+
|
|
2
|
+
import unittest
|
|
3
|
+
from dadascribe.internal_globals import ENV_API_NAME
|
|
4
|
+
|
|
5
|
+
# Mostly just checking that when we modify these, we
|
|
6
|
+
# really want to modify them.
|
|
7
|
+
|
|
8
|
+
class TestInternalGlobals(unittest.TestCase):
|
|
9
|
+
def test_env_api_name(self):
|
|
10
|
+
self.assertEqual(ENV_API_NAME, "DADASCRIBE_API_KEY")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
unittest.main()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch, mock_open
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
from src.dadascribe.request_utils import (
|
|
6
|
+
InternalRequestError,
|
|
7
|
+
InvalidFileError,
|
|
8
|
+
RequestUtils,
|
|
9
|
+
extract_response_content,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestRequestUtils(unittest.TestCase):
|
|
14
|
+
def setUp(self):
|
|
15
|
+
self._request_utils = RequestUtils()
|
|
16
|
+
|
|
17
|
+
def test_extract_response_content(self):
|
|
18
|
+
resp = requests.Response()
|
|
19
|
+
resp._content = b'{"key": "value"}'
|
|
20
|
+
result = extract_response_content(resp)
|
|
21
|
+
self.assertIsInstance(result, dict)
|
|
22
|
+
self.assertEqual(result, {"key": "value"})
|
|
23
|
+
|
|
24
|
+
# Now with a list:
|
|
25
|
+
resp._content = b'["response", "content", "as", "list"]'
|
|
26
|
+
result = extract_response_content(resp)
|
|
27
|
+
self.assertIsInstance(result, list)
|
|
28
|
+
self.assertEqual(result, ["response", "content", "as", "list"])
|
|
29
|
+
|
|
30
|
+
# Now with a string:
|
|
31
|
+
resp._content = b"response as a string"
|
|
32
|
+
result = extract_response_content(resp)
|
|
33
|
+
self.assertIsInstance(result, str)
|
|
34
|
+
self.assertEqual(result, "response as a string")
|
|
35
|
+
|
|
36
|
+
def test_construct_headers(self):
|
|
37
|
+
"""Makes sure that the headers we construct
|
|
38
|
+
are of the expected form. This is more of a confirmation
|
|
39
|
+
that we want to change this header format in the code."""
|
|
40
|
+
headers = self._request_utils.construct_headers("dummy_api_key")
|
|
41
|
+
self.assertIsInstance(headers, dict)
|
|
42
|
+
self.assertEqual(
|
|
43
|
+
headers,
|
|
44
|
+
{
|
|
45
|
+
"Authorization": "Bearer dummy_api_key",
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@patch("requests.post", return_value=requests.Response())
|
|
51
|
+
def test_exec_request_calls_post_correctly(self, mock_post):
|
|
52
|
+
resp = mock_post.return_value
|
|
53
|
+
resp._content = b'{"dummy": "response"}'
|
|
54
|
+
resp.status_code = 200
|
|
55
|
+
self._request_utils.exec_request(
|
|
56
|
+
"dummy_api_key", "https://dummy.url", {}
|
|
57
|
+
)
|
|
58
|
+
mock_post.assert_called_once()
|
|
59
|
+
mock_post.assert_called_with(
|
|
60
|
+
"https://dummy.url",
|
|
61
|
+
headers={
|
|
62
|
+
"Authorization": "Bearer dummy_api_key",
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
},
|
|
65
|
+
json={},
|
|
66
|
+
timeout=0,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@patch("requests.post", return_value=requests.Response())
|
|
70
|
+
def test_exec_request_raises_internal_request_error_on_status_raised(
|
|
71
|
+
self, mock_post
|
|
72
|
+
):
|
|
73
|
+
resp = mock_post.return_value
|
|
74
|
+
resp.status_code = 500
|
|
75
|
+
resp.reason = "Bad Request"
|
|
76
|
+
with self.assertRaises(InternalRequestError) as context:
|
|
77
|
+
self._request_utils.exec_request(
|
|
78
|
+
"dummy_api_key", "https://dummy.url", {}
|
|
79
|
+
)
|
|
80
|
+
self.assertEqual(
|
|
81
|
+
str(context.exception), "Request failed: 500 Bad Request"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@patch("requests.post", return_value=requests.Response())
|
|
85
|
+
def test_exec_request_returns_formatted_extraction_content_output(
|
|
86
|
+
self, mock_post
|
|
87
|
+
):
|
|
88
|
+
resp = mock_post.return_value
|
|
89
|
+
resp._content = b'["response", "content", "as", "list"]'
|
|
90
|
+
resp.status_code = 200
|
|
91
|
+
result = self._request_utils.exec_request(
|
|
92
|
+
"dummy_api_key", "https://dummy.url", {}
|
|
93
|
+
)
|
|
94
|
+
self.assertIsInstance(result, list)
|
|
95
|
+
self.assertEqual(result, ["response", "content", "as", "list"])
|
|
96
|
+
|
|
97
|
+
def test_is_link(self):
|
|
98
|
+
"""Test is_link correctly identifies URLs and non-URLs."""
|
|
99
|
+
# Valid URLs
|
|
100
|
+
self.assertTrue(self._request_utils.is_link("https://example.com"))
|
|
101
|
+
self.assertTrue(self._request_utils.is_link("http://example.com"))
|
|
102
|
+
self.assertTrue(
|
|
103
|
+
self._request_utils.is_link("https://example.com/path/to/file.mp3")
|
|
104
|
+
)
|
|
105
|
+
self.assertTrue(self._request_utils.is_link("ftp://example.com"))
|
|
106
|
+
|
|
107
|
+
# Not valid URLs (no scheme or no netloc)
|
|
108
|
+
self.assertFalse(self._request_utils.is_link("example.com"))
|
|
109
|
+
self.assertFalse(self._request_utils.is_link("/path/to/file.mp3"))
|
|
110
|
+
self.assertFalse(self._request_utils.is_link("file.mp3"))
|
|
111
|
+
self.assertFalse(self._request_utils.is_link("just_a_string"))
|
|
112
|
+
self.assertFalse(self._request_utils.is_link(""))
|
|
113
|
+
|
|
114
|
+
@patch(
|
|
115
|
+
"builtins.open",
|
|
116
|
+
new_callable=mock_open,
|
|
117
|
+
read_data=b"file content",
|
|
118
|
+
)
|
|
119
|
+
@patch("pathlib.Path.is_file", return_value=True)
|
|
120
|
+
@patch("pathlib.Path.exists", return_value=True)
|
|
121
|
+
@patch("requests.post", return_value=requests.Response())
|
|
122
|
+
def test_exec_request_with_file(
|
|
123
|
+
self, mock_post, mock_exists, mock_is_file, mock_open_file
|
|
124
|
+
):
|
|
125
|
+
"""Test that exec_request correctly handles file uploads."""
|
|
126
|
+
resp = mock_post.return_value
|
|
127
|
+
resp._content = b'{"id": "test_id"}'
|
|
128
|
+
resp.status_code = 200
|
|
129
|
+
|
|
130
|
+
result = self._request_utils.exec_request(
|
|
131
|
+
"dummy_api_key",
|
|
132
|
+
"https://dummy.url",
|
|
133
|
+
{"key": "value"},
|
|
134
|
+
file="/path/to/test.mp3",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
mock_post.assert_called_once()
|
|
138
|
+
call_args = mock_post.call_args
|
|
139
|
+
|
|
140
|
+
# Verify headers don't include Content-Type (multipart/form-data is automatic)
|
|
141
|
+
self.assertEqual(
|
|
142
|
+
call_args.kwargs["headers"],
|
|
143
|
+
{"Authorization": "Bearer dummy_api_key"},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Verify files parameter is present
|
|
147
|
+
self.assertIn("files", call_args.kwargs)
|
|
148
|
+
self.assertEqual(result, {"id": "test_id"})
|
|
149
|
+
|
|
150
|
+
@patch("pathlib.Path.is_file", return_value=False)
|
|
151
|
+
def test_construct_file_payload_raises_on_non_file(
|
|
152
|
+
self, mock_is_file
|
|
153
|
+
):
|
|
154
|
+
"""Test that construct_file_payload raises InvalidFileError for non-files."""
|
|
155
|
+
with self.assertRaises(InvalidFileError) as context:
|
|
156
|
+
self._request_utils.construct_file_payload("/path/to/directory")
|
|
157
|
+
self.assertIn(
|
|
158
|
+
"File not found or is not a file", str(context.exception)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def test_construct_headers_with_include_content_false(self):
|
|
162
|
+
"""Test construct_headers without Content-Type for file uploads."""
|
|
163
|
+
headers = self._request_utils.construct_headers(
|
|
164
|
+
"dummy_api_key", include_content=False
|
|
165
|
+
)
|
|
166
|
+
self.assertIsInstance(headers, dict)
|
|
167
|
+
self.assertEqual(
|
|
168
|
+
headers,
|
|
169
|
+
{"Authorization": "Bearer dummy_api_key"},
|
|
170
|
+
)
|
|
171
|
+
self.assertNotIn("Content-Type", headers)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
unittest.main()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import MagicMock, mock_open, patch
|
|
3
|
+
|
|
4
|
+
from src.dadascribe.request_utils import (
|
|
5
|
+
BASE_API_URL,
|
|
6
|
+
EndPoints,
|
|
7
|
+
PayLoadKeys,
|
|
8
|
+
ResponseKeys,
|
|
9
|
+
Status,
|
|
10
|
+
)
|
|
11
|
+
from src.dadascribe.wrapper import DownloadError, ScribeAPIWrapper
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestWrapper(unittest.TestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
self._wrapper = ScribeAPIWrapper(api_key="")
|
|
17
|
+
|
|
18
|
+
@patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
|
|
19
|
+
def test_transcribe_calls_api_request_correctly(self, mock_api_request):
|
|
20
|
+
"""Checks that the transcribe function calls _api_request
|
|
21
|
+
with the correct arguments, such as the payload."""
|
|
22
|
+
self._wrapper.transcribe(
|
|
23
|
+
source="https://example_source_url.com",
|
|
24
|
+
source_language="en",
|
|
25
|
+
destination_language="it",
|
|
26
|
+
)
|
|
27
|
+
mock_api_request.assert_called_once()
|
|
28
|
+
mock_api_request.assert_called_with(
|
|
29
|
+
BASE_API_URL + EndPoints.TRANSCRIBE,
|
|
30
|
+
payload={
|
|
31
|
+
PayLoadKeys.SOURCE_LANGUAGE: "en",
|
|
32
|
+
PayLoadKeys.DEST_LANGUAGE: "it",
|
|
33
|
+
PayLoadKeys.SOURCE: "https://example_source_url.com",
|
|
34
|
+
},
|
|
35
|
+
file=None,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
mock_api_request.reset_mock()
|
|
39
|
+
# Make sure it works out with diarization as well:
|
|
40
|
+
# Note: "example_source_url" without a protocol is treated as a file
|
|
41
|
+
self._wrapper.transcribe(
|
|
42
|
+
source="example_source_url",
|
|
43
|
+
source_language="en",
|
|
44
|
+
destination_language="it",
|
|
45
|
+
diarization="speaker1,speaker2",
|
|
46
|
+
)
|
|
47
|
+
mock_api_request.assert_called_once()
|
|
48
|
+
mock_api_request.assert_called_with(
|
|
49
|
+
BASE_API_URL + EndPoints.TRANSCRIBE,
|
|
50
|
+
payload={
|
|
51
|
+
PayLoadKeys.SOURCE_LANGUAGE: "en",
|
|
52
|
+
PayLoadKeys.DEST_LANGUAGE: "it",
|
|
53
|
+
PayLoadKeys.DIARIZATION: "speaker1,speaker2",
|
|
54
|
+
},
|
|
55
|
+
file="example_source_url",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# As a list for the source:
|
|
59
|
+
mock_api_request.reset_mock()
|
|
60
|
+
self._wrapper.transcribe(
|
|
61
|
+
source=["example_source_url", "example_source_url2"],
|
|
62
|
+
source_language="en",
|
|
63
|
+
destination_language="it",
|
|
64
|
+
diarization="speaker1,speaker2",
|
|
65
|
+
)
|
|
66
|
+
mock_api_request.assert_called_once()
|
|
67
|
+
mock_api_request.assert_called_with(
|
|
68
|
+
BASE_API_URL + EndPoints.TRANSCRIBE,
|
|
69
|
+
payload={
|
|
70
|
+
PayLoadKeys.SOURCE: [
|
|
71
|
+
"example_source_url",
|
|
72
|
+
"example_source_url2",
|
|
73
|
+
],
|
|
74
|
+
PayLoadKeys.SOURCE_LANGUAGE: "en",
|
|
75
|
+
PayLoadKeys.DEST_LANGUAGE: "it",
|
|
76
|
+
PayLoadKeys.DIARIZATION: "speaker1,speaker2",
|
|
77
|
+
},
|
|
78
|
+
file=None,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
|
|
82
|
+
def test_transcribe_with_file_path(self, mock_api_request):
|
|
83
|
+
"""Test that transcribe correctly handles file paths."""
|
|
84
|
+
# Test with a file path (no protocol scheme)
|
|
85
|
+
self._wrapper.transcribe(
|
|
86
|
+
source="/path/to/audio.mp3",
|
|
87
|
+
source_language="en",
|
|
88
|
+
destination_language="fr",
|
|
89
|
+
)
|
|
90
|
+
mock_api_request.assert_called_once()
|
|
91
|
+
mock_api_request.assert_called_with(
|
|
92
|
+
BASE_API_URL + EndPoints.TRANSCRIBE,
|
|
93
|
+
payload={
|
|
94
|
+
PayLoadKeys.SOURCE_LANGUAGE: "en",
|
|
95
|
+
PayLoadKeys.DEST_LANGUAGE: "fr",
|
|
96
|
+
},
|
|
97
|
+
file="/path/to/audio.mp3",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
|
|
101
|
+
def test_transcribe_with_http_url(self, mock_api_request):
|
|
102
|
+
"""Test that transcribe correctly handles HTTP URLs."""
|
|
103
|
+
self._wrapper.transcribe(
|
|
104
|
+
source="http://example.com/audio.mp3",
|
|
105
|
+
source_language="en",
|
|
106
|
+
destination_language="es",
|
|
107
|
+
)
|
|
108
|
+
mock_api_request.assert_called_once()
|
|
109
|
+
mock_api_request.assert_called_with(
|
|
110
|
+
BASE_API_URL + EndPoints.TRANSCRIBE,
|
|
111
|
+
payload={
|
|
112
|
+
PayLoadKeys.SOURCE_LANGUAGE: "en",
|
|
113
|
+
PayLoadKeys.DEST_LANGUAGE: "es",
|
|
114
|
+
PayLoadKeys.SOURCE: "http://example.com/audio.mp3",
|
|
115
|
+
},
|
|
116
|
+
file=None,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
|
|
120
|
+
def test_transcribe_with_relative_file_path(self, mock_api_request):
|
|
121
|
+
"""Test that transcribe correctly handles relative file paths."""
|
|
122
|
+
self._wrapper.transcribe(
|
|
123
|
+
source="audio_files/recording.wav",
|
|
124
|
+
source_language="de",
|
|
125
|
+
destination_language="en",
|
|
126
|
+
)
|
|
127
|
+
mock_api_request.assert_called_once()
|
|
128
|
+
mock_api_request.assert_called_with(
|
|
129
|
+
BASE_API_URL + EndPoints.TRANSCRIBE,
|
|
130
|
+
payload={
|
|
131
|
+
PayLoadKeys.SOURCE_LANGUAGE: "de",
|
|
132
|
+
PayLoadKeys.DEST_LANGUAGE: "en",
|
|
133
|
+
},
|
|
134
|
+
file="audio_files/recording.wav",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
|
|
138
|
+
def test_transcribe_with_list_of_urls(self, mock_api_request):
|
|
139
|
+
"""Test that transcribe handles a list of URLs correctly."""
|
|
140
|
+
self._wrapper.transcribe(
|
|
141
|
+
source=[
|
|
142
|
+
"https://example.com/audio1.mp3",
|
|
143
|
+
"https://example.com/audio2.mp3",
|
|
144
|
+
],
|
|
145
|
+
source_language="en",
|
|
146
|
+
destination_language="es",
|
|
147
|
+
)
|
|
148
|
+
mock_api_request.assert_called_once()
|
|
149
|
+
mock_api_request.assert_called_with(
|
|
150
|
+
BASE_API_URL + EndPoints.TRANSCRIBE,
|
|
151
|
+
payload={
|
|
152
|
+
PayLoadKeys.SOURCE: [
|
|
153
|
+
"https://example.com/audio1.mp3",
|
|
154
|
+
"https://example.com/audio2.mp3",
|
|
155
|
+
],
|
|
156
|
+
PayLoadKeys.SOURCE_LANGUAGE: "en",
|
|
157
|
+
PayLoadKeys.DEST_LANGUAGE: "es",
|
|
158
|
+
},
|
|
159
|
+
file=None,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
|
|
163
|
+
def test_retrieve_status_calls_api_request_correctly(
|
|
164
|
+
self, mock_api_request
|
|
165
|
+
):
|
|
166
|
+
"""Checks that the retrieve_transcription function calls _api_request
|
|
167
|
+
with the correct arguments, such as the payload."""
|
|
168
|
+
self._wrapper.retrieve_status(id="example_id")
|
|
169
|
+
mock_api_request.assert_called_once()
|
|
170
|
+
mock_api_request.assert_called_with(
|
|
171
|
+
BASE_API_URL + EndPoints.STATUS,
|
|
172
|
+
payload={
|
|
173
|
+
PayLoadKeys.ID: "example_id",
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
@patch("os.path.exists")
|
|
178
|
+
def test_download_trsc_raises_err_when_output_dir_dne(self, mock_exists):
|
|
179
|
+
"""When the output dir does not exist, should raise DownloadError."""
|
|
180
|
+
mock_exists.return_value = False
|
|
181
|
+
with self.assertRaises(DownloadError):
|
|
182
|
+
self._wrapper.download_transcription_output(
|
|
183
|
+
id="example_id", output_dir="example_path"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@patch("os.path.exists")
|
|
187
|
+
@patch("src.dadascribe.wrapper.ScribeAPIWrapper.retrieve_status")
|
|
188
|
+
def test_download_trsc_raises_err_when_status_not_complete(
|
|
189
|
+
self, mock_retrieve_status, mock_exists
|
|
190
|
+
):
|
|
191
|
+
"""When the output dir does not exist, should raise DownloadError."""
|
|
192
|
+
mock_exists.return_value = True
|
|
193
|
+
mock_retrieve_status.return_value = {
|
|
194
|
+
ResponseKeys.STATUS: Status.PROCESSING
|
|
195
|
+
}
|
|
196
|
+
with self.assertRaises(DownloadError):
|
|
197
|
+
self._wrapper.download_transcription_output(
|
|
198
|
+
id="example_id", output_dir="example_path"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
unittest.main()
|