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