voicekit-client 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.
- voicekit_client-0.1.0/PKG-INFO +150 -0
- voicekit_client-0.1.0/README.md +135 -0
- voicekit_client-0.1.0/pyproject.toml +24 -0
- voicekit_client-0.1.0/setup.cfg +4 -0
- voicekit_client-0.1.0/voicekit/__init__.py +11 -0
- voicekit_client-0.1.0/voicekit/client.py +545 -0
- voicekit_client-0.1.0/voicekit/streaming.py +84 -0
- voicekit_client-0.1.0/voicekit_client.egg-info/PKG-INFO +150 -0
- voicekit_client-0.1.0/voicekit_client.egg-info/SOURCES.txt +10 -0
- voicekit_client-0.1.0/voicekit_client.egg-info/dependency_links.txt +1 -0
- voicekit_client-0.1.0/voicekit_client.egg-info/requires.txt +4 -0
- voicekit_client-0.1.0/voicekit_client.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: voicekit-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for VoiceKit (synthesis, transcription, analysis, moderation, batches).
|
|
5
|
+
Author: VoiceKit
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://ttsapi.ru
|
|
8
|
+
Project-URL: Documentation, https://ttsapi.ru/swagger
|
|
9
|
+
Keywords: tts,speech,synthesis,transcription,stt,voice,russian
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: httpx>=0.24
|
|
13
|
+
Provides-Extra: streaming
|
|
14
|
+
Requires-Dist: websockets>=12.0; extra == "streaming"
|
|
15
|
+
|
|
16
|
+
# VoiceKit — Python SDK
|
|
17
|
+
|
|
18
|
+
Official Python wrapper for [VoiceKit](https://ttsapi.ru):
|
|
19
|
+
neural speech synthesis, transcription, sentiment analysis, and batch operations.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install voicekit-client
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from voicekit import VoiceKitClient, b64
|
|
31
|
+
|
|
32
|
+
client = VoiceKitClient(api_key="YOUR_KEY")
|
|
33
|
+
|
|
34
|
+
# Synthesis → raw audio bytes
|
|
35
|
+
audio = client.synthesize("Привет! Это синтез русской речи.", voice="preset_anna", format="mp3")
|
|
36
|
+
with open("speech.mp3", "wb") as f:
|
|
37
|
+
f.write(audio)
|
|
38
|
+
|
|
39
|
+
# Streaming (Pro/Business)
|
|
40
|
+
for chunk in client.synthesize_stream("Первое предложение. Второе."):
|
|
41
|
+
pass # write chunks to a file or socket
|
|
42
|
+
|
|
43
|
+
# Transcription (async → poll)
|
|
44
|
+
job = client.transcribe("audio.wav", keyterms=["диагноз"])
|
|
45
|
+
result = client.get_transcription_job(job["job_id"])
|
|
46
|
+
while result["status"] not in ("completed", "failed"):
|
|
47
|
+
result = client.get_transcription_job(job["job_id"])
|
|
48
|
+
|
|
49
|
+
# Short-file sync transcription
|
|
50
|
+
transcript = client.transcribe_sync("audio.wav")
|
|
51
|
+
|
|
52
|
+
# Analysis (sentiment + keywords + entities)
|
|
53
|
+
analysis = client.analyze_sync("audio.wav")
|
|
54
|
+
|
|
55
|
+
# Text intelligence
|
|
56
|
+
lang = client.detect_language("Как дела?")
|
|
57
|
+
topics = client.topics("Нейросети и алгоритмы")
|
|
58
|
+
summary = client.summarize("Длинный текст для резюме.", max_sentences=3)
|
|
59
|
+
moderation = client.moderate("Это оскорбительное сообщение.")
|
|
60
|
+
|
|
61
|
+
# Batches
|
|
62
|
+
batch = client.batch_synthesize([
|
|
63
|
+
{"text": "Первый текст", "voice": "preset_anna"},
|
|
64
|
+
{"text": "Второй текст", "voice": "dmitri"},
|
|
65
|
+
])
|
|
66
|
+
status = client.get_batch(batch["batch_id"])
|
|
67
|
+
|
|
68
|
+
analysis_batch = client.batch_analyze([
|
|
69
|
+
{"audio": b64("a.wav"), "language": "ru"},
|
|
70
|
+
{"audio": b64("b.wav"), "language": "ru"},
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
# Voice cloning (Pro/Business)
|
|
74
|
+
clone = client.create_clone_voice(
|
|
75
|
+
name="My voice",
|
|
76
|
+
prompt_text="Точный текст образца.",
|
|
77
|
+
samples="reference.wav",
|
|
78
|
+
)
|
|
79
|
+
print(client.list_clone_voices())
|
|
80
|
+
client.delete_clone_voice(clone["id"])
|
|
81
|
+
|
|
82
|
+
# VAD (speech segments)
|
|
83
|
+
segments = client.vad("audio.wav")
|
|
84
|
+
|
|
85
|
+
# Account
|
|
86
|
+
usage = client.usage()
|
|
87
|
+
balance = client.billing_balance()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Audio effects (Pro/Business)
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# Inline during synthesis — the chain is applied to the synthesized audio
|
|
94
|
+
audio = client.synthesize(
|
|
95
|
+
"Привет!",
|
|
96
|
+
effects='[{"type":"reverb","room_size":0.5},{"type":"pitch","semitones":2}]',
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Async processing of an existing file
|
|
100
|
+
import time
|
|
101
|
+
|
|
102
|
+
job = client.apply_audio_effects("voice.mp3", [{"type": "compressor", "ratio": 3}])
|
|
103
|
+
while client.get_audio_effects_job(job["job_id"])["status"] not in ("completed", "failed"):
|
|
104
|
+
time.sleep(1)
|
|
105
|
+
result = client.download_audio_effects(job["job_id"])
|
|
106
|
+
open("voice_fx.mp3", "wb").write(result)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### WebSocket streaming (Pro/Business)
|
|
110
|
+
|
|
111
|
+
Требует опциональной зависимости:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install "voicekit-client[streaming]"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import asyncio
|
|
119
|
+
|
|
120
|
+
async def main():
|
|
121
|
+
client = VoiceKitClient(api_key="YOUR_KEY")
|
|
122
|
+
|
|
123
|
+
stream = await client.transcribe_stream(language="ru", keyterms=["диагноз"])
|
|
124
|
+
await stream.send_audio(pcm16_chunk_1) # raw PCM16, 16 kHz mono
|
|
125
|
+
await stream.send_audio(pcm16_chunk_2)
|
|
126
|
+
await stream.stop() # finalize the utterance
|
|
127
|
+
async for event in stream: # session / vad / partial / final / error
|
|
128
|
+
print(event["type"], event)
|
|
129
|
+
await stream.close()
|
|
130
|
+
|
|
131
|
+
vad = await client.vad_stream() # VAD events only (speech_started/ended)
|
|
132
|
+
await vad.send_audio(pcm16_chunk)
|
|
133
|
+
await vad.stop()
|
|
134
|
+
async for event in vad:
|
|
135
|
+
print(event["type"], event)
|
|
136
|
+
await vad.close()
|
|
137
|
+
|
|
138
|
+
asyncio.run(main())
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Configuration
|
|
142
|
+
|
|
143
|
+
| Option | Default | Description |
|
|
144
|
+
| --- | --- | --- |
|
|
145
|
+
| `api_key` | — | API key (required) |
|
|
146
|
+
| `base_url` | `https://ttsapi.ru` | API base URL (e.g. `http://localhost:5080` for local dev) |
|
|
147
|
+
| `timeout` | `120.0` | Per-request timeout in seconds |
|
|
148
|
+
|
|
149
|
+
Errors raise `VoiceKitError` with `.status` (HTTP status), `.code` (machine-readable
|
|
150
|
+
code) and `.message`.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# VoiceKit — Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python wrapper for [VoiceKit](https://ttsapi.ru):
|
|
4
|
+
neural speech synthesis, transcription, sentiment analysis, and batch operations.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install voicekit-client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from voicekit import VoiceKitClient, b64
|
|
16
|
+
|
|
17
|
+
client = VoiceKitClient(api_key="YOUR_KEY")
|
|
18
|
+
|
|
19
|
+
# Synthesis → raw audio bytes
|
|
20
|
+
audio = client.synthesize("Привет! Это синтез русской речи.", voice="preset_anna", format="mp3")
|
|
21
|
+
with open("speech.mp3", "wb") as f:
|
|
22
|
+
f.write(audio)
|
|
23
|
+
|
|
24
|
+
# Streaming (Pro/Business)
|
|
25
|
+
for chunk in client.synthesize_stream("Первое предложение. Второе."):
|
|
26
|
+
pass # write chunks to a file or socket
|
|
27
|
+
|
|
28
|
+
# Transcription (async → poll)
|
|
29
|
+
job = client.transcribe("audio.wav", keyterms=["диагноз"])
|
|
30
|
+
result = client.get_transcription_job(job["job_id"])
|
|
31
|
+
while result["status"] not in ("completed", "failed"):
|
|
32
|
+
result = client.get_transcription_job(job["job_id"])
|
|
33
|
+
|
|
34
|
+
# Short-file sync transcription
|
|
35
|
+
transcript = client.transcribe_sync("audio.wav")
|
|
36
|
+
|
|
37
|
+
# Analysis (sentiment + keywords + entities)
|
|
38
|
+
analysis = client.analyze_sync("audio.wav")
|
|
39
|
+
|
|
40
|
+
# Text intelligence
|
|
41
|
+
lang = client.detect_language("Как дела?")
|
|
42
|
+
topics = client.topics("Нейросети и алгоритмы")
|
|
43
|
+
summary = client.summarize("Длинный текст для резюме.", max_sentences=3)
|
|
44
|
+
moderation = client.moderate("Это оскорбительное сообщение.")
|
|
45
|
+
|
|
46
|
+
# Batches
|
|
47
|
+
batch = client.batch_synthesize([
|
|
48
|
+
{"text": "Первый текст", "voice": "preset_anna"},
|
|
49
|
+
{"text": "Второй текст", "voice": "dmitri"},
|
|
50
|
+
])
|
|
51
|
+
status = client.get_batch(batch["batch_id"])
|
|
52
|
+
|
|
53
|
+
analysis_batch = client.batch_analyze([
|
|
54
|
+
{"audio": b64("a.wav"), "language": "ru"},
|
|
55
|
+
{"audio": b64("b.wav"), "language": "ru"},
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
# Voice cloning (Pro/Business)
|
|
59
|
+
clone = client.create_clone_voice(
|
|
60
|
+
name="My voice",
|
|
61
|
+
prompt_text="Точный текст образца.",
|
|
62
|
+
samples="reference.wav",
|
|
63
|
+
)
|
|
64
|
+
print(client.list_clone_voices())
|
|
65
|
+
client.delete_clone_voice(clone["id"])
|
|
66
|
+
|
|
67
|
+
# VAD (speech segments)
|
|
68
|
+
segments = client.vad("audio.wav")
|
|
69
|
+
|
|
70
|
+
# Account
|
|
71
|
+
usage = client.usage()
|
|
72
|
+
balance = client.billing_balance()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Audio effects (Pro/Business)
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Inline during synthesis — the chain is applied to the synthesized audio
|
|
79
|
+
audio = client.synthesize(
|
|
80
|
+
"Привет!",
|
|
81
|
+
effects='[{"type":"reverb","room_size":0.5},{"type":"pitch","semitones":2}]',
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Async processing of an existing file
|
|
85
|
+
import time
|
|
86
|
+
|
|
87
|
+
job = client.apply_audio_effects("voice.mp3", [{"type": "compressor", "ratio": 3}])
|
|
88
|
+
while client.get_audio_effects_job(job["job_id"])["status"] not in ("completed", "failed"):
|
|
89
|
+
time.sleep(1)
|
|
90
|
+
result = client.download_audio_effects(job["job_id"])
|
|
91
|
+
open("voice_fx.mp3", "wb").write(result)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### WebSocket streaming (Pro/Business)
|
|
95
|
+
|
|
96
|
+
Требует опциональной зависимости:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install "voicekit-client[streaming]"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import asyncio
|
|
104
|
+
|
|
105
|
+
async def main():
|
|
106
|
+
client = VoiceKitClient(api_key="YOUR_KEY")
|
|
107
|
+
|
|
108
|
+
stream = await client.transcribe_stream(language="ru", keyterms=["диагноз"])
|
|
109
|
+
await stream.send_audio(pcm16_chunk_1) # raw PCM16, 16 kHz mono
|
|
110
|
+
await stream.send_audio(pcm16_chunk_2)
|
|
111
|
+
await stream.stop() # finalize the utterance
|
|
112
|
+
async for event in stream: # session / vad / partial / final / error
|
|
113
|
+
print(event["type"], event)
|
|
114
|
+
await stream.close()
|
|
115
|
+
|
|
116
|
+
vad = await client.vad_stream() # VAD events only (speech_started/ended)
|
|
117
|
+
await vad.send_audio(pcm16_chunk)
|
|
118
|
+
await vad.stop()
|
|
119
|
+
async for event in vad:
|
|
120
|
+
print(event["type"], event)
|
|
121
|
+
await vad.close()
|
|
122
|
+
|
|
123
|
+
asyncio.run(main())
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
| Option | Default | Description |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| `api_key` | — | API key (required) |
|
|
131
|
+
| `base_url` | `https://ttsapi.ru` | API base URL (e.g. `http://localhost:5080` for local dev) |
|
|
132
|
+
| `timeout` | `120.0` | Per-request timeout in seconds |
|
|
133
|
+
|
|
134
|
+
Errors raise `VoiceKitError` with `.status` (HTTP status), `.code` (machine-readable
|
|
135
|
+
code) and `.message`.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "voicekit-client"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for VoiceKit (synthesis, transcription, analysis, moderation, batches)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
authors = [{ name = "VoiceKit" }]
|
|
13
|
+
keywords = ["tts", "speech", "synthesis", "transcription", "stt", "voice", "russian"]
|
|
14
|
+
dependencies = ["httpx>=0.24"]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
streaming = ["websockets>=12.0"]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://ttsapi.ru"
|
|
21
|
+
Documentation = "https://ttsapi.ru/swagger"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
include = ["voicekit*"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from voicekit.client import VoiceKitClient, VoiceKitError
|
|
2
|
+
from voicekit.streaming import TranscriptionStream, VadStream, WsStream
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"VoiceKitClient",
|
|
6
|
+
"VoiceKitError",
|
|
7
|
+
"TranscriptionStream",
|
|
8
|
+
"VadStream",
|
|
9
|
+
"WsStream",
|
|
10
|
+
]
|
|
11
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"""Official Python SDK for VoiceKit.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from voicekit import VoiceKitClient
|
|
6
|
+
|
|
7
|
+
client = VoiceKitClient(api_key="YOUR_KEY")
|
|
8
|
+
|
|
9
|
+
audio = client.synthesize("Привет!", voice="preset_anna", format="mp3")
|
|
10
|
+
with open("speech.mp3", "wb") as f:
|
|
11
|
+
f.write(audio)
|
|
12
|
+
|
|
13
|
+
job = client.transcribe("audio.wav") # async → job id
|
|
14
|
+
result = client.get_transcription_job(job["job_id"])
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import base64
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
from typing import Any, BinaryIO, Iterable, Iterator, Optional, Union
|
|
23
|
+
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
24
|
+
|
|
25
|
+
import httpx
|
|
26
|
+
|
|
27
|
+
from .streaming import TranscriptionStream, VadStream, websockets_connect
|
|
28
|
+
|
|
29
|
+
DEFAULT_BASE_URL = "https://ttsapi.ru"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class VoiceKitError(Exception):
|
|
33
|
+
"""Raised when the API returns a non-2xx response."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, status: int, message: str, code: str = "") -> None:
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.status = status
|
|
38
|
+
self.message = message
|
|
39
|
+
self.code = code
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
AudioSource = Union[str, bytes, BinaryIO, os.PathLike]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VoiceKitClient:
|
|
46
|
+
"""Thin, typed wrapper over the VoiceKit REST API."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
api_key: str,
|
|
51
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
52
|
+
*,
|
|
53
|
+
timeout: float = 120.0,
|
|
54
|
+
) -> None:
|
|
55
|
+
if not api_key:
|
|
56
|
+
raise ValueError("api_key is required")
|
|
57
|
+
self._base_url = base_url.rstrip("/")
|
|
58
|
+
self._headers = {"X-Api-Key": api_key}
|
|
59
|
+
self._timeout = timeout
|
|
60
|
+
|
|
61
|
+
# ──────────────────────── Synthesis ────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def synthesize(
|
|
64
|
+
self,
|
|
65
|
+
text: str,
|
|
66
|
+
*,
|
|
67
|
+
voice: str = "preset_anna",
|
|
68
|
+
format: str = "mp3",
|
|
69
|
+
sample_rate: Optional[int] = None,
|
|
70
|
+
speed: Optional[float] = None,
|
|
71
|
+
pitch: Optional[float] = None,
|
|
72
|
+
emotion: Optional[str] = None,
|
|
73
|
+
ssml: Optional[bool] = None,
|
|
74
|
+
put_accent: Optional[bool] = None,
|
|
75
|
+
put_yo: Optional[bool] = None,
|
|
76
|
+
normalize: Optional[bool] = None,
|
|
77
|
+
model: Optional[str] = None,
|
|
78
|
+
language: Optional[str] = None,
|
|
79
|
+
effects: Optional[str] = None,
|
|
80
|
+
) -> bytes:
|
|
81
|
+
"""Synthesize speech and return the raw audio bytes.
|
|
82
|
+
|
|
83
|
+
``effects`` is an optional JSON-encoded string holding an array of
|
|
84
|
+
effect descriptors applied after synthesis (Pro/Business).
|
|
85
|
+
"""
|
|
86
|
+
body = _compact(
|
|
87
|
+
text=text,
|
|
88
|
+
voice=voice,
|
|
89
|
+
format=format,
|
|
90
|
+
sample_rate=sample_rate,
|
|
91
|
+
speed=speed,
|
|
92
|
+
pitch=pitch,
|
|
93
|
+
emotion=emotion,
|
|
94
|
+
ssml=ssml,
|
|
95
|
+
put_accent=put_accent,
|
|
96
|
+
put_yo=put_yo,
|
|
97
|
+
normalize=normalize,
|
|
98
|
+
model=model,
|
|
99
|
+
language=language,
|
|
100
|
+
effects=effects,
|
|
101
|
+
)
|
|
102
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
103
|
+
response = http.post("/v1/synthesize", headers=self._headers, json=body)
|
|
104
|
+
_raise_for_status(response)
|
|
105
|
+
return response.content
|
|
106
|
+
|
|
107
|
+
def synthesize_stream(
|
|
108
|
+
self,
|
|
109
|
+
text: str,
|
|
110
|
+
*,
|
|
111
|
+
voice: str = "preset_anna",
|
|
112
|
+
format: str = "mp3",
|
|
113
|
+
**kwargs: Any,
|
|
114
|
+
) -> Iterator[bytes]:
|
|
115
|
+
"""Synthesize and yield audio chunks as they are produced (Pro/Business)."""
|
|
116
|
+
body = _compact(text=text, voice=voice, format=format, **kwargs)
|
|
117
|
+
with httpx.stream(
|
|
118
|
+
"POST",
|
|
119
|
+
f"{self._base_url}/v1/synthesize/stream",
|
|
120
|
+
headers=self._headers,
|
|
121
|
+
json=body,
|
|
122
|
+
timeout=self._timeout,
|
|
123
|
+
) as response:
|
|
124
|
+
_raise_for_status(response)
|
|
125
|
+
yield from response.iter_bytes()
|
|
126
|
+
|
|
127
|
+
def synthesize_async(
|
|
128
|
+
self,
|
|
129
|
+
text: str,
|
|
130
|
+
*,
|
|
131
|
+
voice: str = "preset_anna",
|
|
132
|
+
format: str = "wav",
|
|
133
|
+
sample_rate: Optional[int] = None,
|
|
134
|
+
speed: Optional[float] = None,
|
|
135
|
+
model: Optional[str] = None,
|
|
136
|
+
language: Optional[str] = None,
|
|
137
|
+
webhook_url: Optional[str] = None,
|
|
138
|
+
) -> dict[str, Any]:
|
|
139
|
+
"""Queue a long-form (audiobook) synthesis job.
|
|
140
|
+
|
|
141
|
+
Poll with :meth:`get_synthesis_job` and download the produced WAV with
|
|
142
|
+
:meth:`download_synthesis_audio`. Long-form synthesis returns WAV only.
|
|
143
|
+
"""
|
|
144
|
+
body = _compact(
|
|
145
|
+
text=text,
|
|
146
|
+
voice=voice,
|
|
147
|
+
format=format,
|
|
148
|
+
sample_rate=sample_rate,
|
|
149
|
+
speed=speed,
|
|
150
|
+
model=model,
|
|
151
|
+
language=language,
|
|
152
|
+
)
|
|
153
|
+
params = {"webhookUrl": webhook_url} if webhook_url else None
|
|
154
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
155
|
+
response = http.post(
|
|
156
|
+
"/v1/synthesize/async", headers=self._headers, json=body, params=params
|
|
157
|
+
)
|
|
158
|
+
_raise_for_status(response)
|
|
159
|
+
return response.json()
|
|
160
|
+
|
|
161
|
+
def get_synthesis_job(self, job_id: str) -> dict[str, Any]:
|
|
162
|
+
"""Poll a long-form synthesis job; returns the audiobook manifest when done."""
|
|
163
|
+
return self._get_json(f"/v1/synthesize/async/{job_id}")
|
|
164
|
+
|
|
165
|
+
def download_synthesis_audio(self, job_id: str) -> bytes:
|
|
166
|
+
"""Download the produced WAV for a completed long-form synthesis job."""
|
|
167
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
168
|
+
response = http.get(
|
|
169
|
+
f"/v1/synthesize/async/{job_id}/audio", headers=self._headers
|
|
170
|
+
)
|
|
171
|
+
_raise_for_status(response)
|
|
172
|
+
return response.content
|
|
173
|
+
|
|
174
|
+
def voices(self) -> list[dict[str, Any]]:
|
|
175
|
+
"""Return the voice catalog."""
|
|
176
|
+
return self._get_json("/v1/voices")
|
|
177
|
+
|
|
178
|
+
def voice(self, voice_id: str) -> dict[str, Any]:
|
|
179
|
+
"""Return one voice by id (e.g. ``preset_anna``)."""
|
|
180
|
+
return self._get_json(f"/v1/voices/{voice_id}")
|
|
181
|
+
|
|
182
|
+
# ──────────────────────── Voice cloning ────────────────────────────
|
|
183
|
+
|
|
184
|
+
def create_clone_voice(
|
|
185
|
+
self,
|
|
186
|
+
name: str,
|
|
187
|
+
prompt_text: str,
|
|
188
|
+
samples: Union[AudioSource, Iterable[AudioSource]],
|
|
189
|
+
language: Optional[str] = None,
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
"""Create a cloned voice from reference audio (Pro/Business).
|
|
192
|
+
|
|
193
|
+
``samples`` is one or more reference audio files; ``prompt_text`` is the
|
|
194
|
+
exact transcript of the reference clip.
|
|
195
|
+
"""
|
|
196
|
+
sample_list = [samples] if _is_audio_source(samples) else list(samples)
|
|
197
|
+
files = [("samples", _file_tuple(sample)) for sample in sample_list]
|
|
198
|
+
data = _compact(name=name, prompt_text=prompt_text, language=language)
|
|
199
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
200
|
+
response = http.post(
|
|
201
|
+
"/v1/voices/clone", headers=self._headers, files=files, data=data
|
|
202
|
+
)
|
|
203
|
+
_raise_for_status(response)
|
|
204
|
+
return response.json()
|
|
205
|
+
|
|
206
|
+
def list_clone_voices(self) -> list[dict[str, Any]]:
|
|
207
|
+
"""List the caller's cloned voices."""
|
|
208
|
+
return self._get_json("/v1/voices/clone")
|
|
209
|
+
|
|
210
|
+
def get_clone_voice(self, clone_id: str) -> dict[str, Any]:
|
|
211
|
+
"""Return one cloned voice by id."""
|
|
212
|
+
return self._get_json(f"/v1/voices/clone/{clone_id}")
|
|
213
|
+
|
|
214
|
+
def delete_clone_voice(self, clone_id: str) -> None:
|
|
215
|
+
"""Delete a cloned voice (and its reference latents)."""
|
|
216
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
217
|
+
response = http.delete(
|
|
218
|
+
f"/v1/voices/clone/{clone_id}", headers=self._headers
|
|
219
|
+
)
|
|
220
|
+
_raise_for_status(response)
|
|
221
|
+
|
|
222
|
+
# ──────────────────────── Transcription ────────────────────────────
|
|
223
|
+
|
|
224
|
+
def transcribe(
|
|
225
|
+
self,
|
|
226
|
+
audio: AudioSource,
|
|
227
|
+
*,
|
|
228
|
+
language: Optional[str] = None,
|
|
229
|
+
diarization: bool = False,
|
|
230
|
+
webhook_url: Optional[str] = None,
|
|
231
|
+
keyterms: Optional[Iterable[str]] = None,
|
|
232
|
+
) -> dict[str, Any]:
|
|
233
|
+
"""Start an async transcription job; poll with get_transcription_job()."""
|
|
234
|
+
files = {"audio": _file_tuple(audio)}
|
|
235
|
+
data = _compact(language=language, diarization=diarization, webhookUrl=webhook_url)
|
|
236
|
+
if keyterms:
|
|
237
|
+
data["keyterms"] = ",".join(keyterms)
|
|
238
|
+
return self._post_form("/v1/transcribe", files=files, data=data)
|
|
239
|
+
|
|
240
|
+
def transcribe_sync(
|
|
241
|
+
self,
|
|
242
|
+
audio: AudioSource,
|
|
243
|
+
*,
|
|
244
|
+
language: Optional[str] = None,
|
|
245
|
+
diarization: bool = False,
|
|
246
|
+
keyterms: Optional[Iterable[str]] = None,
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
"""Transcribe a short file (≤ 3 min) synchronously."""
|
|
249
|
+
files = {"audio": _file_tuple(audio)}
|
|
250
|
+
data = _compact(language=language, diarization=diarization)
|
|
251
|
+
if keyterms:
|
|
252
|
+
data["keyterms"] = ",".join(keyterms)
|
|
253
|
+
return self._post_form("/v1/transcribe/sync", files=files, data=data)
|
|
254
|
+
|
|
255
|
+
def get_transcription_job(self, job_id: str) -> dict[str, Any]:
|
|
256
|
+
return self._get_json(f"/v1/transcribe/{job_id}")
|
|
257
|
+
|
|
258
|
+
def subtitles(self, job_id: str, format: str = "vtt") -> str:
|
|
259
|
+
"""Download VTT/SRT subtitles for a completed transcription job."""
|
|
260
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
261
|
+
response = http.get(
|
|
262
|
+
f"/v1/transcribe/{job_id}/subtitles",
|
|
263
|
+
headers=self._headers,
|
|
264
|
+
params={"format": format},
|
|
265
|
+
)
|
|
266
|
+
_raise_for_status(response)
|
|
267
|
+
return response.text
|
|
268
|
+
|
|
269
|
+
def vad(self, audio: AudioSource) -> dict[str, Any]:
|
|
270
|
+
"""Detect speech segments in an audio file (Silero VAD)."""
|
|
271
|
+
return self._post_form(
|
|
272
|
+
"/v1/vad", files={"audio": _file_tuple(audio)}, data={}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# ──────────────────────── Analysis ─────────────────────────────────
|
|
276
|
+
|
|
277
|
+
def analyze(
|
|
278
|
+
self,
|
|
279
|
+
audio: AudioSource,
|
|
280
|
+
*,
|
|
281
|
+
language: Optional[str] = None,
|
|
282
|
+
diarization: bool = False,
|
|
283
|
+
webhook_url: Optional[str] = None,
|
|
284
|
+
keyterms: Optional[Iterable[str]] = None,
|
|
285
|
+
) -> dict[str, Any]:
|
|
286
|
+
"""Start an async analysis job; poll with get_analysis_job()."""
|
|
287
|
+
files = {"audio": _file_tuple(audio)}
|
|
288
|
+
data = _compact(language=language, diarization=diarization, webhookUrl=webhook_url)
|
|
289
|
+
if keyterms:
|
|
290
|
+
data["keyterms"] = ",".join(keyterms)
|
|
291
|
+
return self._post_form("/v1/analyze", files=files, data=data)
|
|
292
|
+
|
|
293
|
+
def analyze_sync(
|
|
294
|
+
self,
|
|
295
|
+
audio: AudioSource,
|
|
296
|
+
*,
|
|
297
|
+
language: Optional[str] = None,
|
|
298
|
+
diarization: bool = False,
|
|
299
|
+
emotions: bool = True,
|
|
300
|
+
keywords: bool = True,
|
|
301
|
+
entities: bool = True,
|
|
302
|
+
keyterms: Optional[Iterable[str]] = None,
|
|
303
|
+
) -> dict[str, Any]:
|
|
304
|
+
"""Analyze a short file (≤ 3 min) synchronously."""
|
|
305
|
+
files = {"audio": _file_tuple(audio)}
|
|
306
|
+
data = _compact(
|
|
307
|
+
language=language,
|
|
308
|
+
diarization=diarization,
|
|
309
|
+
emotions=emotions,
|
|
310
|
+
keywords=keywords,
|
|
311
|
+
entities=entities,
|
|
312
|
+
)
|
|
313
|
+
if keyterms:
|
|
314
|
+
data["keyterms"] = ",".join(keyterms)
|
|
315
|
+
return self._post_form("/v1/analyze/sync", files=files, data=data)
|
|
316
|
+
|
|
317
|
+
def get_analysis_job(self, job_id: str) -> dict[str, Any]:
|
|
318
|
+
return self._get_json(f"/v1/analyze/{job_id}")
|
|
319
|
+
|
|
320
|
+
# ──────────────────────── Text intelligence ────────────────────────
|
|
321
|
+
|
|
322
|
+
def detect_language(self, text: str) -> dict[str, Any]:
|
|
323
|
+
return self._post_json("/v1/detect-language", {"text": text})
|
|
324
|
+
|
|
325
|
+
def redact(self, text: str, language: Optional[str] = None) -> dict[str, Any]:
|
|
326
|
+
return self._post_json("/v1/redact", _compact(text=text, language=language))
|
|
327
|
+
|
|
328
|
+
def topics(self, text: str, language: Optional[str] = None) -> dict[str, Any]:
|
|
329
|
+
return self._post_json("/v1/analyze/topics", _compact(text=text, language=language))
|
|
330
|
+
|
|
331
|
+
def summarize(
|
|
332
|
+
self, text: str, language: Optional[str] = None, max_sentences: Optional[int] = None
|
|
333
|
+
) -> dict[str, Any]:
|
|
334
|
+
return self._post_json(
|
|
335
|
+
"/v1/analyze/summarize",
|
|
336
|
+
_compact(text=text, language=language, max_sentences=max_sentences),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def moderate(self, text: str, language: Optional[str] = None) -> dict[str, Any]:
|
|
340
|
+
"""Flag profanity, insults and hate speech in raw text."""
|
|
341
|
+
return self._post_json("/v1/moderate", _compact(text=text, language=language))
|
|
342
|
+
|
|
343
|
+
# ──────────────────────── Audio / video effects ───────────────────
|
|
344
|
+
|
|
345
|
+
def apply_audio_effects(
|
|
346
|
+
self,
|
|
347
|
+
audio: AudioSource,
|
|
348
|
+
effects: Iterable[dict[str, Any]],
|
|
349
|
+
*,
|
|
350
|
+
output_format: str = "wav",
|
|
351
|
+
webhook_url: Optional[str] = None,
|
|
352
|
+
) -> dict[str, Any]:
|
|
353
|
+
"""Start an async audio-effects job; poll with get_audio_effects_job().
|
|
354
|
+
|
|
355
|
+
``effects`` is a list of effect descriptors, e.g.
|
|
356
|
+
``[{"type": "reverb", "room_size": 0.5}]``.
|
|
357
|
+
"""
|
|
358
|
+
files = {"audio": _file_tuple(audio)}
|
|
359
|
+
data = {"effects": json.dumps(list(effects), ensure_ascii=False), "output_format": output_format}
|
|
360
|
+
if webhook_url:
|
|
361
|
+
data["webhookUrl"] = webhook_url
|
|
362
|
+
return self._post_form("/v1/audio/effects", files=files, data=data)
|
|
363
|
+
|
|
364
|
+
def get_audio_effects_job(self, job_id: str) -> dict[str, Any]:
|
|
365
|
+
"""Poll an audio-effects job; returns the manifest when completed."""
|
|
366
|
+
return self._get_json(f"/v1/audio/effects/{job_id}")
|
|
367
|
+
|
|
368
|
+
def download_audio_effects(self, job_id: str) -> bytes:
|
|
369
|
+
"""Download the produced audio for a completed audio-effects job."""
|
|
370
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
371
|
+
response = http.get(f"/v1/audio/effects/{job_id}/audio", headers=self._headers)
|
|
372
|
+
_raise_for_status(response)
|
|
373
|
+
return response.content
|
|
374
|
+
|
|
375
|
+
def apply_video_effects(
|
|
376
|
+
self,
|
|
377
|
+
video: AudioSource,
|
|
378
|
+
effects: Iterable[dict[str, Any]],
|
|
379
|
+
*,
|
|
380
|
+
mode: str = "mux",
|
|
381
|
+
audio: Optional[AudioSource] = None,
|
|
382
|
+
output_format: Optional[str] = None,
|
|
383
|
+
webhook_url: Optional[str] = None,
|
|
384
|
+
) -> dict[str, Any]:
|
|
385
|
+
"""Start an async video-effects job; poll with get_video_effects_job().
|
|
386
|
+
|
|
387
|
+
``mode`` is ``mux`` (video output) or ``audio`` (processed audio
|
|
388
|
+
extracted from the video). For ``mux`` you may pass a separate ``audio``
|
|
389
|
+
file to replace the video's audio track.
|
|
390
|
+
"""
|
|
391
|
+
files = {"video": _file_tuple(video, "video/mp4")}
|
|
392
|
+
if audio is not None:
|
|
393
|
+
files["audio"] = _file_tuple(audio)
|
|
394
|
+
data = {"effects": json.dumps(list(effects), ensure_ascii=False), "mode": mode}
|
|
395
|
+
if output_format:
|
|
396
|
+
data["output_format"] = output_format
|
|
397
|
+
if webhook_url:
|
|
398
|
+
data["webhookUrl"] = webhook_url
|
|
399
|
+
return self._post_form("/v1/video/effects", files=files, data=data)
|
|
400
|
+
|
|
401
|
+
def get_video_effects_job(self, job_id: str) -> dict[str, Any]:
|
|
402
|
+
"""Poll a video-effects job; returns the manifest when completed."""
|
|
403
|
+
return self._get_json(f"/v1/video/effects/{job_id}")
|
|
404
|
+
|
|
405
|
+
def download_video_effects(self, job_id: str) -> bytes:
|
|
406
|
+
"""Download the produced artifact (video or audio) for a video-effects job."""
|
|
407
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
408
|
+
response = http.get(f"/v1/video/effects/{job_id}/file", headers=self._headers)
|
|
409
|
+
_raise_for_status(response)
|
|
410
|
+
return response.content
|
|
411
|
+
|
|
412
|
+
# ──────────────────────── Batch ────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
def batch_synthesize(self, items: Iterable[dict[str, Any]]) -> dict[str, Any]:
|
|
415
|
+
"""Queue a batch of synthesis requests; poll with get_batch()."""
|
|
416
|
+
return self._post_json("/v1/batch/synthesize", {"items": list(items)})
|
|
417
|
+
|
|
418
|
+
def batch_analyze(
|
|
419
|
+
self,
|
|
420
|
+
items: Iterable[dict[str, Any]],
|
|
421
|
+
) -> dict[str, Any]:
|
|
422
|
+
"""Queue a batch of analysis requests; each item needs inline base64 `audio`."""
|
|
423
|
+
return self._post_json("/v1/batch/analyze", {"items": list(items)})
|
|
424
|
+
|
|
425
|
+
def get_batch(self, batch_id: str) -> dict[str, Any]:
|
|
426
|
+
return self._get_json(f"/v1/batch/{batch_id}")
|
|
427
|
+
|
|
428
|
+
# ──────────────────────── Account ──────────────────────────────────
|
|
429
|
+
|
|
430
|
+
def usage(self) -> dict[str, Any]:
|
|
431
|
+
"""Current monthly character usage for the authenticated key."""
|
|
432
|
+
return self._get_json("/v1/usage")
|
|
433
|
+
|
|
434
|
+
def billing_balance(self) -> dict[str, Any]:
|
|
435
|
+
"""Current balance, plan and recent transactions."""
|
|
436
|
+
return self._get_json("/v1/billing/balance")
|
|
437
|
+
|
|
438
|
+
# ──────────────────────── Streaming (WebSocket) ────────────────────
|
|
439
|
+
|
|
440
|
+
async def transcribe_stream(
|
|
441
|
+
self,
|
|
442
|
+
*,
|
|
443
|
+
language: Optional[str] = None,
|
|
444
|
+
keyterms: Optional[Iterable[str]] = None,
|
|
445
|
+
interim: bool = True,
|
|
446
|
+
) -> TranscriptionStream:
|
|
447
|
+
"""Open a streaming transcription session (Pro/Business).
|
|
448
|
+
|
|
449
|
+
Send PCM16 chunks with ``send_audio()``, finalize with ``stop()``, then
|
|
450
|
+
iterate the returned object to receive JSON events.
|
|
451
|
+
"""
|
|
452
|
+
params: dict[str, Any] = {"language": language, "interim": interim}
|
|
453
|
+
if keyterms:
|
|
454
|
+
params["keyterms"] = ",".join(keyterms)
|
|
455
|
+
return TranscriptionStream(
|
|
456
|
+
await websockets_connect(self._ws_url("/v1/transcribe/stream", params))
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
async def vad_stream(self) -> VadStream:
|
|
460
|
+
"""Open a streaming turn-detection session (VAD events only)."""
|
|
461
|
+
return VadStream(
|
|
462
|
+
await websockets_connect(self._ws_url("/v1/vad/stream", {}))
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# ──────────────────────── Transport ────────────────────────────────
|
|
466
|
+
|
|
467
|
+
def _get_json(self, path: str) -> Any:
|
|
468
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
469
|
+
response = http.get(path, headers=self._headers)
|
|
470
|
+
_raise_for_status(response)
|
|
471
|
+
return response.json()
|
|
472
|
+
|
|
473
|
+
def _post_json(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
474
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
475
|
+
response = http.post(path, headers=self._headers, json=body)
|
|
476
|
+
_raise_for_status(response)
|
|
477
|
+
return response.json()
|
|
478
|
+
|
|
479
|
+
def _post_form(
|
|
480
|
+
self,
|
|
481
|
+
path: str,
|
|
482
|
+
*,
|
|
483
|
+
files: dict[str, Any],
|
|
484
|
+
data: dict[str, Any],
|
|
485
|
+
) -> dict[str, Any]:
|
|
486
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
487
|
+
response = http.post(path, headers=self._headers, files=files, data=data)
|
|
488
|
+
_raise_for_status(response)
|
|
489
|
+
return response.json()
|
|
490
|
+
|
|
491
|
+
def _ws_url(self, path: str, params: dict[str, Any]) -> str:
|
|
492
|
+
"""Build a ``ws(s)://`` URL for a streaming endpoint from the base URL."""
|
|
493
|
+
parts = urlsplit(self._base_url)
|
|
494
|
+
scheme = "wss" if parts.scheme == "https" else "ws"
|
|
495
|
+
query = urlencode(_query_params(params))
|
|
496
|
+
return urlunsplit((scheme, parts.netloc, path, query, ""))
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# ──────────────────────── Helpers ──────────────────────────────────────
|
|
500
|
+
|
|
501
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
502
|
+
if response.is_success:
|
|
503
|
+
return
|
|
504
|
+
code = ""
|
|
505
|
+
detail = response.text
|
|
506
|
+
try:
|
|
507
|
+
payload = response.json()
|
|
508
|
+
code = payload.get("code", "")
|
|
509
|
+
detail = payload.get("detail") or payload.get("title") or detail
|
|
510
|
+
except ValueError:
|
|
511
|
+
pass
|
|
512
|
+
raise VoiceKitError(response.status_code, detail, code)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _compact(**kwargs: Any) -> dict[str, Any]:
|
|
516
|
+
return {key: value for key, value in kwargs.items() if value is not None}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _file_tuple(audio: AudioSource, content_type: str = "audio/wav") -> tuple[str, bytes, str]:
|
|
520
|
+
if isinstance(audio, bytes):
|
|
521
|
+
return ("audio.wav", audio, content_type)
|
|
522
|
+
if hasattr(audio, "read"):
|
|
523
|
+
data = audio.read() # type: ignore[union-attr]
|
|
524
|
+
return ("audio.wav", data, content_type)
|
|
525
|
+
with open(os.fspath(audio), "rb") as f:
|
|
526
|
+
return (os.path.basename(os.fspath(audio)), f.read(), content_type)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _is_audio_source(value: Any) -> bool:
|
|
530
|
+
return isinstance(value, (str, bytes, os.PathLike)) or hasattr(value, "read")
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _query_params(params: dict[str, Any]) -> dict[str, Any]:
|
|
534
|
+
out: dict[str, Any] = {}
|
|
535
|
+
for key, value in params.items():
|
|
536
|
+
if value is None or value == "":
|
|
537
|
+
continue
|
|
538
|
+
out[key] = "true" if value is True else "false" if value is False else value
|
|
539
|
+
return out
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def b64(audio: AudioSource) -> str:
|
|
543
|
+
"""Encode audio (bytes or a file path) as base64 for batch_analyze()."""
|
|
544
|
+
_, data, _ = _file_tuple(audio)
|
|
545
|
+
return base64.b64encode(data).decode("ascii")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""WebSocket streaming helpers for VoiceKit.
|
|
2
|
+
|
|
3
|
+
Streaming requires the optional dependency::
|
|
4
|
+
|
|
5
|
+
pip install "voicekit[streaming]"
|
|
6
|
+
|
|
7
|
+
Both helpers send raw PCM16 (16 kHz, mono, little-endian) binary frames to the
|
|
8
|
+
server and receive JSON events back (`session`, `vad`, `partial`, `final`,
|
|
9
|
+
`error`). See the API docs for the full event schema.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from typing import Any, AsyncIterator, Optional
|
|
16
|
+
|
|
17
|
+
try: # optional dependency — imported lazily so the core SDK stays light
|
|
18
|
+
import websockets
|
|
19
|
+
except ImportError: # pragma: no cover
|
|
20
|
+
websockets = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _require_websockets() -> Any:
|
|
24
|
+
if websockets is None:
|
|
25
|
+
raise ImportError(
|
|
26
|
+
"WebSocket streaming requires the 'websockets' package. "
|
|
27
|
+
'Install it with: pip install "voicekit[streaming]"'
|
|
28
|
+
)
|
|
29
|
+
return websockets
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def websockets_connect(url: str) -> Any:
|
|
33
|
+
ws = _require_websockets()
|
|
34
|
+
return await ws.connect(url)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WsStream:
|
|
38
|
+
"""A connected streaming session (transcription or VAD)."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, ws: Any) -> None:
|
|
41
|
+
self._ws = ws
|
|
42
|
+
|
|
43
|
+
async def send_audio(self, pcm16: bytes) -> None:
|
|
44
|
+
"""Send raw PCM16 (16 kHz, mono, little-endian) as a binary frame."""
|
|
45
|
+
await self._ws.send(pcm16)
|
|
46
|
+
|
|
47
|
+
async def send_text(self, payload: Any) -> None:
|
|
48
|
+
"""Send a text frame; dicts/lists are JSON-encoded."""
|
|
49
|
+
if isinstance(payload, str):
|
|
50
|
+
await self._ws.send(payload)
|
|
51
|
+
else:
|
|
52
|
+
await self._ws.send(json.dumps(payload, ensure_ascii=False))
|
|
53
|
+
|
|
54
|
+
async def stop(self) -> None:
|
|
55
|
+
"""Signal end of speech so the server finalizes the utterance."""
|
|
56
|
+
await self.send_text({"type": "stop"})
|
|
57
|
+
|
|
58
|
+
async def recv_event(self) -> Optional[dict[str, Any]]:
|
|
59
|
+
"""Receive the next JSON event, or ``None`` when the session closed."""
|
|
60
|
+
try:
|
|
61
|
+
message = await self._ws.recv()
|
|
62
|
+
except _require_websockets().exceptions.ConnectionClosed:
|
|
63
|
+
return None
|
|
64
|
+
return message if isinstance(message, dict) else json.loads(message)
|
|
65
|
+
|
|
66
|
+
async def close(self) -> None:
|
|
67
|
+
await self._ws.close()
|
|
68
|
+
|
|
69
|
+
def __aiter__(self) -> AsyncIterator[dict[str, Any]]:
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
async def __anext__(self) -> dict[str, Any]:
|
|
73
|
+
event = await self.recv_event()
|
|
74
|
+
if event is None:
|
|
75
|
+
raise StopAsyncIteration
|
|
76
|
+
return event
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TranscriptionStream(WsStream):
|
|
80
|
+
"""Streaming transcription session (``WS /v1/transcribe/stream``)."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class VadStream(WsStream):
|
|
84
|
+
"""Streaming turn detection session (``WS /v1/vad/stream``)."""
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: voicekit-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for VoiceKit (synthesis, transcription, analysis, moderation, batches).
|
|
5
|
+
Author: VoiceKit
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://ttsapi.ru
|
|
8
|
+
Project-URL: Documentation, https://ttsapi.ru/swagger
|
|
9
|
+
Keywords: tts,speech,synthesis,transcription,stt,voice,russian
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: httpx>=0.24
|
|
13
|
+
Provides-Extra: streaming
|
|
14
|
+
Requires-Dist: websockets>=12.0; extra == "streaming"
|
|
15
|
+
|
|
16
|
+
# VoiceKit — Python SDK
|
|
17
|
+
|
|
18
|
+
Official Python wrapper for [VoiceKit](https://ttsapi.ru):
|
|
19
|
+
neural speech synthesis, transcription, sentiment analysis, and batch operations.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install voicekit-client
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from voicekit import VoiceKitClient, b64
|
|
31
|
+
|
|
32
|
+
client = VoiceKitClient(api_key="YOUR_KEY")
|
|
33
|
+
|
|
34
|
+
# Synthesis → raw audio bytes
|
|
35
|
+
audio = client.synthesize("Привет! Это синтез русской речи.", voice="preset_anna", format="mp3")
|
|
36
|
+
with open("speech.mp3", "wb") as f:
|
|
37
|
+
f.write(audio)
|
|
38
|
+
|
|
39
|
+
# Streaming (Pro/Business)
|
|
40
|
+
for chunk in client.synthesize_stream("Первое предложение. Второе."):
|
|
41
|
+
pass # write chunks to a file or socket
|
|
42
|
+
|
|
43
|
+
# Transcription (async → poll)
|
|
44
|
+
job = client.transcribe("audio.wav", keyterms=["диагноз"])
|
|
45
|
+
result = client.get_transcription_job(job["job_id"])
|
|
46
|
+
while result["status"] not in ("completed", "failed"):
|
|
47
|
+
result = client.get_transcription_job(job["job_id"])
|
|
48
|
+
|
|
49
|
+
# Short-file sync transcription
|
|
50
|
+
transcript = client.transcribe_sync("audio.wav")
|
|
51
|
+
|
|
52
|
+
# Analysis (sentiment + keywords + entities)
|
|
53
|
+
analysis = client.analyze_sync("audio.wav")
|
|
54
|
+
|
|
55
|
+
# Text intelligence
|
|
56
|
+
lang = client.detect_language("Как дела?")
|
|
57
|
+
topics = client.topics("Нейросети и алгоритмы")
|
|
58
|
+
summary = client.summarize("Длинный текст для резюме.", max_sentences=3)
|
|
59
|
+
moderation = client.moderate("Это оскорбительное сообщение.")
|
|
60
|
+
|
|
61
|
+
# Batches
|
|
62
|
+
batch = client.batch_synthesize([
|
|
63
|
+
{"text": "Первый текст", "voice": "preset_anna"},
|
|
64
|
+
{"text": "Второй текст", "voice": "dmitri"},
|
|
65
|
+
])
|
|
66
|
+
status = client.get_batch(batch["batch_id"])
|
|
67
|
+
|
|
68
|
+
analysis_batch = client.batch_analyze([
|
|
69
|
+
{"audio": b64("a.wav"), "language": "ru"},
|
|
70
|
+
{"audio": b64("b.wav"), "language": "ru"},
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
# Voice cloning (Pro/Business)
|
|
74
|
+
clone = client.create_clone_voice(
|
|
75
|
+
name="My voice",
|
|
76
|
+
prompt_text="Точный текст образца.",
|
|
77
|
+
samples="reference.wav",
|
|
78
|
+
)
|
|
79
|
+
print(client.list_clone_voices())
|
|
80
|
+
client.delete_clone_voice(clone["id"])
|
|
81
|
+
|
|
82
|
+
# VAD (speech segments)
|
|
83
|
+
segments = client.vad("audio.wav")
|
|
84
|
+
|
|
85
|
+
# Account
|
|
86
|
+
usage = client.usage()
|
|
87
|
+
balance = client.billing_balance()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Audio effects (Pro/Business)
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# Inline during synthesis — the chain is applied to the synthesized audio
|
|
94
|
+
audio = client.synthesize(
|
|
95
|
+
"Привет!",
|
|
96
|
+
effects='[{"type":"reverb","room_size":0.5},{"type":"pitch","semitones":2}]',
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Async processing of an existing file
|
|
100
|
+
import time
|
|
101
|
+
|
|
102
|
+
job = client.apply_audio_effects("voice.mp3", [{"type": "compressor", "ratio": 3}])
|
|
103
|
+
while client.get_audio_effects_job(job["job_id"])["status"] not in ("completed", "failed"):
|
|
104
|
+
time.sleep(1)
|
|
105
|
+
result = client.download_audio_effects(job["job_id"])
|
|
106
|
+
open("voice_fx.mp3", "wb").write(result)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### WebSocket streaming (Pro/Business)
|
|
110
|
+
|
|
111
|
+
Требует опциональной зависимости:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install "voicekit-client[streaming]"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import asyncio
|
|
119
|
+
|
|
120
|
+
async def main():
|
|
121
|
+
client = VoiceKitClient(api_key="YOUR_KEY")
|
|
122
|
+
|
|
123
|
+
stream = await client.transcribe_stream(language="ru", keyterms=["диагноз"])
|
|
124
|
+
await stream.send_audio(pcm16_chunk_1) # raw PCM16, 16 kHz mono
|
|
125
|
+
await stream.send_audio(pcm16_chunk_2)
|
|
126
|
+
await stream.stop() # finalize the utterance
|
|
127
|
+
async for event in stream: # session / vad / partial / final / error
|
|
128
|
+
print(event["type"], event)
|
|
129
|
+
await stream.close()
|
|
130
|
+
|
|
131
|
+
vad = await client.vad_stream() # VAD events only (speech_started/ended)
|
|
132
|
+
await vad.send_audio(pcm16_chunk)
|
|
133
|
+
await vad.stop()
|
|
134
|
+
async for event in vad:
|
|
135
|
+
print(event["type"], event)
|
|
136
|
+
await vad.close()
|
|
137
|
+
|
|
138
|
+
asyncio.run(main())
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Configuration
|
|
142
|
+
|
|
143
|
+
| Option | Default | Description |
|
|
144
|
+
| --- | --- | --- |
|
|
145
|
+
| `api_key` | — | API key (required) |
|
|
146
|
+
| `base_url` | `https://ttsapi.ru` | API base URL (e.g. `http://localhost:5080` for local dev) |
|
|
147
|
+
| `timeout` | `120.0` | Per-request timeout in seconds |
|
|
148
|
+
|
|
149
|
+
Errors raise `VoiceKitError` with `.status` (HTTP status), `.code` (machine-readable
|
|
150
|
+
code) and `.message`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
voicekit/__init__.py
|
|
4
|
+
voicekit/client.py
|
|
5
|
+
voicekit/streaming.py
|
|
6
|
+
voicekit_client.egg-info/PKG-INFO
|
|
7
|
+
voicekit_client.egg-info/SOURCES.txt
|
|
8
|
+
voicekit_client.egg-info/dependency_links.txt
|
|
9
|
+
voicekit_client.egg-info/requires.txt
|
|
10
|
+
voicekit_client.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
voicekit
|