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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,4 @@
1
+ httpx>=0.24
2
+
3
+ [streaming]
4
+ websockets>=12.0