smallestai 3.0.3__tar.gz → 4.0.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.

Potentially problematic release.


This version of smallestai might be problematic. Click here for more details.

Files changed (96) hide show
  1. {smallestai-3.0.3/smallestai.egg-info → smallestai-4.0.0}/PKG-INFO +2 -1
  2. {smallestai-3.0.3 → smallestai-4.0.0}/pyproject.toml +2 -1
  3. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/__init__.py +1 -1
  4. smallestai-4.0.0/smallestai/waves/__init__.py +5 -0
  5. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/waves/async_waves_client.py +44 -71
  6. smallestai-4.0.0/smallestai/waves/models.py +8 -0
  7. smallestai-4.0.0/smallestai/waves/stream_tts.py +207 -0
  8. smallestai-4.0.0/smallestai/waves/utils.py +58 -0
  9. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/waves/waves_client.py +43 -71
  10. {smallestai-3.0.3 → smallestai-4.0.0/smallestai.egg-info}/PKG-INFO +2 -1
  11. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai.egg-info/requires.txt +1 -0
  12. smallestai-3.0.3/smallestai/waves/__init__.py +0 -5
  13. smallestai-3.0.3/smallestai/waves/models.py +0 -5
  14. smallestai-3.0.3/smallestai/waves/stream_tts.py +0 -272
  15. smallestai-3.0.3/smallestai/waves/utils.py +0 -97
  16. {smallestai-3.0.3 → smallestai-4.0.0}/LICENSE +0 -0
  17. {smallestai-3.0.3 → smallestai-4.0.0}/README.md +0 -0
  18. {smallestai-3.0.3 → smallestai-4.0.0}/setup.cfg +0 -0
  19. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/__init__.py +0 -0
  20. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/api/__init__.py +0 -0
  21. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/api/agent_templates_api.py +0 -0
  22. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/api/agents_api.py +0 -0
  23. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/api/calls_api.py +0 -0
  24. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/api/campaigns_api.py +0 -0
  25. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/api/knowledge_base_api.py +0 -0
  26. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/api/logs_api.py +0 -0
  27. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/api/organization_api.py +0 -0
  28. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/api/user_api.py +0 -0
  29. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/api_client.py +0 -0
  30. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/api_response.py +0 -0
  31. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/atoms_client.py +0 -0
  32. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/configuration.py +0 -0
  33. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/exceptions.py +0 -0
  34. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/__init__.py +0 -0
  35. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/agent_dto.py +0 -0
  36. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/agent_dto_language.py +0 -0
  37. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/agent_dto_synthesizer.py +0 -0
  38. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/agent_dto_synthesizer_voice_config.py +0 -0
  39. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/api_response.py +0 -0
  40. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/bad_request_error_response.py +0 -0
  41. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/create_agent_from_template200_response.py +0 -0
  42. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/create_agent_from_template_request.py +0 -0
  43. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/create_agent_request.py +0 -0
  44. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/create_agent_request_language.py +0 -0
  45. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/create_agent_request_language_synthesizer.py +0 -0
  46. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/create_agent_request_language_synthesizer_voice_config.py +0 -0
  47. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/create_campaign201_response.py +0 -0
  48. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/create_campaign201_response_data.py +0 -0
  49. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/create_campaign_request.py +0 -0
  50. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/create_knowledge_base201_response.py +0 -0
  51. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/create_knowledge_base_request.py +0 -0
  52. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/delete_agent200_response.py +0 -0
  53. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_agent_by_id200_response.py +0 -0
  54. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_agent_templates200_response.py +0 -0
  55. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_agent_templates200_response_data_inner.py +0 -0
  56. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_agents200_response.py +0 -0
  57. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_agents200_response_data.py +0 -0
  58. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_campaign_by_id200_response.py +0 -0
  59. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_campaign_by_id200_response_data.py +0 -0
  60. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_campaigns200_response.py +0 -0
  61. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_campaigns200_response_data_inner.py +0 -0
  62. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_campaigns200_response_data_inner_agent.py +0 -0
  63. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_campaigns200_response_data_inner_audience.py +0 -0
  64. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_campaigns_request.py +0 -0
  65. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_conversation_logs200_response.py +0 -0
  66. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_conversation_logs200_response_data.py +0 -0
  67. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_current_user200_response.py +0 -0
  68. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_current_user200_response_data.py +0 -0
  69. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_knowledge_base_by_id200_response.py +0 -0
  70. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_knowledge_base_items200_response.py +0 -0
  71. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_knowledge_bases200_response.py +0 -0
  72. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_organization200_response.py +0 -0
  73. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_organization200_response_data.py +0 -0
  74. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_organization200_response_data_members_inner.py +0 -0
  75. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/get_organization200_response_data_subscription.py +0 -0
  76. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/internal_server_error_response.py +0 -0
  77. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/knowledge_base_dto.py +0 -0
  78. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/knowledge_base_item_dto.py +0 -0
  79. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/start_outbound_call200_response.py +0 -0
  80. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/start_outbound_call200_response_data.py +0 -0
  81. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/start_outbound_call_request.py +0 -0
  82. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/unauthorized_error_reponse.py +0 -0
  83. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/update_agent200_response.py +0 -0
  84. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/update_agent_request.py +0 -0
  85. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/update_agent_request_language.py +0 -0
  86. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/update_agent_request_synthesizer.py +0 -0
  87. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/update_agent_request_synthesizer_voice_config.py +0 -0
  88. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/update_agent_request_synthesizer_voice_config_one_of.py +0 -0
  89. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/update_agent_request_synthesizer_voice_config_one_of1.py +0 -0
  90. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/models/upload_text_to_knowledge_base_request.py +0 -0
  91. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/py.typed +0 -0
  92. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/atoms/rest.py +0 -0
  93. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai/waves/exceptions.py +0 -0
  94. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai.egg-info/SOURCES.txt +0 -0
  95. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai.egg-info/dependency_links.txt +0 -0
  96. {smallestai-3.0.3 → smallestai-4.0.0}/smallestai.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smallestai
3
- Version: 3.0.3
3
+ Version: 4.0.0
4
4
  Summary: Official Python client for the Smallest AI API
5
5
  Author-email: Smallest <support@smallest.ai>
6
6
  License: MIT
@@ -16,6 +16,7 @@ Requires-Dist: aiohttp
16
16
  Requires-Dist: aiofiles
17
17
  Requires-Dist: requests
18
18
  Requires-Dist: pydub
19
+ Requires-Dist: websocket-client
19
20
  Requires-Dist: urllib3<3.0.0,>=1.25.3
20
21
  Requires-Dist: python-dateutil>=2.8.2
21
22
  Requires-Dist: pydantic>=2
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "smallestai"
3
- version = "3.0.3"
3
+ version = "4.0.0"
4
4
  description = "Official Python client for the Smallest AI API"
5
5
  authors = [
6
6
  {name = "Smallest", email = "support@smallest.ai"},
@@ -19,6 +19,7 @@ dependencies = [
19
19
  "aiofiles",
20
20
  "requests",
21
21
  "pydub",
22
+ "websocket-client",
22
23
  "urllib3 >= 1.25.3, < 3.0.0",
23
24
  "python-dateutil >= 2.8.2",
24
25
  "pydantic >= 2",
@@ -84,7 +84,7 @@ from smallestai.atoms import (
84
84
  from smallestai.waves import (
85
85
  WavesClient,
86
86
  AsyncWavesClient,
87
- TextToAudioStream
87
+ WavesStreamingTTS
88
88
  )
89
89
 
90
90
  from smallestai.atoms import __all__ as atoms_all
@@ -0,0 +1,5 @@
1
+ from smallestai.waves.waves_client import WavesClient
2
+ from smallestai.waves.async_waves_client import AsyncWavesClient
3
+ from smallestai.waves.stream_tts import WavesStreamingTTS, TTSConfig
4
+
5
+ __all__ = ["WavesClient", "AsyncWavesClient", "WavesStreamingTTS", "TTSConfig"]
@@ -4,10 +4,10 @@ import json
4
4
  import aiohttp
5
5
  import aiofiles
6
6
  import requests
7
- from typing import Optional, Union, List, AsyncIterator
7
+ from typing import Optional, Union, List
8
8
 
9
9
  from smallestai.waves.exceptions import TTSError, APIError
10
- from smallestai.waves.utils import (TTSOptions, validate_input, preprocess_text, add_wav_header, chunk_text,
10
+ from smallestai.waves.utils import (TTSOptions, validate_input,
11
11
  get_smallest_languages, get_smallest_models, ALLOWED_AUDIO_EXTENSIONS, API_BASE_URL)
12
12
 
13
13
 
@@ -22,7 +22,8 @@ class AsyncWavesClient:
22
22
  consistency: Optional[float] = 0.5,
23
23
  similarity: Optional[float] = 0.0,
24
24
  enhancement: Optional[int] = 1,
25
- add_wav_header: Optional[bool] = True
25
+ language: Optional[str] = "en",
26
+ output_format: Optional[str] = "wav"
26
27
  ) -> None:
27
28
  """
28
29
  AsyncSmallest Instance for asynchronous text-to-speech synthesis.
@@ -40,7 +41,8 @@ class AsyncWavesClient:
40
41
  - consistency (float): This parameter controls word repetition and skipping. Decrease it to prevent skipped words, and increase it to prevent repetition. Only supported in `lightning-large` model. Range - [0, 1]
41
42
  - similarity (float): This parameter controls the similarity between the synthesized audio and the reference audio. Increase it to make the speech more similar to the reference audio. Only supported in `lightning-large` model. Range - [0, 1]
42
43
  - enhancement (int): Enhances speech quality at the cost of increased latency. Only supported in `lightning-large` model. Range - [0, 2].
43
- - add_wav_header (bool): Whether to add a WAV header to the output audio.
44
+ - language (str): The language for synthesis. Default is "en".
45
+ - output_format (str): The output audio format. Options: "pcm", "mp3", "wav", "mulaw". Default is "pcm".
44
46
 
45
47
  Methods:
46
48
  - get_languages: Returns a list of available languages for synthesis.
@@ -61,11 +63,12 @@ class AsyncWavesClient:
61
63
  sample_rate=sample_rate,
62
64
  voice_id=voice_id,
63
65
  api_key=self.api_key,
64
- add_wav_header=add_wav_header,
65
66
  speed=speed,
66
67
  consistency=consistency,
67
68
  similarity=similarity,
68
- enhancement=enhancement
69
+ enhancement=enhancement,
70
+ language=language,
71
+ output_format=output_format
69
72
  )
70
73
  self.session = None
71
74
 
@@ -89,9 +92,9 @@ class AsyncWavesClient:
89
92
  return False
90
93
 
91
94
 
92
- def get_languages(self) -> List[str]:
95
+ def get_languages(self, model="lightning") -> List[str]:
93
96
  """Returns a list of available languages."""
94
- return get_smallest_languages()
97
+ return get_smallest_languages(model)
95
98
 
96
99
  def get_cloned_voices(self) -> str:
97
100
  """Returns a list of your cloned voices."""
@@ -130,18 +133,14 @@ class AsyncWavesClient:
130
133
  async def synthesize(
131
134
  self,
132
135
  text: str,
133
- stream: Optional[bool] = False,
134
- save_as: Optional[str] = None,
135
136
  **kwargs
136
- ) -> Union[bytes, None, AsyncIterator[bytes]]:
137
+ ) -> Union[bytes]:
137
138
  """
138
139
  Asynchronously synthesize speech from the provided text.
139
140
 
140
141
  Args:
141
142
  - text (str): The text to be converted to speech.
142
143
  - stream (Optional[bool]): If True, returns an iterator yielding audio chunks instead of a full byte array.
143
- - save_as (Optional[str]): If provided, the synthesized audio will be saved to this file path.
144
- The file must have a .wav extension.
145
144
  - kwargs: Additional optional parameters to override `__init__` options for this call.
146
145
 
147
146
  Returns:
@@ -151,7 +150,7 @@ class AsyncWavesClient:
151
150
  - Otherwise, returns the synthesized audio content as bytes.
152
151
 
153
152
  Raises:
154
- - TTSError: If the provided file name does not have a .wav extension when `save_as` is specified.
153
+ - TTSError: If the provided file name does not have a .wav or .mp3 extension when `save_as` is specified.
155
154
  - APIError: If the API request fails or returns an error.
156
155
  - ValueError: If an unexpected parameter is passed in `kwargs`.
157
156
  """
@@ -172,65 +171,40 @@ class AsyncWavesClient:
172
171
  for key, value in kwargs.items():
173
172
  setattr(opts, key, value)
174
173
 
175
- text = preprocess_text(text)
176
174
  validate_input(text, opts.model, opts.sample_rate, opts.speed, opts.consistency, opts.similarity, opts.enhancement)
177
175
 
178
- self.chunk_size = 250
179
- if opts.model == 'lightning-large':
180
- self.chunk_size = 140
181
-
182
- chunks = chunk_text(text, self.chunk_size)
183
-
184
- async def audio_stream():
185
- for chunk in chunks:
186
- payload = {
187
- "text": chunk,
188
- "sample_rate": opts.sample_rate,
189
- "voice_id": opts.voice_id,
190
- "add_wav_header": False,
191
- "speed": opts.speed,
192
- "model": opts.model
193
- }
194
-
195
- if opts.model == "lightning-large":
196
- if opts.consistency is not None:
197
- payload["consistency"] = opts.consistency
198
- if opts.similarity is not None:
199
- payload["similarity"] = opts.similarity
200
- if opts.enhancement is not None:
201
- payload["enhancement"] = opts.enhancement
202
-
203
-
204
- headers = {
205
- "Authorization": f"Bearer {self.api_key}",
206
- "Content-Type": "application/json",
207
- }
208
-
209
- async with self.session.post(f"{API_BASE_URL}/{opts.model}/get_speech", json=payload, headers=headers) as res:
210
- if res.status != 200:
211
- raise APIError(f"Failed to synthesize speech: {await res.text()}. For more information, visit https://waves.smallest.ai/")
212
-
213
- yield await res.read()
176
+ payload = {
177
+ "text": text,
178
+ "voice_id": opts.voice_id,
179
+ "sample_rate": opts.sample_rate,
180
+ "speed": opts.speed,
181
+ "consistency": opts.consistency,
182
+ "similarity": opts.similarity,
183
+ "enhancement": opts.enhancement,
184
+ "language": opts.language,
185
+ "output_format": opts.output_format
186
+ }
214
187
 
215
- if stream:
216
- return audio_stream()
217
-
218
- audio_content = b"".join([chunk async for chunk in audio_stream()])
219
-
220
- if save_as:
221
- if not save_as.endswith(".wav"):
222
- raise TTSError("Invalid file name. Extension must be .wav")
223
-
224
- async with aiofiles.open(save_as, mode='wb') as f:
225
- await f.write(add_wav_header(audio_content, opts.sample_rate))
226
-
227
- return None
228
-
229
- if opts.add_wav_header:
230
- return add_wav_header(audio_content, opts.sample_rate)
188
+ if opts.model == "lightning-large" or opts.model == "lightning-v2":
189
+ if opts.consistency is not None:
190
+ payload["consistency"] = opts.consistency
191
+ if opts.similarity is not None:
192
+ payload["similarity"] = opts.similarity
193
+ if opts.enhancement is not None:
194
+ payload["enhancement"] = opts.enhancement
195
+
196
+ headers = {
197
+ "Authorization": f"Bearer {self.api_key}",
198
+ "Content-Type": "application/json",
199
+ }
231
200
 
232
- return audio_content
201
+ async with self.session.post(f"{API_BASE_URL}/{opts.model}/get_speech", json=payload, headers=headers) as res:
202
+ if res.status != 200:
203
+ raise APIError(f"Failed to synthesize speech: {await res.text()}. For more information, visit https://waves.smallest.ai/")
204
+
205
+ audio_bytes = await res.content.read()
233
206
 
207
+ return audio_bytes
234
208
  finally:
235
209
  if should_cleanup and self.session:
236
210
  await self.session.close()
@@ -316,9 +290,8 @@ class AsyncWavesClient:
316
290
  if res.status != 200:
317
291
  raise APIError(f"Failed to delete voice: {await res.text()}. For more information, visit https://waves.smallest.ai/")
318
292
 
319
- return await res.text()
320
-
293
+ return json.dumps(await res.json(), indent=4, ensure_ascii=False)
321
294
  finally:
322
295
  if should_cleanup and self.session:
323
296
  await self.session.close()
324
- self.session = None
297
+ self.session = None
@@ -0,0 +1,8 @@
1
+ TTSLanguages_lightning = ["en", "hi"]
2
+ TTSLanguages_lightning_large = ["en", "hi"]
3
+ TTSLanguages_lightning_v2 = ["en", "hi", "mr", "kn", "ta", "bn", "gu", "de", "fr", "es", "it", "pl", "nl", "ru", "ar", "he"]
4
+ TTSModels = [
5
+ "lightning",
6
+ "lightning-large",
7
+ "lightning-v2"
8
+ ]
@@ -0,0 +1,207 @@
1
+ import json
2
+ import base64
3
+ import time
4
+ import threading
5
+ import queue
6
+ from typing import Generator
7
+ from dataclasses import dataclass
8
+ from websocket import WebSocketApp
9
+
10
+ @dataclass
11
+ class TTSConfig:
12
+ voice_id: str
13
+ api_key: str
14
+ language: str = "en"
15
+ sample_rate: int = 24000
16
+ speed: float = 1.0
17
+ consistency: float = 0.5
18
+ enhancement: int = 1
19
+ similarity: float = 0
20
+ max_buffer_flush_ms: int = 0
21
+
22
+ class WavesStreamingTTS:
23
+ def __init__(self, config: TTSConfig):
24
+ self.config = config
25
+ self.ws_url = "wss://waves-api.smallest.ai/api/v1/lightning-v2/get_speech/stream"
26
+ self.ws = None
27
+ self.audio_queue = queue.Queue()
28
+ self.error_queue = queue.Queue()
29
+ self.is_complete = False
30
+ self.is_connected = False
31
+ self.request_id = None
32
+
33
+ def _get_headers(self):
34
+ return [f"Authorization: Bearer {self.config.api_key}"]
35
+
36
+ def _create_payload(self, text: str, continue_stream: bool = False, flush: bool = False):
37
+ return {
38
+ "voice_id": self.config.voice_id,
39
+ "text": text,
40
+ "language": self.config.language,
41
+ "sample_rate": self.config.sample_rate,
42
+ "speed": self.config.speed,
43
+ "consistency": self.config.consistency,
44
+ "similarity": self.config.similarity,
45
+ "enhancement": self.config.enhancement,
46
+ "max_buffer_flush_ms": self.config.max_buffer_flush_ms,
47
+ "continue": continue_stream,
48
+ "flush": flush
49
+ }
50
+
51
+ def _on_open(self, ws):
52
+ self.is_connected = True
53
+
54
+ def _on_message(self, ws, message):
55
+ try:
56
+ data = json.loads(message)
57
+ status = data.get("status", "")
58
+
59
+ if status == "error":
60
+ self.error_queue.put(Exception(data.get("message", "Unknown error")))
61
+ return
62
+
63
+ if not self.request_id:
64
+ self.request_id = data.get("request_id")
65
+
66
+ audio_b64 = data.get("data", {}).get("audio")
67
+ if audio_b64:
68
+ self.audio_queue.put(base64.b64decode(audio_b64))
69
+
70
+ if status == "complete":
71
+ self.is_complete = True
72
+ self.audio_queue.put(None)
73
+
74
+ except Exception as e:
75
+ self.error_queue.put(e)
76
+
77
+ def _on_error(self, ws, error):
78
+ self.error_queue.put(error)
79
+
80
+ def _on_close(self, ws, *args):
81
+ self.is_connected = False
82
+ if not self.is_complete:
83
+ self.audio_queue.put(None)
84
+
85
+ def _connect(self):
86
+ if self.ws:
87
+ self.ws.close()
88
+
89
+ self.ws = WebSocketApp(
90
+ self.ws_url,
91
+ header=self._get_headers(),
92
+ on_open=self._on_open,
93
+ on_message=self._on_message,
94
+ on_error=self._on_error,
95
+ on_close=self._on_close
96
+ )
97
+
98
+ ws_thread = threading.Thread(target=self.ws.run_forever)
99
+ ws_thread.daemon = True
100
+ ws_thread.start()
101
+
102
+ timeout = 5.0
103
+ start_time = time.time()
104
+ while not self.is_connected and time.time() - start_time < timeout:
105
+ time.sleep(0.1)
106
+
107
+ if not self.is_connected:
108
+ raise Exception("Failed to connect to WebSocket")
109
+
110
+ def synthesize(self, text: str) -> Generator[bytes, None, None]:
111
+ self._reset_state()
112
+ self._connect()
113
+
114
+ payload = self._create_payload(text)
115
+ self.ws.send(json.dumps(payload))
116
+
117
+ while True:
118
+ if not self.error_queue.empty():
119
+ raise self.error_queue.get()
120
+
121
+ try:
122
+ chunk = self.audio_queue.get(timeout=1.0)
123
+ if chunk is None:
124
+ break
125
+ yield chunk
126
+ except queue.Empty:
127
+ if self.is_complete:
128
+ break
129
+ continue
130
+
131
+ self.ws.close()
132
+
133
+ def synthesize_streaming(self, text_stream: Generator[str, None, None],
134
+ continue_stream: bool = True,
135
+ auto_flush: bool = True) -> Generator[bytes, None, None]:
136
+ self._reset_state()
137
+ self._connect()
138
+
139
+ def send_text():
140
+ try:
141
+ for text_chunk in text_stream:
142
+ if text_chunk.strip():
143
+ payload = self._create_payload(text_chunk, continue_stream=continue_stream)
144
+ self.ws.send(json.dumps(payload))
145
+
146
+ if auto_flush:
147
+ flush_payload = self._create_payload("", flush=True)
148
+ self.ws.send(json.dumps(flush_payload))
149
+ except Exception as e:
150
+ self.error_queue.put(e)
151
+
152
+ sender_thread = threading.Thread(target=send_text)
153
+ sender_thread.daemon = True
154
+ sender_thread.start()
155
+
156
+ while True:
157
+ if not self.error_queue.empty():
158
+ raise self.error_queue.get()
159
+
160
+ try:
161
+ chunk = self.audio_queue.get(timeout=1.0)
162
+ if chunk is None:
163
+ break
164
+ yield chunk
165
+ except queue.Empty:
166
+ if self.is_complete:
167
+ break
168
+ continue
169
+
170
+ self.ws.close()
171
+
172
+ def send_text_chunk(self, text: str, continue_stream: bool = True, flush: bool = False):
173
+ if not self.is_connected:
174
+ raise Exception("WebSocket not connected")
175
+ payload = self._create_payload(text, continue_stream=continue_stream, flush=flush)
176
+ self.ws.send(json.dumps(payload))
177
+
178
+ def flush_buffer(self):
179
+ if not self.is_connected:
180
+ raise Exception("WebSocket not connected")
181
+ payload = self._create_payload("", flush=True)
182
+ self.ws.send(json.dumps(payload))
183
+
184
+ def start_streaming_session(self) -> Generator[bytes, None, None]:
185
+ self._reset_state()
186
+ self._connect()
187
+
188
+ while True:
189
+ if not self.error_queue.empty():
190
+ raise self.error_queue.get()
191
+
192
+ try:
193
+ chunk = self.audio_queue.get(timeout=0.1)
194
+ if chunk is None:
195
+ break
196
+ yield chunk
197
+ except queue.Empty:
198
+ if self.is_complete:
199
+ break
200
+ continue
201
+
202
+ def _reset_state(self):
203
+ self.audio_queue = queue.Queue()
204
+ self.error_queue = queue.Queue()
205
+ self.is_complete = False
206
+ self.is_connected = False
207
+ self.request_id = None
@@ -0,0 +1,58 @@
1
+ from typing import List
2
+ from typing import Optional
3
+ from dataclasses import dataclass
4
+
5
+ from smallestai.waves.exceptions import ValidationError
6
+ from smallestai.waves.models import TTSModels, TTSLanguages_lightning, TTSLanguages_lightning_large, TTSLanguages_lightning_v2
7
+
8
+
9
+ API_BASE_URL = "https://waves-api.smallest.ai/api/v1"
10
+ WEBSOCKET_URL = "wss://waves-api.smallest.ai/api/v1/lightning-v2/get_speech/stream"
11
+ SAMPLE_WIDTH = 2
12
+ CHANNELS = 1
13
+ ALLOWED_AUDIO_EXTENSIONS = ['.mp3', '.wav']
14
+
15
+
16
+ @dataclass
17
+ class TTSOptions:
18
+ model: str
19
+ sample_rate: int
20
+ voice_id: str
21
+ api_key: str
22
+ speed: float
23
+ consistency: float
24
+ similarity: float
25
+ enhancement: int
26
+ language: str
27
+ output_format: str
28
+
29
+
30
+ def validate_input(text: str, model: str, sample_rate: int, speed: float, consistency: Optional[float] = None, similarity: Optional[float] = None, enhancement: Optional[int] = None):
31
+ if not text:
32
+ raise ValidationError("Text cannot be empty.")
33
+ if model not in TTSModels:
34
+ raise ValidationError(f"Invalid model: {model}. Must be one of {TTSModels}")
35
+ if not 8000 <= sample_rate <= 24000:
36
+ raise ValidationError(f"Invalid sample rate: {sample_rate}. Must be between 8000 and 24000")
37
+ if not 0.5 <= speed <= 2.0:
38
+ raise ValidationError(f"Invalid speed: {speed}. Must be between 0.5 and 2.0")
39
+ if consistency is not None and not 0.0 <= consistency <= 1.0:
40
+ raise ValidationError(f"Invalid consistency: {consistency}. Must be between 0.0 and 1.0")
41
+ if similarity is not None and not 0.0 <= similarity <= 1.0:
42
+ raise ValidationError(f"Invalid similarity: {similarity}. Must be between 0.0 and 1.0")
43
+ if enhancement is not None and not 0 <= enhancement <= 2:
44
+ raise ValidationError(f"Invalid enhancement: {enhancement}. Must be between 0 and 2.")
45
+
46
+
47
+ def get_smallest_languages(model: str = 'lightning') -> List[str]:
48
+ if model == 'lightning':
49
+ return TTSLanguages_lightning
50
+ elif model == 'lightning-large':
51
+ return TTSLanguages_lightning_large
52
+ elif model == 'lightning-v2':
53
+ return TTSLanguages_lightning_v2
54
+ else:
55
+ raise ValidationError(f"Invalid model: {model}. Must be one of {TTSModels}")
56
+
57
+ def get_smallest_models() -> List[str]:
58
+ return TTSModels