smallestai 3.1.0__py3-none-any.whl → 4.0.1__py3-none-any.whl
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.
- smallestai/__init__.py +35 -45
- smallestai/atoms/__init__.py +249 -123
- smallestai/atoms/api/__init__.py +0 -1
- smallestai/atoms/api/agent_templates_api.py +26 -26
- smallestai/atoms/api/agents_api.py +1316 -190
- smallestai/atoms/api/calls_api.py +29 -29
- smallestai/atoms/api/campaigns_api.py +165 -165
- smallestai/atoms/api/knowledge_base_api.py +290 -290
- smallestai/atoms/api/logs_api.py +13 -13
- smallestai/atoms/api/organization_api.py +13 -13
- smallestai/atoms/api/user_api.py +13 -13
- smallestai/atoms/atoms_client.py +77 -49
- smallestai/atoms/models/__init__.py +103 -43
- smallestai/atoms/models/agent_agent_id_webhook_subscriptions_delete200_response.py +89 -0
- smallestai/atoms/models/{get_agent_templates200_response.py → agent_agent_id_webhook_subscriptions_get200_response.py} +7 -7
- smallestai/atoms/models/agent_agent_id_webhook_subscriptions_get404_response.py +89 -0
- smallestai/atoms/models/agent_agent_id_webhook_subscriptions_post201_response.py +89 -0
- smallestai/atoms/models/agent_agent_id_webhook_subscriptions_post400_response.py +89 -0
- smallestai/atoms/models/agent_agent_id_webhook_subscriptions_post_request.py +97 -0
- smallestai/atoms/models/agent_dto.py +8 -6
- smallestai/atoms/models/agent_dto_language.py +17 -3
- smallestai/atoms/models/agent_dto_language_switching.py +95 -0
- smallestai/atoms/models/agent_dto_synthesizer.py +1 -1
- smallestai/atoms/models/{create_agent_from_template200_response.py → agent_from_template_post200_response.py} +4 -4
- smallestai/atoms/models/{get_agents200_response.py → agent_get200_response.py} +7 -7
- smallestai/atoms/models/{get_agents200_response_data.py → agent_get200_response_data.py} +9 -13
- smallestai/atoms/models/{delete_agent200_response.py → agent_id_delete200_response.py} +4 -4
- smallestai/atoms/models/{get_agent_by_id200_response.py → agent_id_get200_response.py} +4 -4
- smallestai/atoms/models/{update_agent200_response.py → agent_id_patch200_response.py} +4 -4
- smallestai/atoms/models/{update_agent_request.py → agent_id_patch_request.py} +17 -15
- smallestai/atoms/models/{update_agent_request_language.py → agent_id_patch_request_language.py} +14 -10
- smallestai/atoms/models/agent_id_patch_request_language_switching.py +96 -0
- smallestai/atoms/models/{update_agent_request_synthesizer.py → agent_id_patch_request_synthesizer.py} +6 -6
- smallestai/atoms/models/{update_agent_request_synthesizer_voice_config.py → agent_id_patch_request_synthesizer_voice_config.py} +27 -27
- smallestai/atoms/models/{update_agent_request_synthesizer_voice_config_one_of.py → agent_id_patch_request_synthesizer_voice_config_one_of.py} +4 -4
- smallestai/atoms/models/{update_agent_request_synthesizer_voice_config_one_of1.py → agent_id_patch_request_synthesizer_voice_config_one_of1.py} +4 -4
- smallestai/atoms/models/{get_campaign_by_id200_response.py → agent_id_workflow_get200_response.py} +7 -7
- smallestai/atoms/models/agent_id_workflow_get200_response_data.py +105 -0
- smallestai/atoms/models/agent_id_workflow_get200_response_data_edges_inner.py +127 -0
- smallestai/atoms/models/agent_id_workflow_get200_response_data_edges_inner_data.py +91 -0
- smallestai/atoms/models/agent_id_workflow_get200_response_data_edges_inner_marker_end.py +91 -0
- smallestai/atoms/models/agent_id_workflow_get200_response_data_nodes_inner.py +114 -0
- smallestai/atoms/models/agent_id_workflow_get200_response_data_nodes_inner_data.py +115 -0
- smallestai/atoms/models/agent_id_workflow_get200_response_data_nodes_inner_data_variables.py +97 -0
- smallestai/atoms/models/agent_id_workflow_get200_response_data_nodes_inner_data_variables_data_inner.py +91 -0
- smallestai/atoms/models/agent_id_workflow_get200_response_data_nodes_inner_position.py +89 -0
- smallestai/atoms/models/agent_id_workflow_get404_response.py +89 -0
- smallestai/atoms/models/agent_template_get200_response.py +97 -0
- smallestai/atoms/models/{get_agent_templates200_response_data_inner.py → agent_template_get200_response_data_inner.py} +6 -6
- smallestai/atoms/models/{get_campaigns200_response.py → audience_get200_response.py} +7 -7
- smallestai/atoms/models/{create_campaign201_response_data.py → audience_get200_response_data_inner.py} +16 -18
- smallestai/atoms/models/audience_id_delete200_response.py +89 -0
- smallestai/atoms/models/audience_id_delete400_response.py +89 -0
- smallestai/atoms/models/{get_current_user200_response.py → audience_id_get200_response.py} +7 -7
- smallestai/atoms/models/audience_id_get400_response.py +89 -0
- smallestai/atoms/models/audience_id_get403_response.py +89 -0
- smallestai/atoms/models/audience_id_get404_response.py +89 -0
- smallestai/atoms/models/audience_id_members_delete200_response.py +93 -0
- smallestai/atoms/models/audience_id_members_delete200_response_data.py +87 -0
- smallestai/atoms/models/audience_id_members_delete_request.py +87 -0
- smallestai/atoms/models/audience_id_members_get200_response.py +93 -0
- smallestai/atoms/models/audience_id_members_get200_response_data.py +101 -0
- smallestai/atoms/models/{get_campaigns200_response_data_inner_audience.py → audience_id_members_get200_response_data_members_inner.py} +8 -8
- smallestai/atoms/models/audience_id_members_get400_response.py +89 -0
- smallestai/atoms/models/audience_id_members_get500_response.py +89 -0
- smallestai/atoms/models/audience_id_members_post200_response.py +97 -0
- smallestai/atoms/models/audience_id_members_post200_response_data_inner.py +93 -0
- smallestai/atoms/models/audience_id_members_post200_response_data_inner_data.py +89 -0
- smallestai/atoms/models/audience_id_members_post400_response.py +89 -0
- smallestai/atoms/models/audience_id_members_post403_response.py +89 -0
- smallestai/atoms/models/audience_id_members_post_request.py +87 -0
- smallestai/atoms/models/audience_id_members_search_get200_response.py +93 -0
- smallestai/atoms/models/audience_id_members_search_get200_response_data.py +101 -0
- smallestai/atoms/models/audience_id_members_search_get200_response_data_search_info.py +103 -0
- smallestai/atoms/models/audience_id_members_search_get400_response.py +89 -0
- smallestai/atoms/models/audience_id_members_search_get500_response.py +89 -0
- smallestai/atoms/models/{create_campaign201_response.py → audience_post200_response.py} +7 -7
- smallestai/atoms/models/audience_post200_response_data.py +104 -0
- smallestai/atoms/models/audience_post400_response.py +89 -0
- smallestai/atoms/models/campaign_get200_response.py +93 -0
- smallestai/atoms/models/campaign_get200_response_data.py +87 -0
- smallestai/atoms/models/{get_campaigns_request.py → campaign_get_request.py} +4 -4
- smallestai/atoms/models/campaign_id_get200_response.py +93 -0
- smallestai/atoms/models/{get_campaign_by_id200_response_data.py → campaign_id_get200_response_data.py} +4 -4
- smallestai/atoms/models/campaign_post201_response.py +89 -0
- smallestai/atoms/models/{create_campaign_request.py → campaign_post_request.py} +4 -4
- smallestai/atoms/models/{start_outbound_call200_response.py → conversation_id_get200_response.py} +7 -7
- smallestai/atoms/models/{get_conversation_logs200_response_data.py → conversation_id_get200_response_data.py} +4 -4
- smallestai/atoms/models/conversation_outbound_post200_response.py +93 -0
- smallestai/atoms/models/{start_outbound_call200_response_data.py → conversation_outbound_post200_response_data.py} +4 -4
- smallestai/atoms/models/{start_outbound_call_request.py → conversation_outbound_post_request.py} +4 -4
- smallestai/atoms/models/create_agent_request.py +10 -6
- smallestai/atoms/models/create_agent_request_language.py +11 -7
- smallestai/atoms/models/create_agent_request_language_synthesizer_voice_config.py +24 -24
- smallestai/atoms/models/{knowledge_base_dto.py → knowledge_base.py} +15 -8
- smallestai/atoms/models/{knowledge_base_item_dto.py → knowledge_base_item.py} +19 -17
- smallestai/atoms/models/{get_knowledge_bases200_response.py → knowledgebase_get200_response.py} +7 -7
- smallestai/atoms/models/{get_knowledge_base_by_id200_response.py → knowledgebase_id_get200_response.py} +7 -7
- smallestai/atoms/models/{get_knowledge_base_items200_response.py → knowledgebase_id_items_get200_response.py} +7 -7
- smallestai/atoms/models/{upload_text_to_knowledge_base_request.py → knowledgebase_id_items_upload_text_post_request.py} +4 -4
- smallestai/atoms/models/{create_knowledge_base201_response.py → knowledgebase_post201_response.py} +4 -4
- smallestai/atoms/models/{create_knowledge_base_request.py → knowledgebase_post_request.py} +4 -4
- smallestai/atoms/models/{get_organization200_response.py → organization_get200_response.py} +7 -7
- smallestai/atoms/models/{get_organization200_response_data.py → organization_get200_response_data.py} +10 -10
- smallestai/atoms/models/{get_organization200_response_data_members_inner.py → organization_get200_response_data_members_inner.py} +4 -4
- smallestai/atoms/models/{get_organization200_response_data_subscription.py → organization_get200_response_data_subscription.py} +4 -4
- smallestai/atoms/models/product_phone_numbers_get200_response.py +97 -0
- smallestai/atoms/models/product_phone_numbers_get200_response_data_inner.py +100 -0
- smallestai/atoms/models/product_phone_numbers_get200_response_data_inner_attributes.py +89 -0
- smallestai/atoms/models/user_get200_response.py +93 -0
- smallestai/atoms/models/{get_current_user200_response_data.py → user_get200_response_data.py} +4 -4
- smallestai/atoms/models/webhook.py +124 -0
- smallestai/atoms/models/{get_campaigns200_response_data_inner_agent.py → webhook_agent.py} +8 -6
- smallestai/atoms/models/webhook_event.py +98 -0
- smallestai/atoms/models/webhook_get200_response.py +93 -0
- smallestai/atoms/models/webhook_get200_response_data.py +140 -0
- smallestai/atoms/models/webhook_id_delete404_response.py +89 -0
- smallestai/atoms/models/webhook_post201_response.py +89 -0
- smallestai/atoms/models/webhook_post_request.py +99 -0
- smallestai/atoms/models/webhook_post_request_events_inner.py +99 -0
- smallestai/atoms/models/webhook_subscription.py +108 -0
- smallestai/atoms/models/webhook_subscription_populated.py +112 -0
- smallestai/waves/__init__.py +2 -2
- smallestai/waves/async_waves_client.py +42 -69
- smallestai/waves/stream_tts.py +189 -254
- smallestai/waves/utils.py +3 -49
- smallestai/waves/waves_client.py +41 -69
- {smallestai-3.1.0.dist-info → smallestai-4.0.1.dist-info}/METADATA +3 -2
- smallestai-4.0.1.dist-info/RECORD +147 -0
- {smallestai-3.1.0.dist-info → smallestai-4.0.1.dist-info}/WHEEL +1 -1
- smallestai/atoms/models/get_campaigns200_response_data_inner.py +0 -118
- smallestai/atoms/models/get_conversation_logs200_response.py +0 -93
- smallestai-3.1.0.dist-info/RECORD +0 -87
- {smallestai-3.1.0.dist-info → smallestai-4.0.1.dist-info}/licenses/LICENSE +0 -0
- {smallestai-3.1.0.dist-info → smallestai-4.0.1.dist-info}/top_level.txt +0 -0
smallestai/waves/stream_tts.py
CHANGED
|
@@ -1,272 +1,207 @@
|
|
|
1
|
-
import
|
|
1
|
+
import json
|
|
2
|
+
import base64
|
|
2
3
|
import time
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from typing import
|
|
6
|
-
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- Low latency between text generation and speech output.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
tts_instance: The text-to-speech engine to use (Smallest or AsyncSmallest)
|
|
35
|
-
queue_timeout: How long to wait for new text (seconds, default: 1.0)
|
|
36
|
-
max_retries: Number of retry attempts for failed synthesis (default: 3)
|
|
37
|
-
verbose: Whether to log detailed metrics about TTS requests (default: False)
|
|
38
|
-
"""
|
|
39
|
-
self.tts_instance = tts_instance
|
|
40
|
-
self.tts_instance.opts.add_wav_header = False
|
|
41
|
-
self.sentence_end_regex = SENTENCE_END_REGEX
|
|
42
|
-
self.queue_timeout = queue_timeout
|
|
43
|
-
self.max_retries = max_retries
|
|
44
|
-
self.queue = Queue()
|
|
45
|
-
self.buffer_size = 250
|
|
46
|
-
self.stop_flag = False
|
|
47
|
-
self.verbose = verbose
|
|
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
|
|
48
32
|
|
|
49
|
-
|
|
50
|
-
self.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
self.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
async for chunk in llm_output:
|
|
71
|
-
buffer += chunk
|
|
72
|
-
|
|
73
|
-
while len(buffer) > self.buffer_size:
|
|
74
|
-
chunk_text = buffer[:self.buffer_size]
|
|
75
|
-
last_break_index = -1
|
|
76
|
-
|
|
77
|
-
# Find last sentence boundary using regex
|
|
78
|
-
for i in range(len(chunk_text) - 1, -1, -1):
|
|
79
|
-
if self.sentence_end_regex.match(chunk_text[:i + 1]):
|
|
80
|
-
last_break_index = i
|
|
81
|
-
break
|
|
82
|
-
|
|
83
|
-
if last_break_index == -1:
|
|
84
|
-
# Fallback to space if no sentence boundary found
|
|
85
|
-
last_space = chunk_text.rfind(' ')
|
|
86
|
-
if last_space != -1:
|
|
87
|
-
last_break_index = last_space
|
|
88
|
-
else:
|
|
89
|
-
last_break_index = self.buffer_size - 1
|
|
90
|
-
|
|
91
|
-
# Add chunk to queue and update buffer
|
|
92
|
-
self.queue.put(f'{buffer[:last_break_index + 1].replace("—", " ").strip()} ')
|
|
93
|
-
buffer = buffer[last_break_index + 1:].strip()
|
|
94
|
-
|
|
95
|
-
# Don't forget the remaining text
|
|
96
|
-
if buffer:
|
|
97
|
-
self.queue.put(f'{buffer.replace("—", " ").strip()} ')
|
|
98
|
-
|
|
99
|
-
self.stop_flag = True
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _synthesize_sync(self, sentence: str, retries: int = 0) -> Optional[bytes]:
|
|
103
|
-
"""Synchronously synthesizes a given sentence."""
|
|
104
|
-
request_start_time = time.time()
|
|
105
|
-
request_id = self.request_count + 1
|
|
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
|
|
106
53
|
|
|
54
|
+
def _on_message(self, ws, message):
|
|
107
55
|
try:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
request_end_time = time.time()
|
|
56
|
+
data = json.loads(message)
|
|
57
|
+
status = data.get("status", "")
|
|
111
58
|
|
|
112
|
-
if
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
self.first_api_response_time = time.time() - self.start_time
|
|
59
|
+
if status == "error":
|
|
60
|
+
self.error_queue.put(Exception(data.get("message", "Unknown error")))
|
|
61
|
+
return
|
|
116
62
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"text": sentence,
|
|
120
|
-
"start_time": request_start_time - self.start_time,
|
|
121
|
-
"end_time": request_end_time - self.start_time,
|
|
122
|
-
"duration": request_duration,
|
|
123
|
-
"char_count": len(sentence),
|
|
124
|
-
"retries": retries
|
|
125
|
-
})
|
|
63
|
+
if not self.request_id:
|
|
64
|
+
self.request_id = data.get("request_id")
|
|
126
65
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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()
|
|
137
88
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
)
|
|
143
97
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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)
|
|
148
106
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
})
|
|
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()
|
|
163
120
|
|
|
164
|
-
return audio_content
|
|
165
|
-
except APIError as e:
|
|
166
|
-
if retries < self.max_retries:
|
|
167
|
-
if self.verbose:
|
|
168
|
-
print(f"Retry {retries + 1}/{self.max_retries} for request: '{sentence[:30]}...'")
|
|
169
|
-
return await self._synthesize_async(sentence, retries + 1)
|
|
170
|
-
else:
|
|
171
|
-
if self.verbose:
|
|
172
|
-
print(f"Synthesis failed for sentence: {sentence} - Error: {e}. Retries Exhausted, for more information, visit https://waves.smallest.ai/")
|
|
173
|
-
return None
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
async def _run_synthesis(self) -> AsyncGenerator[bytes, None]:
|
|
177
|
-
"""
|
|
178
|
-
Continuously synthesizes sentences from the queue, yielding audio content.
|
|
179
|
-
If no sentences are in the queue, it waits until new data is available or streaming is complete.
|
|
180
|
-
"""
|
|
181
|
-
while not self.stop_flag or not self.queue.empty():
|
|
182
121
|
try:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if isinstance(self.tts_instance, AsyncWavesClient):
|
|
186
|
-
audio_content = await self._synthesize_async(sentence)
|
|
187
|
-
else:
|
|
188
|
-
loop = asyncio.get_running_loop()
|
|
189
|
-
audio_content = await loop.run_in_executor(None, self._synthesize_sync, sentence)
|
|
190
|
-
|
|
191
|
-
if audio_content:
|
|
192
|
-
yield audio_content
|
|
193
|
-
|
|
194
|
-
except Empty:
|
|
195
|
-
# Quick check if we should exit
|
|
196
|
-
if self.stop_flag and self.queue.empty():
|
|
122
|
+
chunk = self.audio_queue.get(timeout=1.0)
|
|
123
|
+
if chunk is None:
|
|
197
124
|
break
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
print(f"TEXT-TO-AUDIO STREAM METRICS")
|
|
212
|
-
print("="*100)
|
|
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()
|
|
213
138
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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)
|
|
218
151
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
print("\n" + header)
|
|
223
|
-
print("-" * 100)
|
|
152
|
+
sender_thread = threading.Thread(target=send_text)
|
|
153
|
+
sender_thread.daemon = True
|
|
154
|
+
sender_thread.start()
|
|
224
155
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
f"{log['id']:4} "
|
|
229
|
-
f"{log['start_time']:10.3f} "
|
|
230
|
-
f"{log['end_time']:10.3f} "
|
|
231
|
-
f"{log['duration']:12.3f} "
|
|
232
|
-
f"{log['char_count']:15} "
|
|
233
|
-
f"{log['text'][:50]}{'...' if len(log['text']) > 50 else ''}"
|
|
234
|
-
)
|
|
235
|
-
print(row)
|
|
236
|
-
|
|
237
|
-
# Print retry information if any
|
|
238
|
-
if log['retries'] > 0:
|
|
239
|
-
print(f"{'':4} {'':10} {'':10} {'':12} {'':15} Retries: {log['retries']}")
|
|
156
|
+
while True:
|
|
157
|
+
if not self.error_queue.empty():
|
|
158
|
+
raise self.error_queue.get()
|
|
240
159
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
llm_thread.join()
|
|
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()
|
|
270
187
|
|
|
271
|
-
|
|
272
|
-
|
|
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
|
smallestai/waves/utils.py
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import io
|
|
3
1
|
from typing import List
|
|
4
2
|
from typing import Optional
|
|
5
|
-
from pydub import AudioSegment
|
|
6
3
|
from dataclasses import dataclass
|
|
7
4
|
|
|
8
5
|
from smallestai.waves.exceptions import ValidationError
|
|
@@ -10,7 +7,7 @@ from smallestai.waves.models import TTSModels, TTSLanguages_lightning, TTSLangua
|
|
|
10
7
|
|
|
11
8
|
|
|
12
9
|
API_BASE_URL = "https://waves-api.smallest.ai/api/v1"
|
|
13
|
-
|
|
10
|
+
WEBSOCKET_URL = "wss://waves-api.smallest.ai/api/v1/lightning-v2/get_speech/stream"
|
|
14
11
|
SAMPLE_WIDTH = 2
|
|
15
12
|
CHANNELS = 1
|
|
16
13
|
ALLOWED_AUDIO_EXTENSIONS = ['.mp3', '.wav']
|
|
@@ -22,11 +19,12 @@ class TTSOptions:
|
|
|
22
19
|
sample_rate: int
|
|
23
20
|
voice_id: str
|
|
24
21
|
api_key: str
|
|
25
|
-
add_wav_header: bool
|
|
26
22
|
speed: float
|
|
27
23
|
consistency: float
|
|
28
24
|
similarity: float
|
|
29
25
|
enhancement: int
|
|
26
|
+
language: str
|
|
27
|
+
output_format: str
|
|
30
28
|
|
|
31
29
|
|
|
32
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):
|
|
@@ -46,50 +44,6 @@ def validate_input(text: str, model: str, sample_rate: int, speed: float, consis
|
|
|
46
44
|
raise ValidationError(f"Invalid enhancement: {enhancement}. Must be between 0 and 2.")
|
|
47
45
|
|
|
48
46
|
|
|
49
|
-
def add_wav_header(frame_input: bytes, sample_rate: int = 24000, sample_width: int = 2, channels: int = 1) -> bytes:
|
|
50
|
-
audio = AudioSegment(data=frame_input, sample_width=sample_width, frame_rate=sample_rate, channels=channels)
|
|
51
|
-
wav_buf = io.BytesIO()
|
|
52
|
-
audio.export(wav_buf, format="wav")
|
|
53
|
-
wav_buf.seek(0)
|
|
54
|
-
return wav_buf.read()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def preprocess_text(text: str) -> str:
|
|
58
|
-
text = text.replace("\n", " ").replace("\t", " ")
|
|
59
|
-
text = re.sub(r'\s+', ' ', text)
|
|
60
|
-
return text.strip()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def chunk_text(text: str, chunk_size: int = 250) -> List[str]:
|
|
64
|
-
chunks = []
|
|
65
|
-
while text:
|
|
66
|
-
if len(text) <= chunk_size:
|
|
67
|
-
chunks.append(text.strip())
|
|
68
|
-
break
|
|
69
|
-
|
|
70
|
-
chunk_text = text[:chunk_size]
|
|
71
|
-
last_break_index = -1
|
|
72
|
-
|
|
73
|
-
# Find last sentence boundary using regex
|
|
74
|
-
for i in range(len(chunk_text) - 1, -1, -1):
|
|
75
|
-
if SENTENCE_END_REGEX.match(chunk_text[:i + 1]):
|
|
76
|
-
last_break_index = i
|
|
77
|
-
break
|
|
78
|
-
|
|
79
|
-
if last_break_index == -1:
|
|
80
|
-
# Fallback to space if no sentence boundary found
|
|
81
|
-
last_space = chunk_text.rfind(' ')
|
|
82
|
-
if last_space != -1:
|
|
83
|
-
last_break_index = last_space
|
|
84
|
-
else:
|
|
85
|
-
last_break_index = chunk_size - 1
|
|
86
|
-
|
|
87
|
-
chunks.append(text[:last_break_index + 1].strip())
|
|
88
|
-
text = text[last_break_index + 1:].strip()
|
|
89
|
-
|
|
90
|
-
return chunks
|
|
91
|
-
|
|
92
|
-
|
|
93
47
|
def get_smallest_languages(model: str = 'lightning') -> List[str]:
|
|
94
48
|
if model == 'lightning':
|
|
95
49
|
return TTSLanguages_lightning
|