ttsapi-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.
- ttsapi_client-0.1.0/PKG-INFO +131 -0
- ttsapi_client-0.1.0/README.md +116 -0
- ttsapi_client-0.1.0/pyproject.toml +24 -0
- ttsapi_client-0.1.0/setup.cfg +4 -0
- ttsapi_client-0.1.0/ttsapi/__init__.py +11 -0
- ttsapi_client-0.1.0/ttsapi/client.py +422 -0
- ttsapi_client-0.1.0/ttsapi/streaming.py +84 -0
- ttsapi_client-0.1.0/ttsapi_client.egg-info/PKG-INFO +131 -0
- ttsapi_client-0.1.0/ttsapi_client.egg-info/SOURCES.txt +10 -0
- ttsapi_client-0.1.0/ttsapi_client.egg-info/dependency_links.txt +1 -0
- ttsapi_client-0.1.0/ttsapi_client.egg-info/requires.txt +4 -0
- ttsapi_client-0.1.0/ttsapi_client.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ttsapi-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Russian TTS / Voice Intelligence API (synthesis, transcription, analysis, moderation, batches).
|
|
5
|
+
Author: Russian TTS API
|
|
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
|
+
# Russian TTS API — Python SDK
|
|
17
|
+
|
|
18
|
+
Official Python wrapper for the [Voice Intelligence API](https://ttsapi.ru):
|
|
19
|
+
neural speech synthesis, transcription, sentiment analysis, and batch operations.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install ttsapi-client
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from ttsapi import RussianTtsClient, b64
|
|
31
|
+
|
|
32
|
+
client = RussianTtsClient(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
|
+
### WebSocket streaming (Pro/Business)
|
|
91
|
+
|
|
92
|
+
Требует опциональной зависимости:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install "ttsapi-client[streaming]"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import asyncio
|
|
100
|
+
|
|
101
|
+
async def main():
|
|
102
|
+
client = RussianTtsClient(api_key="YOUR_KEY")
|
|
103
|
+
|
|
104
|
+
stream = await client.transcribe_stream(language="ru", keyterms=["диагноз"])
|
|
105
|
+
await stream.send_audio(pcm16_chunk_1) # raw PCM16, 16 kHz mono
|
|
106
|
+
await stream.send_audio(pcm16_chunk_2)
|
|
107
|
+
await stream.stop() # finalize the utterance
|
|
108
|
+
async for event in stream: # session / vad / partial / final / error
|
|
109
|
+
print(event["type"], event)
|
|
110
|
+
await stream.close()
|
|
111
|
+
|
|
112
|
+
vad = await client.vad_stream() # VAD events only (speech_started/ended)
|
|
113
|
+
await vad.send_audio(pcm16_chunk)
|
|
114
|
+
await vad.stop()
|
|
115
|
+
async for event in vad:
|
|
116
|
+
print(event["type"], event)
|
|
117
|
+
await vad.close()
|
|
118
|
+
|
|
119
|
+
asyncio.run(main())
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Configuration
|
|
123
|
+
|
|
124
|
+
| Option | Default | Description |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| `api_key` | — | API key (required) |
|
|
127
|
+
| `base_url` | `https://ttsapi.ru` | API base URL (e.g. `http://localhost:5080` for local dev) |
|
|
128
|
+
| `timeout` | `120.0` | Per-request timeout in seconds |
|
|
129
|
+
|
|
130
|
+
Errors raise `RussianTtsError` with `.status` (HTTP status), `.code` (machine-readable
|
|
131
|
+
code) and `.message`.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Russian TTS API — Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python wrapper for the [Voice Intelligence API](https://ttsapi.ru):
|
|
4
|
+
neural speech synthesis, transcription, sentiment analysis, and batch operations.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install ttsapi-client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from ttsapi import RussianTtsClient, b64
|
|
16
|
+
|
|
17
|
+
client = RussianTtsClient(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
|
+
### WebSocket streaming (Pro/Business)
|
|
76
|
+
|
|
77
|
+
Требует опциональной зависимости:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install "ttsapi-client[streaming]"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import asyncio
|
|
85
|
+
|
|
86
|
+
async def main():
|
|
87
|
+
client = RussianTtsClient(api_key="YOUR_KEY")
|
|
88
|
+
|
|
89
|
+
stream = await client.transcribe_stream(language="ru", keyterms=["диагноз"])
|
|
90
|
+
await stream.send_audio(pcm16_chunk_1) # raw PCM16, 16 kHz mono
|
|
91
|
+
await stream.send_audio(pcm16_chunk_2)
|
|
92
|
+
await stream.stop() # finalize the utterance
|
|
93
|
+
async for event in stream: # session / vad / partial / final / error
|
|
94
|
+
print(event["type"], event)
|
|
95
|
+
await stream.close()
|
|
96
|
+
|
|
97
|
+
vad = await client.vad_stream() # VAD events only (speech_started/ended)
|
|
98
|
+
await vad.send_audio(pcm16_chunk)
|
|
99
|
+
await vad.stop()
|
|
100
|
+
async for event in vad:
|
|
101
|
+
print(event["type"], event)
|
|
102
|
+
await vad.close()
|
|
103
|
+
|
|
104
|
+
asyncio.run(main())
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Configuration
|
|
108
|
+
|
|
109
|
+
| Option | Default | Description |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| `api_key` | — | API key (required) |
|
|
112
|
+
| `base_url` | `https://ttsapi.ru` | API base URL (e.g. `http://localhost:5080` for local dev) |
|
|
113
|
+
| `timeout` | `120.0` | Per-request timeout in seconds |
|
|
114
|
+
|
|
115
|
+
Errors raise `RussianTtsError` with `.status` (HTTP status), `.code` (machine-readable
|
|
116
|
+
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 = "ttsapi-client"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for the Russian TTS / Voice Intelligence API (synthesis, transcription, analysis, moderation, batches)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
authors = [{ name = "Russian TTS API" }]
|
|
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 = ["ttsapi*"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from ttsapi.client import RussianTtsClient, RussianTtsError
|
|
2
|
+
from ttsapi.streaming import TranscriptionStream, VadStream, WsStream
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"RussianTtsClient",
|
|
6
|
+
"RussianTtsError",
|
|
7
|
+
"TranscriptionStream",
|
|
8
|
+
"VadStream",
|
|
9
|
+
"WsStream",
|
|
10
|
+
]
|
|
11
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""Official Python SDK for the Russian TTS / Voice Intelligence API.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from ttsapi import RussianTtsClient
|
|
6
|
+
|
|
7
|
+
client = RussianTtsClient(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 os
|
|
21
|
+
from typing import Any, BinaryIO, Iterable, Iterator, Optional, Union
|
|
22
|
+
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
23
|
+
|
|
24
|
+
import httpx
|
|
25
|
+
|
|
26
|
+
from .streaming import TranscriptionStream, VadStream, websockets_connect
|
|
27
|
+
|
|
28
|
+
DEFAULT_BASE_URL = "https://ttsapi.ru"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RussianTtsError(Exception):
|
|
32
|
+
"""Raised when the API returns a non-2xx response."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, status: int, message: str, code: str = "") -> None:
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.status = status
|
|
37
|
+
self.message = message
|
|
38
|
+
self.code = code
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
AudioSource = Union[str, bytes, BinaryIO, os.PathLike]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RussianTtsClient:
|
|
45
|
+
"""Thin, typed wrapper over the Voice Intelligence REST API."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
api_key: str,
|
|
50
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
51
|
+
*,
|
|
52
|
+
timeout: float = 120.0,
|
|
53
|
+
) -> None:
|
|
54
|
+
if not api_key:
|
|
55
|
+
raise ValueError("api_key is required")
|
|
56
|
+
self._base_url = base_url.rstrip("/")
|
|
57
|
+
self._headers = {"X-Api-Key": api_key}
|
|
58
|
+
self._timeout = timeout
|
|
59
|
+
|
|
60
|
+
# ──────────────────────── Synthesis ────────────────────────────────
|
|
61
|
+
|
|
62
|
+
def synthesize(
|
|
63
|
+
self,
|
|
64
|
+
text: str,
|
|
65
|
+
*,
|
|
66
|
+
voice: str = "preset_anna",
|
|
67
|
+
format: str = "mp3",
|
|
68
|
+
sample_rate: Optional[int] = None,
|
|
69
|
+
speed: Optional[float] = None,
|
|
70
|
+
pitch: Optional[float] = None,
|
|
71
|
+
emotion: Optional[str] = None,
|
|
72
|
+
ssml: Optional[bool] = None,
|
|
73
|
+
put_accent: Optional[bool] = None,
|
|
74
|
+
put_yo: Optional[bool] = None,
|
|
75
|
+
normalize: Optional[bool] = None,
|
|
76
|
+
model: Optional[str] = None,
|
|
77
|
+
language: Optional[str] = None,
|
|
78
|
+
) -> bytes:
|
|
79
|
+
"""Synthesize speech and return the raw audio bytes."""
|
|
80
|
+
body = _compact(
|
|
81
|
+
text=text,
|
|
82
|
+
voice=voice,
|
|
83
|
+
format=format,
|
|
84
|
+
sample_rate=sample_rate,
|
|
85
|
+
speed=speed,
|
|
86
|
+
pitch=pitch,
|
|
87
|
+
emotion=emotion,
|
|
88
|
+
ssml=ssml,
|
|
89
|
+
put_accent=put_accent,
|
|
90
|
+
put_yo=put_yo,
|
|
91
|
+
normalize=normalize,
|
|
92
|
+
model=model,
|
|
93
|
+
language=language,
|
|
94
|
+
)
|
|
95
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
96
|
+
response = http.post("/v1/synthesize", headers=self._headers, json=body)
|
|
97
|
+
_raise_for_status(response)
|
|
98
|
+
return response.content
|
|
99
|
+
|
|
100
|
+
def synthesize_stream(
|
|
101
|
+
self,
|
|
102
|
+
text: str,
|
|
103
|
+
*,
|
|
104
|
+
voice: str = "preset_anna",
|
|
105
|
+
format: str = "mp3",
|
|
106
|
+
**kwargs: Any,
|
|
107
|
+
) -> Iterator[bytes]:
|
|
108
|
+
"""Synthesize and yield audio chunks as they are produced (Pro/Business)."""
|
|
109
|
+
body = _compact(text=text, voice=voice, format=format, **kwargs)
|
|
110
|
+
with httpx.stream(
|
|
111
|
+
"POST",
|
|
112
|
+
f"{self._base_url}/v1/synthesize/stream",
|
|
113
|
+
headers=self._headers,
|
|
114
|
+
json=body,
|
|
115
|
+
timeout=self._timeout,
|
|
116
|
+
) as response:
|
|
117
|
+
_raise_for_status(response)
|
|
118
|
+
yield from response.iter_bytes()
|
|
119
|
+
|
|
120
|
+
def voices(self) -> list[dict[str, Any]]:
|
|
121
|
+
"""Return the voice catalog."""
|
|
122
|
+
return self._get_json("/v1/voices")
|
|
123
|
+
|
|
124
|
+
def voice(self, voice_id: str) -> dict[str, Any]:
|
|
125
|
+
"""Return one voice by id (e.g. ``preset_anna``)."""
|
|
126
|
+
return self._get_json(f"/v1/voices/{voice_id}")
|
|
127
|
+
|
|
128
|
+
# ──────────────────────── Voice cloning ────────────────────────────
|
|
129
|
+
|
|
130
|
+
def create_clone_voice(
|
|
131
|
+
self,
|
|
132
|
+
name: str,
|
|
133
|
+
prompt_text: str,
|
|
134
|
+
samples: Union[AudioSource, Iterable[AudioSource]],
|
|
135
|
+
language: Optional[str] = None,
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
"""Create a cloned voice from reference audio (Pro/Business).
|
|
138
|
+
|
|
139
|
+
``samples`` is one or more reference audio files; ``prompt_text`` is the
|
|
140
|
+
exact transcript of the reference clip.
|
|
141
|
+
"""
|
|
142
|
+
sample_list = [samples] if _is_audio_source(samples) else list(samples)
|
|
143
|
+
files = [("samples", _file_tuple(sample)) for sample in sample_list]
|
|
144
|
+
data = _compact(name=name, prompt_text=prompt_text, language=language)
|
|
145
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
146
|
+
response = http.post(
|
|
147
|
+
"/v1/voices/clone", headers=self._headers, files=files, data=data
|
|
148
|
+
)
|
|
149
|
+
_raise_for_status(response)
|
|
150
|
+
return response.json()
|
|
151
|
+
|
|
152
|
+
def list_clone_voices(self) -> list[dict[str, Any]]:
|
|
153
|
+
"""List the caller's cloned voices."""
|
|
154
|
+
return self._get_json("/v1/voices/clone")
|
|
155
|
+
|
|
156
|
+
def get_clone_voice(self, clone_id: str) -> dict[str, Any]:
|
|
157
|
+
"""Return one cloned voice by id."""
|
|
158
|
+
return self._get_json(f"/v1/voices/clone/{clone_id}")
|
|
159
|
+
|
|
160
|
+
def delete_clone_voice(self, clone_id: str) -> None:
|
|
161
|
+
"""Delete a cloned voice (and its reference latents)."""
|
|
162
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
163
|
+
response = http.delete(
|
|
164
|
+
f"/v1/voices/clone/{clone_id}", headers=self._headers
|
|
165
|
+
)
|
|
166
|
+
_raise_for_status(response)
|
|
167
|
+
|
|
168
|
+
# ──────────────────────── Transcription ────────────────────────────
|
|
169
|
+
|
|
170
|
+
def transcribe(
|
|
171
|
+
self,
|
|
172
|
+
audio: AudioSource,
|
|
173
|
+
*,
|
|
174
|
+
language: Optional[str] = None,
|
|
175
|
+
diarization: bool = False,
|
|
176
|
+
webhook_url: Optional[str] = None,
|
|
177
|
+
keyterms: Optional[Iterable[str]] = None,
|
|
178
|
+
) -> dict[str, Any]:
|
|
179
|
+
"""Start an async transcription job; poll with get_transcription_job()."""
|
|
180
|
+
files = {"audio": _file_tuple(audio)}
|
|
181
|
+
data = _compact(language=language, diarization=diarization, webhookUrl=webhook_url)
|
|
182
|
+
if keyterms:
|
|
183
|
+
data["keyterms"] = ",".join(keyterms)
|
|
184
|
+
return self._post_form("/v1/transcribe", files=files, data=data)
|
|
185
|
+
|
|
186
|
+
def transcribe_sync(
|
|
187
|
+
self,
|
|
188
|
+
audio: AudioSource,
|
|
189
|
+
*,
|
|
190
|
+
language: Optional[str] = None,
|
|
191
|
+
diarization: bool = False,
|
|
192
|
+
keyterms: Optional[Iterable[str]] = None,
|
|
193
|
+
) -> dict[str, Any]:
|
|
194
|
+
"""Transcribe a short file (≤ 3 min) synchronously."""
|
|
195
|
+
files = {"audio": _file_tuple(audio)}
|
|
196
|
+
data = _compact(language=language, diarization=diarization)
|
|
197
|
+
if keyterms:
|
|
198
|
+
data["keyterms"] = ",".join(keyterms)
|
|
199
|
+
return self._post_form("/v1/transcribe/sync", files=files, data=data)
|
|
200
|
+
|
|
201
|
+
def get_transcription_job(self, job_id: str) -> dict[str, Any]:
|
|
202
|
+
return self._get_json(f"/v1/transcribe/{job_id}")
|
|
203
|
+
|
|
204
|
+
def subtitles(self, job_id: str, format: str = "vtt") -> str:
|
|
205
|
+
"""Download VTT/SRT subtitles for a completed transcription job."""
|
|
206
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
207
|
+
response = http.get(
|
|
208
|
+
f"/v1/transcribe/{job_id}/subtitles",
|
|
209
|
+
headers=self._headers,
|
|
210
|
+
params={"format": format},
|
|
211
|
+
)
|
|
212
|
+
_raise_for_status(response)
|
|
213
|
+
return response.text
|
|
214
|
+
|
|
215
|
+
def vad(self, audio: AudioSource) -> dict[str, Any]:
|
|
216
|
+
"""Detect speech segments in an audio file (Silero VAD)."""
|
|
217
|
+
return self._post_form(
|
|
218
|
+
"/v1/vad", files={"audio": _file_tuple(audio)}, data={}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# ──────────────────────── Analysis ─────────────────────────────────
|
|
222
|
+
|
|
223
|
+
def analyze(
|
|
224
|
+
self,
|
|
225
|
+
audio: AudioSource,
|
|
226
|
+
*,
|
|
227
|
+
language: Optional[str] = None,
|
|
228
|
+
diarization: bool = False,
|
|
229
|
+
webhook_url: Optional[str] = None,
|
|
230
|
+
keyterms: Optional[Iterable[str]] = None,
|
|
231
|
+
) -> dict[str, Any]:
|
|
232
|
+
"""Start an async analysis job; poll with get_analysis_job()."""
|
|
233
|
+
files = {"audio": _file_tuple(audio)}
|
|
234
|
+
data = _compact(language=language, diarization=diarization, webhookUrl=webhook_url)
|
|
235
|
+
if keyterms:
|
|
236
|
+
data["keyterms"] = ",".join(keyterms)
|
|
237
|
+
return self._post_form("/v1/analyze", files=files, data=data)
|
|
238
|
+
|
|
239
|
+
def analyze_sync(
|
|
240
|
+
self,
|
|
241
|
+
audio: AudioSource,
|
|
242
|
+
*,
|
|
243
|
+
language: Optional[str] = None,
|
|
244
|
+
diarization: bool = False,
|
|
245
|
+
emotions: bool = True,
|
|
246
|
+
keywords: bool = True,
|
|
247
|
+
entities: bool = True,
|
|
248
|
+
keyterms: Optional[Iterable[str]] = None,
|
|
249
|
+
) -> dict[str, Any]:
|
|
250
|
+
"""Analyze a short file (≤ 3 min) synchronously."""
|
|
251
|
+
files = {"audio": _file_tuple(audio)}
|
|
252
|
+
data = _compact(
|
|
253
|
+
language=language,
|
|
254
|
+
diarization=diarization,
|
|
255
|
+
emotions=emotions,
|
|
256
|
+
keywords=keywords,
|
|
257
|
+
entities=entities,
|
|
258
|
+
)
|
|
259
|
+
if keyterms:
|
|
260
|
+
data["keyterms"] = ",".join(keyterms)
|
|
261
|
+
return self._post_form("/v1/analyze/sync", files=files, data=data)
|
|
262
|
+
|
|
263
|
+
def get_analysis_job(self, job_id: str) -> dict[str, Any]:
|
|
264
|
+
return self._get_json(f"/v1/analyze/{job_id}")
|
|
265
|
+
|
|
266
|
+
# ──────────────────────── Text intelligence ────────────────────────
|
|
267
|
+
|
|
268
|
+
def detect_language(self, text: str) -> dict[str, Any]:
|
|
269
|
+
return self._post_json("/v1/detect-language", {"text": text})
|
|
270
|
+
|
|
271
|
+
def redact(self, text: str, language: Optional[str] = None) -> dict[str, Any]:
|
|
272
|
+
return self._post_json("/v1/redact", _compact(text=text, language=language))
|
|
273
|
+
|
|
274
|
+
def topics(self, text: str, language: Optional[str] = None) -> dict[str, Any]:
|
|
275
|
+
return self._post_json("/v1/analyze/topics", _compact(text=text, language=language))
|
|
276
|
+
|
|
277
|
+
def summarize(
|
|
278
|
+
self, text: str, language: Optional[str] = None, max_sentences: Optional[int] = None
|
|
279
|
+
) -> dict[str, Any]:
|
|
280
|
+
return self._post_json(
|
|
281
|
+
"/v1/analyze/summarize",
|
|
282
|
+
_compact(text=text, language=language, max_sentences=max_sentences),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def moderate(self, text: str, language: Optional[str] = None) -> dict[str, Any]:
|
|
286
|
+
"""Flag profanity, insults and hate speech in raw text."""
|
|
287
|
+
return self._post_json("/v1/moderate", _compact(text=text, language=language))
|
|
288
|
+
|
|
289
|
+
# ──────────────────────── Batch ────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
def batch_synthesize(self, items: Iterable[dict[str, Any]]) -> dict[str, Any]:
|
|
292
|
+
"""Queue a batch of synthesis requests; poll with get_batch()."""
|
|
293
|
+
return self._post_json("/v1/batch/synthesize", {"items": list(items)})
|
|
294
|
+
|
|
295
|
+
def batch_analyze(
|
|
296
|
+
self,
|
|
297
|
+
items: Iterable[dict[str, Any]],
|
|
298
|
+
) -> dict[str, Any]:
|
|
299
|
+
"""Queue a batch of analysis requests; each item needs inline base64 `audio`."""
|
|
300
|
+
return self._post_json("/v1/batch/analyze", {"items": list(items)})
|
|
301
|
+
|
|
302
|
+
def get_batch(self, batch_id: str) -> dict[str, Any]:
|
|
303
|
+
return self._get_json(f"/v1/batch/{batch_id}")
|
|
304
|
+
|
|
305
|
+
# ──────────────────────── Account ──────────────────────────────────
|
|
306
|
+
|
|
307
|
+
def usage(self) -> dict[str, Any]:
|
|
308
|
+
"""Current monthly character usage for the authenticated key."""
|
|
309
|
+
return self._get_json("/v1/usage")
|
|
310
|
+
|
|
311
|
+
def billing_balance(self) -> dict[str, Any]:
|
|
312
|
+
"""Current balance, plan and recent transactions."""
|
|
313
|
+
return self._get_json("/v1/billing/balance")
|
|
314
|
+
|
|
315
|
+
# ──────────────────────── Streaming (WebSocket) ────────────────────
|
|
316
|
+
|
|
317
|
+
async def transcribe_stream(
|
|
318
|
+
self,
|
|
319
|
+
*,
|
|
320
|
+
language: Optional[str] = None,
|
|
321
|
+
keyterms: Optional[Iterable[str]] = None,
|
|
322
|
+
interim: bool = True,
|
|
323
|
+
) -> TranscriptionStream:
|
|
324
|
+
"""Open a streaming transcription session (Pro/Business).
|
|
325
|
+
|
|
326
|
+
Send PCM16 chunks with ``send_audio()``, finalize with ``stop()``, then
|
|
327
|
+
iterate the returned object to receive JSON events.
|
|
328
|
+
"""
|
|
329
|
+
params: dict[str, Any] = {"language": language, "interim": interim}
|
|
330
|
+
if keyterms:
|
|
331
|
+
params["keyterms"] = ",".join(keyterms)
|
|
332
|
+
return TranscriptionStream(
|
|
333
|
+
await websockets_connect(self._ws_url("/v1/transcribe/stream", params))
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
async def vad_stream(self) -> VadStream:
|
|
337
|
+
"""Open a streaming turn-detection session (VAD events only)."""
|
|
338
|
+
return VadStream(
|
|
339
|
+
await websockets_connect(self._ws_url("/v1/vad/stream", {}))
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# ──────────────────────── Transport ────────────────────────────────
|
|
343
|
+
|
|
344
|
+
def _get_json(self, path: str) -> Any:
|
|
345
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
346
|
+
response = http.get(path, headers=self._headers)
|
|
347
|
+
_raise_for_status(response)
|
|
348
|
+
return response.json()
|
|
349
|
+
|
|
350
|
+
def _post_json(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
351
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
352
|
+
response = http.post(path, headers=self._headers, json=body)
|
|
353
|
+
_raise_for_status(response)
|
|
354
|
+
return response.json()
|
|
355
|
+
|
|
356
|
+
def _post_form(
|
|
357
|
+
self,
|
|
358
|
+
path: str,
|
|
359
|
+
*,
|
|
360
|
+
files: dict[str, Any],
|
|
361
|
+
data: dict[str, Any],
|
|
362
|
+
) -> dict[str, Any]:
|
|
363
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
364
|
+
response = http.post(path, headers=self._headers, files=files, data=data)
|
|
365
|
+
_raise_for_status(response)
|
|
366
|
+
return response.json()
|
|
367
|
+
|
|
368
|
+
def _ws_url(self, path: str, params: dict[str, Any]) -> str:
|
|
369
|
+
"""Build a ``ws(s)://`` URL for a streaming endpoint from the base URL."""
|
|
370
|
+
parts = urlsplit(self._base_url)
|
|
371
|
+
scheme = "wss" if parts.scheme == "https" else "ws"
|
|
372
|
+
query = urlencode(_query_params(params))
|
|
373
|
+
return urlunsplit((scheme, parts.netloc, path, query, ""))
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ──────────────────────── Helpers ──────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
379
|
+
if response.is_success:
|
|
380
|
+
return
|
|
381
|
+
code = ""
|
|
382
|
+
detail = response.text
|
|
383
|
+
try:
|
|
384
|
+
payload = response.json()
|
|
385
|
+
code = payload.get("code", "")
|
|
386
|
+
detail = payload.get("detail") or payload.get("title") or detail
|
|
387
|
+
except ValueError:
|
|
388
|
+
pass
|
|
389
|
+
raise RussianTtsError(response.status_code, detail, code)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _compact(**kwargs: Any) -> dict[str, Any]:
|
|
393
|
+
return {key: value for key, value in kwargs.items() if value is not None}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _file_tuple(audio: AudioSource) -> tuple[str, bytes, str]:
|
|
397
|
+
if isinstance(audio, bytes):
|
|
398
|
+
return ("audio.wav", audio, "audio/wav")
|
|
399
|
+
if hasattr(audio, "read"):
|
|
400
|
+
data = audio.read() # type: ignore[union-attr]
|
|
401
|
+
return ("audio.wav", data, "audio/wav")
|
|
402
|
+
with open(os.fspath(audio), "rb") as f:
|
|
403
|
+
return (os.path.basename(os.fspath(audio)), f.read(), "audio/wav")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _is_audio_source(value: Any) -> bool:
|
|
407
|
+
return isinstance(value, (str, bytes, os.PathLike)) or hasattr(value, "read")
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _query_params(params: dict[str, Any]) -> dict[str, Any]:
|
|
411
|
+
out: dict[str, Any] = {}
|
|
412
|
+
for key, value in params.items():
|
|
413
|
+
if value is None or value == "":
|
|
414
|
+
continue
|
|
415
|
+
out[key] = "true" if value is True else "false" if value is False else value
|
|
416
|
+
return out
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def b64(audio: AudioSource) -> str:
|
|
420
|
+
"""Encode audio (bytes or a file path) as base64 for batch_analyze()."""
|
|
421
|
+
_, data, _ = _file_tuple(audio)
|
|
422
|
+
return base64.b64encode(data).decode("ascii")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""WebSocket streaming helpers for the Russian TTS / Voice Intelligence API.
|
|
2
|
+
|
|
3
|
+
Streaming requires the optional dependency::
|
|
4
|
+
|
|
5
|
+
pip install "ttsapi[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 "ttsapi[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,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ttsapi-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Russian TTS / Voice Intelligence API (synthesis, transcription, analysis, moderation, batches).
|
|
5
|
+
Author: Russian TTS API
|
|
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
|
+
# Russian TTS API — Python SDK
|
|
17
|
+
|
|
18
|
+
Official Python wrapper for the [Voice Intelligence API](https://ttsapi.ru):
|
|
19
|
+
neural speech synthesis, transcription, sentiment analysis, and batch operations.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install ttsapi-client
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from ttsapi import RussianTtsClient, b64
|
|
31
|
+
|
|
32
|
+
client = RussianTtsClient(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
|
+
### WebSocket streaming (Pro/Business)
|
|
91
|
+
|
|
92
|
+
Требует опциональной зависимости:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install "ttsapi-client[streaming]"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import asyncio
|
|
100
|
+
|
|
101
|
+
async def main():
|
|
102
|
+
client = RussianTtsClient(api_key="YOUR_KEY")
|
|
103
|
+
|
|
104
|
+
stream = await client.transcribe_stream(language="ru", keyterms=["диагноз"])
|
|
105
|
+
await stream.send_audio(pcm16_chunk_1) # raw PCM16, 16 kHz mono
|
|
106
|
+
await stream.send_audio(pcm16_chunk_2)
|
|
107
|
+
await stream.stop() # finalize the utterance
|
|
108
|
+
async for event in stream: # session / vad / partial / final / error
|
|
109
|
+
print(event["type"], event)
|
|
110
|
+
await stream.close()
|
|
111
|
+
|
|
112
|
+
vad = await client.vad_stream() # VAD events only (speech_started/ended)
|
|
113
|
+
await vad.send_audio(pcm16_chunk)
|
|
114
|
+
await vad.stop()
|
|
115
|
+
async for event in vad:
|
|
116
|
+
print(event["type"], event)
|
|
117
|
+
await vad.close()
|
|
118
|
+
|
|
119
|
+
asyncio.run(main())
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Configuration
|
|
123
|
+
|
|
124
|
+
| Option | Default | Description |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| `api_key` | — | API key (required) |
|
|
127
|
+
| `base_url` | `https://ttsapi.ru` | API base URL (e.g. `http://localhost:5080` for local dev) |
|
|
128
|
+
| `timeout` | `120.0` | Per-request timeout in seconds |
|
|
129
|
+
|
|
130
|
+
Errors raise `RussianTtsError` with `.status` (HTTP status), `.code` (machine-readable
|
|
131
|
+
code) and `.message`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
ttsapi/__init__.py
|
|
4
|
+
ttsapi/client.py
|
|
5
|
+
ttsapi/streaming.py
|
|
6
|
+
ttsapi_client.egg-info/PKG-INFO
|
|
7
|
+
ttsapi_client.egg-info/SOURCES.txt
|
|
8
|
+
ttsapi_client.egg-info/dependency_links.txt
|
|
9
|
+
ttsapi_client.egg-info/requires.txt
|
|
10
|
+
ttsapi_client.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ttsapi
|