voices-az 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.
- voices_az-0.1.0/.gitignore +16 -0
- voices_az-0.1.0/CHANGELOG.md +13 -0
- voices_az-0.1.0/LICENSE +21 -0
- voices_az-0.1.0/PKG-INFO +303 -0
- voices_az-0.1.0/README.md +266 -0
- voices_az-0.1.0/docs/README.az.md +91 -0
- voices_az-0.1.0/docs/README.ru.md +91 -0
- voices_az-0.1.0/examples/dub_youtube.py +61 -0
- voices_az-0.1.0/examples/transcribe_file.py +50 -0
- voices_az-0.1.0/examples/tts_to_file.py +59 -0
- voices_az-0.1.0/pyproject.toml +118 -0
- voices_az-0.1.0/src/voices_az/__init__.py +63 -0
- voices_az-0.1.0/src/voices_az/_base.py +421 -0
- voices_az-0.1.0/src/voices_az/_client.py +453 -0
- voices_az-0.1.0/src/voices_az/_version.py +1 -0
- voices_az-0.1.0/src/voices_az/exceptions.py +288 -0
- voices_az-0.1.0/src/voices_az/py.typed +0 -0
- voices_az-0.1.0/src/voices_az/resources/__init__.py +24 -0
- voices_az-0.1.0/src/voices_az/resources/_common.py +294 -0
- voices_az-0.1.0/src/voices_az/resources/account.py +47 -0
- voices_az-0.1.0/src/voices_az/resources/catalogue.py +155 -0
- voices_az-0.1.0/src/voices_az/resources/dubbing.py +692 -0
- voices_az-0.1.0/src/voices_az/resources/stt.py +499 -0
- voices_az-0.1.0/src/voices_az/resources/tts.py +412 -0
- voices_az-0.1.0/src/voices_az/types.py +727 -0
- voices_az-0.1.0/tests/__init__.py +0 -0
- voices_az-0.1.0/tests/conftest.py +136 -0
- voices_az-0.1.0/tests/fixtures/openapi.json +3553 -0
- voices_az-0.1.0/tests/test_async.py +184 -0
- voices_az-0.1.0/tests/test_catalogue.py +109 -0
- voices_az-0.1.0/tests/test_dubbing.py +331 -0
- voices_az-0.1.0/tests/test_errors.py +315 -0
- voices_az-0.1.0/tests/test_examples.py +95 -0
- voices_az-0.1.0/tests/test_retries.py +212 -0
- voices_az-0.1.0/tests/test_spec_coverage.py +182 -0
- voices_az-0.1.0/tests/test_stt.py +249 -0
- voices_az-0.1.0/tests/test_tts.py +311 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-10-14
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- `Client` and `AsyncClient` covering every operation of the public Voices.az API:
|
|
8
|
+
text-to-speech, speech-to-text, dubbing, voices, languages, services and the account
|
|
9
|
+
balance.
|
|
10
|
+
- Helpers that wait for results: `tts.synthesize()`, `stt.recognize()`,
|
|
11
|
+
`dubbing.wait()`, `dubbing.download()`.
|
|
12
|
+
- Typed results and errors, automatic `Idempotency-Key` on paid calls, safe retries,
|
|
13
|
+
streamed uploads and downloads.
|
voices_az-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Learning Machine
|
|
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.
|
voices_az-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: voices-az
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Voices.az API: text-to-speech, speech-to-text and video dubbing.
|
|
5
|
+
Project-URL: Homepage, https://voices.az
|
|
6
|
+
Project-URL: Documentation, https://voices.az/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/Voices-az/voices-python
|
|
8
|
+
Project-URL: Issues, https://github.com/Voices-az/voices-python/issues
|
|
9
|
+
Author-email: Learning Machine <salam@learningmachine.az>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api-client,azerbaijani,dubbing,speech-to-text,stt,text-to-speech,transcription,tts,voices.az
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Requires-Dist: httpx<1,>=0.24
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=4; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
34
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# voices-az
|
|
39
|
+
|
|
40
|
+
The official Python client for the [Voices.az](https://voices.az) API: text-to-speech,
|
|
41
|
+
speech-to-text and video dubbing, with Azerbaijani first.
|
|
42
|
+
|
|
43
|
+
- One call to synthesize, transcribe or dub - the client waits for the result for you.
|
|
44
|
+
- Sync and `asyncio` clients with the same methods.
|
|
45
|
+
- Typed results (dataclasses), typed errors, safe automatic retries.
|
|
46
|
+
- Large files are streamed from disk, never loaded into memory.
|
|
47
|
+
- Python 3.9+, one dependency (`httpx`).
|
|
48
|
+
|
|
49
|
+
Documentation of the HTTP API: <https://voices.az/docs>
|
|
50
|
+
|
|
51
|
+
Also in: [Azərbaycan dili](https://github.com/Voices-az/voices-python/blob/main/docs/README.az.md) · [Русский](https://github.com/Voices-az/voices-python/blob/main/docs/README.ru.md)
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install voices-az
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Authentication
|
|
60
|
+
|
|
61
|
+
Every request is made with your account's API key. Find it in the panel at
|
|
62
|
+
**[app.voices.az](https://app.voices.az) → Profile → API key**: create the key there
|
|
63
|
+
(or rotate / revoke it). A new key is shown only once - store it right away.
|
|
64
|
+
|
|
65
|
+
Pass the key explicitly, or set the `VOICES_API_KEY` environment variable:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
export VOICES_API_KEY=vz_live_...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import voices_az
|
|
73
|
+
|
|
74
|
+
client = voices_az.Client() # reads VOICES_API_KEY
|
|
75
|
+
client = voices_az.Client(api_key="vz_live_...") # or explicitly
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Keep the key on the server side: anyone who has it can spend your balance. The client
|
|
79
|
+
always sends it in the `X-API-KEY` header (never in a URL), and never sends it to the
|
|
80
|
+
file-storage links it downloads from.
|
|
81
|
+
|
|
82
|
+
## Text-to-speech
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import voices_az
|
|
86
|
+
|
|
87
|
+
with voices_az.Client() as client:
|
|
88
|
+
result = client.tts.synthesize(
|
|
89
|
+
"Salam! Bu mətn Voices.az ilə səsləndirilib.",
|
|
90
|
+
language="az", # a language code or id
|
|
91
|
+
voice=41, # a voice id, see client.voices.list()
|
|
92
|
+
speed=1, # 1, 1.5, 2, 2.5 or 3
|
|
93
|
+
)
|
|
94
|
+
print(result.preview_url) # a listening copy, free
|
|
95
|
+
print(result.duration) # seconds
|
|
96
|
+
|
|
97
|
+
# Charges the balance, then saves the MP3:
|
|
98
|
+
client.tts.download(result.project_id, "salam.mp3")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`synthesize()` queues the text and polls until the audio is ready (`wait=False` returns
|
|
102
|
+
right away; call `client.tts.wait(request_id)` later). The text may contain inline tags:
|
|
103
|
+
`[PAUSE=1.5]`, `[LAUGH]`, `[SIGH]`, `[UHM]`, `[SHH]`.
|
|
104
|
+
|
|
105
|
+
Multi-block projects: `client.tts.add_block()`, `update_block()`, `delete_block()`, then
|
|
106
|
+
`client.tts.generate(project_id, wait=True)`.
|
|
107
|
+
|
|
108
|
+
### Voices and languages
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
for language in client.languages.list():
|
|
112
|
+
print(language.id, language.code, language.title)
|
|
113
|
+
|
|
114
|
+
for voice in client.voices.iter(language="az", gender="female"):
|
|
115
|
+
print(voice.id, voice.full_name)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Speech-to-text
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
transcript = client.stt.recognize("interview.mp3", language="az", speakers=2)
|
|
122
|
+
print(transcript.text)
|
|
123
|
+
for segment in transcript.segments:
|
|
124
|
+
print(f"{segment.start:.2f}-{segment.end:.2f} {segment.text}")
|
|
125
|
+
|
|
126
|
+
# Subtitles or plain text: txt, srt, vtt, sbv, dfxp, csv
|
|
127
|
+
client.stt.download(transcript.project_id, "interview.srt", format="srt")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`file=` takes a path, `bytes` or a binary file object; use `youtube_url=` for a YouTube
|
|
131
|
+
video. Options: `speakers` (1-10), `remove_background=True` (strip music and noise first),
|
|
132
|
+
`convert_numbers=True` (digits instead of words).
|
|
133
|
+
|
|
134
|
+
To see the price before paying, upload first and start later:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
upload = client.stt.upload("interview.mp3")
|
|
138
|
+
print(upload.duration, upload.price)
|
|
139
|
+
client.stt.generate(upload.project_id, language="az") # charges the balance
|
|
140
|
+
transcript = client.stt.wait(upload.project_id)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Dubbing
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
draft = client.dubbing.create(
|
|
147
|
+
youtube_url="https://www.youtube.com/watch?v=...", # or file="video.mp4"
|
|
148
|
+
target_language="az",
|
|
149
|
+
background_mode="duck", # or "separate": keep music and effects at full level
|
|
150
|
+
)
|
|
151
|
+
print(draft.project_id, draft.price) # the quote; nothing is charged yet
|
|
152
|
+
|
|
153
|
+
preview = client.dubbing.preview(draft.project_id) # the first seconds
|
|
154
|
+
print(client.dubbing.wait(preview.request_id).audio_url)
|
|
155
|
+
|
|
156
|
+
run = client.dubbing.generate(draft.project_id) # charges the balance
|
|
157
|
+
client.dubbing.wait(run.request_id)
|
|
158
|
+
|
|
159
|
+
client.dubbing.download(draft.project_id, "dubbed.mp4", audio_path="dubbed.mp3")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`download()` asks for the clean (watermark-free) files and, when the video still has to be
|
|
163
|
+
assembled, waits for it. Choose a voice per speaker with
|
|
164
|
+
`generate(..., voice_map={"SPEAKER_00": 41})` - the speaker keys are in
|
|
165
|
+
`client.dubbing.get_project(id).body["speakers"]`. Edit single lines with `add_chunk()`,
|
|
166
|
+
`update_chunk()`, `delete_chunk()`, `regenerate_chunk()`, then `render()`.
|
|
167
|
+
|
|
168
|
+
## Async
|
|
169
|
+
|
|
170
|
+
`AsyncClient` has exactly the same methods, awaitable:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import asyncio
|
|
174
|
+
import voices_az
|
|
175
|
+
|
|
176
|
+
async def main() -> None:
|
|
177
|
+
async with voices_az.AsyncClient() as client:
|
|
178
|
+
result = await client.tts.synthesize("Salam!", language="az")
|
|
179
|
+
await client.tts.download(result.project_id, "salam.mp3")
|
|
180
|
+
|
|
181
|
+
async for voice in client.voices.iter(language="az"):
|
|
182
|
+
print(voice.full_name)
|
|
183
|
+
|
|
184
|
+
asyncio.run(main())
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## What costs money
|
|
188
|
+
|
|
189
|
+
The API bills at the same per-minute rates as the panel (`client.services.list()` shows
|
|
190
|
+
them) and takes the money from the same balance (`client.account.balance()`).
|
|
191
|
+
|
|
192
|
+
| Call | Charges the balance |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| `tts.synthesize()`, `tts.generate()` | No |
|
|
195
|
+
| `tts.export()`, `tts.download()` | **Yes**, on the first export of a project; exporting it again is free |
|
|
196
|
+
| `stt.recognize()`, `stt.generate()` | **Yes**, when recognition starts |
|
|
197
|
+
| `stt.upload()`, `stt.export()`, `stt.download()` | No |
|
|
198
|
+
| `dubbing.create()` | No |
|
|
199
|
+
| `dubbing.preview()`, `dubbing.render()` | The first few per project are free, then each one is charged (`price` on the result) |
|
|
200
|
+
| `dubbing.generate()` | **Yes**, the whole dub (plus the auto-emotion surcharge when it is on) |
|
|
201
|
+
| `dubbing.regenerate_chunk()` | **Yes** (`price` on the result) |
|
|
202
|
+
| `dubbing.export()`, `dubbing.download()` | No for dubs paid on `generate()` |
|
|
203
|
+
|
|
204
|
+
When the balance is too low the call raises `InsufficientBalanceError` and nothing is
|
|
205
|
+
charged or queued; `missing` tells how much to top up.
|
|
206
|
+
|
|
207
|
+
## Errors
|
|
208
|
+
|
|
209
|
+
Every exception derives from `voices_az.VoicesError`:
|
|
210
|
+
|
|
211
|
+
| Exception | When |
|
|
212
|
+
| --- | --- |
|
|
213
|
+
| `AuthenticationError` | 401 - missing, unknown or revoked API key |
|
|
214
|
+
| `PermissionDeniedError` | 403 - the key may not call this endpoint (`type == "api_key_scope"`) |
|
|
215
|
+
| `NotFoundError` | 404 - no such project (or it belongs to another account) |
|
|
216
|
+
| `ConflictError`, `IdempotencyInProgressError` | 409 |
|
|
217
|
+
| `ProjectExpiredError` | 410 - the project's files were deleted after the retention window (`project_id`, `expired_at`) |
|
|
218
|
+
| `PayloadTooLargeError` | 413 - the file is over the upload limit |
|
|
219
|
+
| `ValidationError` | 422 - invalid input; `errors` holds `{"field": ["message"]}` |
|
|
220
|
+
| `InsufficientBalanceError` | 422 - `price`, `balance`, `missing`, `currency`, `project_id` (a `ValidationError`) |
|
|
221
|
+
| `ContentPolicyError` | 422 - the text or result was refused by the content policy |
|
|
222
|
+
| `RateLimitError` | 429 - `retry_after`, `limit`, `remaining` |
|
|
223
|
+
| `ServerError` | 5xx |
|
|
224
|
+
| `APIConnectionError`, `APITimeoutError` | the API could not be reached / the request timed out |
|
|
225
|
+
| `JobFailedError` | a synthesis, recognition or dubbing run failed |
|
|
226
|
+
| `voices_az.TimeoutError` | a `wait` helper gave up; the run continues on the server |
|
|
227
|
+
|
|
228
|
+
HTTP errors carry `status_code`, `type` (the API's stable `error.type` slug), `message`,
|
|
229
|
+
`data` and the raw `body`. Messages are localised; pick their language with
|
|
230
|
+
`Client(language="az" | "en" | "ru" | "tr")`, and branch on the class or `type`, not on the
|
|
231
|
+
message.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
try:
|
|
235
|
+
client.tts.download(project_id, "out.mp3")
|
|
236
|
+
except voices_az.InsufficientBalanceError as exc:
|
|
237
|
+
print(f"Top up {exc.missing} {exc.currency} at https://app.voices.az")
|
|
238
|
+
except voices_az.ProjectExpiredError:
|
|
239
|
+
print("The files of this project are no longer stored.")
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Retries and idempotency
|
|
243
|
+
|
|
244
|
+
Failed requests are repeated (`max_retries=2` by default, exponential backoff) only when
|
|
245
|
+
that cannot charge twice:
|
|
246
|
+
|
|
247
|
+
- reads (`GET`) on network errors and on `502`/`503`/`504`;
|
|
248
|
+
- any request when the connection could not be opened at all, or on `429` (after
|
|
249
|
+
`Retry-After`; a wait longer than a minute raises `RateLimitError` instead);
|
|
250
|
+
- the calls that queue paid work (`synthesize`, `recognize`, `generate`, `preview`,
|
|
251
|
+
`render`, ...): the client sends an `Idempotency-Key` with them, so the API replays
|
|
252
|
+
the first answer instead of running and charging again. Pass your own key with
|
|
253
|
+
`idempotency_key=...` to make a retry safe across processes too.
|
|
254
|
+
|
|
255
|
+
`export` is never repeated automatically: it can charge and the API does not accept an
|
|
256
|
+
idempotency key there.
|
|
257
|
+
|
|
258
|
+
## Polling, limits and retention
|
|
259
|
+
|
|
260
|
+
- The `wait` helpers poll every 2 seconds (`poll_interval`, never less) and give up after
|
|
261
|
+
`wait_timeout` seconds (`None` waits indefinitely). `on_progress` receives every status.
|
|
262
|
+
- The API allows 60 requests a minute and 5000 a day per key.
|
|
263
|
+
- Files are kept for 6 months after the last activity on a project (30 days for a dubbing
|
|
264
|
+
project that was only previewed). Afterwards the project still exists but its file calls
|
|
265
|
+
raise `ProjectExpiredError` - download what you need in time.
|
|
266
|
+
|
|
267
|
+
## Configuration
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
client = voices_az.Client(
|
|
271
|
+
api_key="vz_live_...", # default: VOICES_API_KEY
|
|
272
|
+
base_url="https://voices.az/api", # default (or VOICES_BASE_URL)
|
|
273
|
+
timeout=120, # seconds, or an httpx.Timeout
|
|
274
|
+
max_retries=2,
|
|
275
|
+
language="en", # language of error messages
|
|
276
|
+
poll_interval=2,
|
|
277
|
+
http_client=None, # your own httpx.Client (proxies, TLS, ...)
|
|
278
|
+
)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Every result keeps the decoded JSON in `.raw`, and `client.request("GET", "/services")`
|
|
282
|
+
calls any endpoint directly.
|
|
283
|
+
|
|
284
|
+
## Examples
|
|
285
|
+
|
|
286
|
+
Runnable scripts in [`examples/`](examples): `tts_to_file.py`, `transcribe_file.py`,
|
|
287
|
+
`dub_youtube.py`. Each reads `VOICES_API_KEY`.
|
|
288
|
+
|
|
289
|
+
## Development
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
python -m venv .venv && . .venv/bin/activate
|
|
293
|
+
pip install -e ".[dev]"
|
|
294
|
+
pytest -q
|
|
295
|
+
ruff check .
|
|
296
|
+
mypy
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The tests never touch the network.
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
MIT - see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# voices-az
|
|
2
|
+
|
|
3
|
+
The official Python client for the [Voices.az](https://voices.az) API: text-to-speech,
|
|
4
|
+
speech-to-text and video dubbing, with Azerbaijani first.
|
|
5
|
+
|
|
6
|
+
- One call to synthesize, transcribe or dub - the client waits for the result for you.
|
|
7
|
+
- Sync and `asyncio` clients with the same methods.
|
|
8
|
+
- Typed results (dataclasses), typed errors, safe automatic retries.
|
|
9
|
+
- Large files are streamed from disk, never loaded into memory.
|
|
10
|
+
- Python 3.9+, one dependency (`httpx`).
|
|
11
|
+
|
|
12
|
+
Documentation of the HTTP API: <https://voices.az/docs>
|
|
13
|
+
|
|
14
|
+
Also in: [Azərbaycan dili](https://github.com/Voices-az/voices-python/blob/main/docs/README.az.md) · [Русский](https://github.com/Voices-az/voices-python/blob/main/docs/README.ru.md)
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install voices-az
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Authentication
|
|
23
|
+
|
|
24
|
+
Every request is made with your account's API key. Find it in the panel at
|
|
25
|
+
**[app.voices.az](https://app.voices.az) → Profile → API key**: create the key there
|
|
26
|
+
(or rotate / revoke it). A new key is shown only once - store it right away.
|
|
27
|
+
|
|
28
|
+
Pass the key explicitly, or set the `VOICES_API_KEY` environment variable:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export VOICES_API_KEY=vz_live_...
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import voices_az
|
|
36
|
+
|
|
37
|
+
client = voices_az.Client() # reads VOICES_API_KEY
|
|
38
|
+
client = voices_az.Client(api_key="vz_live_...") # or explicitly
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Keep the key on the server side: anyone who has it can spend your balance. The client
|
|
42
|
+
always sends it in the `X-API-KEY` header (never in a URL), and never sends it to the
|
|
43
|
+
file-storage links it downloads from.
|
|
44
|
+
|
|
45
|
+
## Text-to-speech
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import voices_az
|
|
49
|
+
|
|
50
|
+
with voices_az.Client() as client:
|
|
51
|
+
result = client.tts.synthesize(
|
|
52
|
+
"Salam! Bu mətn Voices.az ilə səsləndirilib.",
|
|
53
|
+
language="az", # a language code or id
|
|
54
|
+
voice=41, # a voice id, see client.voices.list()
|
|
55
|
+
speed=1, # 1, 1.5, 2, 2.5 or 3
|
|
56
|
+
)
|
|
57
|
+
print(result.preview_url) # a listening copy, free
|
|
58
|
+
print(result.duration) # seconds
|
|
59
|
+
|
|
60
|
+
# Charges the balance, then saves the MP3:
|
|
61
|
+
client.tts.download(result.project_id, "salam.mp3")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`synthesize()` queues the text and polls until the audio is ready (`wait=False` returns
|
|
65
|
+
right away; call `client.tts.wait(request_id)` later). The text may contain inline tags:
|
|
66
|
+
`[PAUSE=1.5]`, `[LAUGH]`, `[SIGH]`, `[UHM]`, `[SHH]`.
|
|
67
|
+
|
|
68
|
+
Multi-block projects: `client.tts.add_block()`, `update_block()`, `delete_block()`, then
|
|
69
|
+
`client.tts.generate(project_id, wait=True)`.
|
|
70
|
+
|
|
71
|
+
### Voices and languages
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
for language in client.languages.list():
|
|
75
|
+
print(language.id, language.code, language.title)
|
|
76
|
+
|
|
77
|
+
for voice in client.voices.iter(language="az", gender="female"):
|
|
78
|
+
print(voice.id, voice.full_name)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Speech-to-text
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
transcript = client.stt.recognize("interview.mp3", language="az", speakers=2)
|
|
85
|
+
print(transcript.text)
|
|
86
|
+
for segment in transcript.segments:
|
|
87
|
+
print(f"{segment.start:.2f}-{segment.end:.2f} {segment.text}")
|
|
88
|
+
|
|
89
|
+
# Subtitles or plain text: txt, srt, vtt, sbv, dfxp, csv
|
|
90
|
+
client.stt.download(transcript.project_id, "interview.srt", format="srt")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`file=` takes a path, `bytes` or a binary file object; use `youtube_url=` for a YouTube
|
|
94
|
+
video. Options: `speakers` (1-10), `remove_background=True` (strip music and noise first),
|
|
95
|
+
`convert_numbers=True` (digits instead of words).
|
|
96
|
+
|
|
97
|
+
To see the price before paying, upload first and start later:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
upload = client.stt.upload("interview.mp3")
|
|
101
|
+
print(upload.duration, upload.price)
|
|
102
|
+
client.stt.generate(upload.project_id, language="az") # charges the balance
|
|
103
|
+
transcript = client.stt.wait(upload.project_id)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Dubbing
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
draft = client.dubbing.create(
|
|
110
|
+
youtube_url="https://www.youtube.com/watch?v=...", # or file="video.mp4"
|
|
111
|
+
target_language="az",
|
|
112
|
+
background_mode="duck", # or "separate": keep music and effects at full level
|
|
113
|
+
)
|
|
114
|
+
print(draft.project_id, draft.price) # the quote; nothing is charged yet
|
|
115
|
+
|
|
116
|
+
preview = client.dubbing.preview(draft.project_id) # the first seconds
|
|
117
|
+
print(client.dubbing.wait(preview.request_id).audio_url)
|
|
118
|
+
|
|
119
|
+
run = client.dubbing.generate(draft.project_id) # charges the balance
|
|
120
|
+
client.dubbing.wait(run.request_id)
|
|
121
|
+
|
|
122
|
+
client.dubbing.download(draft.project_id, "dubbed.mp4", audio_path="dubbed.mp3")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`download()` asks for the clean (watermark-free) files and, when the video still has to be
|
|
126
|
+
assembled, waits for it. Choose a voice per speaker with
|
|
127
|
+
`generate(..., voice_map={"SPEAKER_00": 41})` - the speaker keys are in
|
|
128
|
+
`client.dubbing.get_project(id).body["speakers"]`. Edit single lines with `add_chunk()`,
|
|
129
|
+
`update_chunk()`, `delete_chunk()`, `regenerate_chunk()`, then `render()`.
|
|
130
|
+
|
|
131
|
+
## Async
|
|
132
|
+
|
|
133
|
+
`AsyncClient` has exactly the same methods, awaitable:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import asyncio
|
|
137
|
+
import voices_az
|
|
138
|
+
|
|
139
|
+
async def main() -> None:
|
|
140
|
+
async with voices_az.AsyncClient() as client:
|
|
141
|
+
result = await client.tts.synthesize("Salam!", language="az")
|
|
142
|
+
await client.tts.download(result.project_id, "salam.mp3")
|
|
143
|
+
|
|
144
|
+
async for voice in client.voices.iter(language="az"):
|
|
145
|
+
print(voice.full_name)
|
|
146
|
+
|
|
147
|
+
asyncio.run(main())
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## What costs money
|
|
151
|
+
|
|
152
|
+
The API bills at the same per-minute rates as the panel (`client.services.list()` shows
|
|
153
|
+
them) and takes the money from the same balance (`client.account.balance()`).
|
|
154
|
+
|
|
155
|
+
| Call | Charges the balance |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| `tts.synthesize()`, `tts.generate()` | No |
|
|
158
|
+
| `tts.export()`, `tts.download()` | **Yes**, on the first export of a project; exporting it again is free |
|
|
159
|
+
| `stt.recognize()`, `stt.generate()` | **Yes**, when recognition starts |
|
|
160
|
+
| `stt.upload()`, `stt.export()`, `stt.download()` | No |
|
|
161
|
+
| `dubbing.create()` | No |
|
|
162
|
+
| `dubbing.preview()`, `dubbing.render()` | The first few per project are free, then each one is charged (`price` on the result) |
|
|
163
|
+
| `dubbing.generate()` | **Yes**, the whole dub (plus the auto-emotion surcharge when it is on) |
|
|
164
|
+
| `dubbing.regenerate_chunk()` | **Yes** (`price` on the result) |
|
|
165
|
+
| `dubbing.export()`, `dubbing.download()` | No for dubs paid on `generate()` |
|
|
166
|
+
|
|
167
|
+
When the balance is too low the call raises `InsufficientBalanceError` and nothing is
|
|
168
|
+
charged or queued; `missing` tells how much to top up.
|
|
169
|
+
|
|
170
|
+
## Errors
|
|
171
|
+
|
|
172
|
+
Every exception derives from `voices_az.VoicesError`:
|
|
173
|
+
|
|
174
|
+
| Exception | When |
|
|
175
|
+
| --- | --- |
|
|
176
|
+
| `AuthenticationError` | 401 - missing, unknown or revoked API key |
|
|
177
|
+
| `PermissionDeniedError` | 403 - the key may not call this endpoint (`type == "api_key_scope"`) |
|
|
178
|
+
| `NotFoundError` | 404 - no such project (or it belongs to another account) |
|
|
179
|
+
| `ConflictError`, `IdempotencyInProgressError` | 409 |
|
|
180
|
+
| `ProjectExpiredError` | 410 - the project's files were deleted after the retention window (`project_id`, `expired_at`) |
|
|
181
|
+
| `PayloadTooLargeError` | 413 - the file is over the upload limit |
|
|
182
|
+
| `ValidationError` | 422 - invalid input; `errors` holds `{"field": ["message"]}` |
|
|
183
|
+
| `InsufficientBalanceError` | 422 - `price`, `balance`, `missing`, `currency`, `project_id` (a `ValidationError`) |
|
|
184
|
+
| `ContentPolicyError` | 422 - the text or result was refused by the content policy |
|
|
185
|
+
| `RateLimitError` | 429 - `retry_after`, `limit`, `remaining` |
|
|
186
|
+
| `ServerError` | 5xx |
|
|
187
|
+
| `APIConnectionError`, `APITimeoutError` | the API could not be reached / the request timed out |
|
|
188
|
+
| `JobFailedError` | a synthesis, recognition or dubbing run failed |
|
|
189
|
+
| `voices_az.TimeoutError` | a `wait` helper gave up; the run continues on the server |
|
|
190
|
+
|
|
191
|
+
HTTP errors carry `status_code`, `type` (the API's stable `error.type` slug), `message`,
|
|
192
|
+
`data` and the raw `body`. Messages are localised; pick their language with
|
|
193
|
+
`Client(language="az" | "en" | "ru" | "tr")`, and branch on the class or `type`, not on the
|
|
194
|
+
message.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
try:
|
|
198
|
+
client.tts.download(project_id, "out.mp3")
|
|
199
|
+
except voices_az.InsufficientBalanceError as exc:
|
|
200
|
+
print(f"Top up {exc.missing} {exc.currency} at https://app.voices.az")
|
|
201
|
+
except voices_az.ProjectExpiredError:
|
|
202
|
+
print("The files of this project are no longer stored.")
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Retries and idempotency
|
|
206
|
+
|
|
207
|
+
Failed requests are repeated (`max_retries=2` by default, exponential backoff) only when
|
|
208
|
+
that cannot charge twice:
|
|
209
|
+
|
|
210
|
+
- reads (`GET`) on network errors and on `502`/`503`/`504`;
|
|
211
|
+
- any request when the connection could not be opened at all, or on `429` (after
|
|
212
|
+
`Retry-After`; a wait longer than a minute raises `RateLimitError` instead);
|
|
213
|
+
- the calls that queue paid work (`synthesize`, `recognize`, `generate`, `preview`,
|
|
214
|
+
`render`, ...): the client sends an `Idempotency-Key` with them, so the API replays
|
|
215
|
+
the first answer instead of running and charging again. Pass your own key with
|
|
216
|
+
`idempotency_key=...` to make a retry safe across processes too.
|
|
217
|
+
|
|
218
|
+
`export` is never repeated automatically: it can charge and the API does not accept an
|
|
219
|
+
idempotency key there.
|
|
220
|
+
|
|
221
|
+
## Polling, limits and retention
|
|
222
|
+
|
|
223
|
+
- The `wait` helpers poll every 2 seconds (`poll_interval`, never less) and give up after
|
|
224
|
+
`wait_timeout` seconds (`None` waits indefinitely). `on_progress` receives every status.
|
|
225
|
+
- The API allows 60 requests a minute and 5000 a day per key.
|
|
226
|
+
- Files are kept for 6 months after the last activity on a project (30 days for a dubbing
|
|
227
|
+
project that was only previewed). Afterwards the project still exists but its file calls
|
|
228
|
+
raise `ProjectExpiredError` - download what you need in time.
|
|
229
|
+
|
|
230
|
+
## Configuration
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
client = voices_az.Client(
|
|
234
|
+
api_key="vz_live_...", # default: VOICES_API_KEY
|
|
235
|
+
base_url="https://voices.az/api", # default (or VOICES_BASE_URL)
|
|
236
|
+
timeout=120, # seconds, or an httpx.Timeout
|
|
237
|
+
max_retries=2,
|
|
238
|
+
language="en", # language of error messages
|
|
239
|
+
poll_interval=2,
|
|
240
|
+
http_client=None, # your own httpx.Client (proxies, TLS, ...)
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Every result keeps the decoded JSON in `.raw`, and `client.request("GET", "/services")`
|
|
245
|
+
calls any endpoint directly.
|
|
246
|
+
|
|
247
|
+
## Examples
|
|
248
|
+
|
|
249
|
+
Runnable scripts in [`examples/`](examples): `tts_to_file.py`, `transcribe_file.py`,
|
|
250
|
+
`dub_youtube.py`. Each reads `VOICES_API_KEY`.
|
|
251
|
+
|
|
252
|
+
## Development
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
python -m venv .venv && . .venv/bin/activate
|
|
256
|
+
pip install -e ".[dev]"
|
|
257
|
+
pytest -q
|
|
258
|
+
ruff check .
|
|
259
|
+
mypy
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The tests never touch the network.
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT - see [LICENSE](LICENSE).
|