-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: remove starter code * docs(README.md): add installation and dev instructions * feat: initial commit of classes and types * refactor(classes.py): combined common lines in requests * docs(classes.py): add docstrings * fix(classes.py): strip params from uri before downloading * docs(README.md): add sample code and envars needed * test(types_test.py): add initial tests * test(classes_test.py): add initial tests * test(classes_test.py): add tests for _request and download_file
- Loading branch information
Showing
15 changed files
with
309 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,62 @@ | ||
# python-project-template | ||
# pyplayht | ||
|
||
Python wrapper for PlayHT API | ||
https://docs.play.ht/reference/api-getting-started | ||
|
||
### Installation | ||
```bash | ||
pip install pyplayht | ||
``` | ||
|
||
### Environmental Variables | ||
Get your keys from https://play.ht/app/api-access | ||
| Name | Value | | ||
| --- | --- | | ||
| `PLAY_HT_USER_ID` | account user id | | ||
| `PLAY_HT_API_KEY` | account secret key | | ||
|
||
### Sample Code | ||
```python | ||
from pathlib import Path | ||
|
||
from pyplayht.classes import Client | ||
|
||
# create new client | ||
client = Client() | ||
|
||
# create new conversion job | ||
job = client.new_conversion_job( | ||
text="Hello, World!", | ||
voice="en-US-JennyNeural", | ||
) | ||
|
||
# check job status | ||
job = client.get_coversion_job_status(job.get("transcriptionId")) | ||
|
||
# download audio from job | ||
data = client.download_file(job.get('audioUrl')) | ||
|
||
# do something with audio bytes | ||
path = Path("demo.mp3") | ||
path.write_bytes(data) | ||
``` | ||
|
||
|
||
### Developer Instructions | ||
Run the dev setup scripts inside `scripts` directory | ||
```bash | ||
├── scripts | ||
│ ├── setup-dev.bat # windows | ||
│ └── setup-dev.sh # linux | ||
``` | ||
|
||
Install the `pyplayht` package as editable | ||
https://setuptools.pypa.io/en/latest/userguide/development_mode.html | ||
```bash | ||
pip install -e . | ||
``` | ||
|
||
When making a commit, use the command `cz commit` or `cz c` | ||
|
||
You may also use the regular `git commit` command but make sure to follow the `Conventional Commits` specification | ||
https://www.conventionalcommits.org/en/v1.0.0/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
python-dotenv>=0.20.0 | ||
requests>=2.28.2 |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
__version__ = "0.0.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import os | ||
from typing import List, Union | ||
from urllib.parse import urljoin, urlparse, urlunparse | ||
|
||
import requests | ||
|
||
from pyplayht.types import VoiceType | ||
|
||
|
||
class Client: | ||
base_url: str = "https://api.play.ht/" | ||
|
||
def __init__(self) -> None: | ||
self.session = requests.Session() | ||
# Set default headers for the session | ||
headers = { | ||
"AUTHORIZATION": os.getenv("PLAY_HT_API_KEY"), | ||
"X-USER-ID": os.getenv("PLAY_HT_USER_ID"), | ||
"accept": "application/json", | ||
"content-type": "application/json", | ||
} | ||
self.session.headers.update(headers) | ||
self._voices = [] | ||
|
||
@property | ||
def voices(self) -> List[VoiceType]: | ||
return self._voices if self._voices else self.get_voices() | ||
|
||
def get_voices(self) -> List[VoiceType]: | ||
""" | ||
Get list of available voices from server | ||
Returns: | ||
List[VoiceType]: list of available voice types | ||
""" | ||
path = "/api/v1/getVoices" | ||
response = self._request("GET", path) | ||
voices = response.json().get("voices") | ||
voices = [VoiceType(**voice) for voice in voices] | ||
self._voices = voices | ||
return voices | ||
|
||
def new_conversion_job( | ||
self, | ||
text: Union[str, List[str]], | ||
voice: str = "en-US-JennyNeural", | ||
) -> dict: | ||
""" | ||
Create new transcription job | ||
Args: | ||
text (Union[str, List[str]]): text to transcribe | ||
voice (str, optional): voice model to use. | ||
Defaults to "en-US-JennyNeural". | ||
Returns: | ||
dict: new transcription job details | ||
""" | ||
path = "/api/v1/convert" | ||
content = text if isinstance(text, list) else [text] | ||
payload = { | ||
"content": content, | ||
"voice": voice, | ||
} | ||
response = self._request("POST", path, json=payload) | ||
return response.json() | ||
|
||
def get_coversion_job_status(self, transcription_id: str) -> dict: | ||
""" | ||
Check status of job specified by transcription_id | ||
Args: | ||
transcription_id (str): job to check | ||
Returns: | ||
dict: status of specified job | ||
""" | ||
path = "/api/v1/articleStatus" | ||
params = {"transcriptionId": transcription_id} | ||
response = self._request("GET", path, params=params) | ||
return response.json() | ||
|
||
def download_file(self, uri: str) -> bytes: | ||
""" | ||
Download bytes of given file | ||
Args: | ||
uri (str): location of file | ||
Returns: | ||
bytes: byte data of file | ||
""" | ||
parsed_url = urlparse(uri) | ||
# Create a new URL without the query string | ||
new_url = urlunparse( | ||
( | ||
parsed_url.scheme, | ||
parsed_url.netloc, | ||
parsed_url.path, | ||
"", | ||
"", | ||
"", | ||
), | ||
) | ||
response = self._request("GET", new_url) | ||
return response.content | ||
|
||
def _request( | ||
self, | ||
method: str, | ||
path: str, | ||
params: dict = None, | ||
json: dict = None, | ||
stream: bool = False, | ||
timeout: int = 30, | ||
) -> requests.Response: | ||
url = urljoin(self.base_url, path) | ||
if urlparse(path).scheme: | ||
url = path | ||
response = self.session.request( | ||
method=method, | ||
url=url, | ||
params=params, | ||
json=json, | ||
stream=stream, | ||
timeout=timeout, | ||
) | ||
response.raise_for_status() | ||
return response |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
from dataclasses import dataclass, field | ||
from typing import List | ||
|
||
|
||
@dataclass(frozen=True) | ||
class OutputFormat: | ||
MP3 = "mp3" | ||
WAV = "wav" | ||
OGG = "ogg" | ||
FLAC = "flac" | ||
MULAW = "mulaw" | ||
|
||
|
||
@dataclass(frozen=True) | ||
class OutputQuality: | ||
DRAFT = "draft" | ||
LOW = "low" | ||
MEDIUM = "medium" | ||
HIGH = "high" | ||
PREMIUM = "premium" | ||
|
||
|
||
@dataclass(frozen=True) | ||
class GenerateStatus: | ||
GENERATING = "generating" | ||
COMPLETED = "completed" | ||
ERROR = "error" | ||
|
||
|
||
@dataclass | ||
class VoiceType: | ||
value: str | ||
name: str | ||
language: str | ||
voiceType: str | ||
languageCode: str | ||
gender: str | ||
service: str | ||
sample: str | ||
isKid: bool = False | ||
isNew: bool = False | ||
styles: List[str] = field(default_factory=list) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
from http import HTTPStatus | ||
from unittest.mock import Mock, patch | ||
|
||
import pytest | ||
import requests | ||
|
||
from pyplayht.classes import Client | ||
|
||
|
||
def test_client(client: Client): | ||
# check for available methods | ||
assert hasattr(client, "get_voices") and callable(client.get_voices) | ||
assert hasattr(client, "new_conversion_job") and callable( | ||
client.new_conversion_job, | ||
) | ||
assert hasattr(client, "get_coversion_job_status") and callable( | ||
client.get_coversion_job_status, | ||
) | ||
assert hasattr(client, "download_file") and callable(client.download_file) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"method", | ||
[ | ||
"GET", | ||
"POST", | ||
], | ||
) | ||
def test_request(method: str, client: Client): | ||
response = client._request( | ||
method=method, path=f"https://postman-echo.com/{method.lower()}" | ||
) | ||
assert isinstance(response, requests.Response) | ||
assert response.status_code == HTTPStatus.OK | ||
|
||
|
||
def test_download_file(client: Client): | ||
mock_request = Mock() | ||
mock_request.content = bytes() | ||
with patch("pyplayht.classes.Client._request", return_value=mock_request): | ||
response = client.download_file("http://127.0.0.1/test_path") | ||
assert isinstance(response, bytes) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import pytest | ||
|
||
from pyplayht.classes import Client | ||
|
||
|
||
@pytest.fixture | ||
def client() -> Client: | ||
return Client() |
Empty file.
This file was deleted.
Oops, something went wrong.
Empty file.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import pytest | ||
|
||
from pyplayht.types import GenerateStatus, OutputFormat, OutputQuality | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"type_class, fields", | ||
[ | ||
(OutputFormat, {"FLAC", "MP3", "MULAW", "OGG", "WAV"}), | ||
(GenerateStatus, {"GENERATING", "COMPLETED", "ERROR"}), | ||
(OutputQuality, {"DRAFT", "LOW", "MEDIUM", "HIGH", "PREMIUM"}), | ||
], | ||
) | ||
def test_static_types(type_class, fields): | ||
test_fields = set() | ||
for attr in dir(type_class): | ||
test_condition_1 = not callable(getattr(type_class, attr)) | ||
test_condition_2 = not attr.startswith("__") | ||
if test_condition_1 and test_condition_2: | ||
test_fields.add(attr) | ||
assert test_fields == fields |