sunholo 0.105.7__py3-none-any.whl → 0.106.0__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.
- sunholo/__init__.py +2 -0
- sunholo/cli/cli.py +3 -0
- sunholo/cli/cli_init.py +16 -17
- sunholo/genai/process_funcs_cls.py +2 -0
- sunholo/senses/__init__.py +1 -0
- sunholo/senses/stream_voice.py +414 -0
- sunholo/terraform/tfvars_editor.py +5 -5
- {sunholo-0.105.7.dist-info → sunholo-0.106.0.dist-info}/METADATA +10 -2
- {sunholo-0.105.7.dist-info → sunholo-0.106.0.dist-info}/RECORD +13 -11
- {sunholo-0.105.7.dist-info → sunholo-0.106.0.dist-info}/LICENSE.txt +0 -0
- {sunholo-0.105.7.dist-info → sunholo-0.106.0.dist-info}/WHEEL +0 -0
- {sunholo-0.105.7.dist-info → sunholo-0.106.0.dist-info}/entry_points.txt +0 -0
- {sunholo-0.105.7.dist-info → sunholo-0.106.0.dist-info}/top_level.txt +0 -0
sunholo/__init__.py
CHANGED
|
@@ -18,6 +18,7 @@ from . import lookup
|
|
|
18
18
|
from . import patches
|
|
19
19
|
from . import pubsub
|
|
20
20
|
from . import qna
|
|
21
|
+
from . import senses
|
|
21
22
|
from . import streaming
|
|
22
23
|
from . import terraform
|
|
23
24
|
from . import tools
|
|
@@ -46,6 +47,7 @@ __all__ = ['agents',
|
|
|
46
47
|
'patches',
|
|
47
48
|
'pubsub',
|
|
48
49
|
'qna',
|
|
50
|
+
'senses',
|
|
49
51
|
'streaming',
|
|
50
52
|
'terraform',
|
|
51
53
|
'tools',
|
sunholo/cli/cli.py
CHANGED
|
@@ -13,6 +13,7 @@ from .vertex import setup_vertex_subparser
|
|
|
13
13
|
from ..llamaindex import setup_llamaindex_subparser
|
|
14
14
|
from ..excel import setup_excel_subparser
|
|
15
15
|
from ..terraform import setup_tfvarseditor_subparser
|
|
16
|
+
from ..senses.stream_voice import setup_tts_subparser
|
|
16
17
|
|
|
17
18
|
from ..utils import ConfigManager
|
|
18
19
|
from ..utils.version import sunholo_version
|
|
@@ -98,6 +99,8 @@ def main(args=None):
|
|
|
98
99
|
setup_excel_subparser(subparsers)
|
|
99
100
|
# terraform
|
|
100
101
|
setup_tfvarseditor_subparser(subparsers)
|
|
102
|
+
# tts
|
|
103
|
+
setup_tts_subparser(subparsers)
|
|
101
104
|
|
|
102
105
|
#TODO: add database setup commands: alloydb and supabase
|
|
103
106
|
|
sunholo/cli/cli_init.py
CHANGED
|
@@ -39,27 +39,26 @@ This will create a new directory named `my_genai_project` with the template file
|
|
|
39
39
|
|
|
40
40
|
# Create project directory
|
|
41
41
|
if os.path.exists(project_dir):
|
|
42
|
-
console.print(f"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
console.print(f"Directory {project_dir} already exists. Skipping template creation. If you wish to init a new project, please choose a different project name.")
|
|
43
|
+
else:
|
|
44
|
+
console.print(f"Directory {project_dir} not found. Copying over template files.")
|
|
45
|
+
os.makedirs(project_dir)
|
|
46
|
+
|
|
47
|
+
# Copy template files
|
|
48
|
+
template_dir = get_module_filepath("templates/project")
|
|
49
|
+
for filename in os.listdir(template_dir):
|
|
50
|
+
src_path = os.path.join(template_dir, filename)
|
|
51
|
+
dest_path = os.path.join(project_dir, filename)
|
|
52
|
+
if os.path.isfile(src_path):
|
|
53
|
+
shutil.copy(src_path, dest_path)
|
|
54
|
+
elif os.path.isdir(src_path):
|
|
55
|
+
shutil.copytree(src_path, dest_path)
|
|
58
56
|
|
|
59
57
|
# Determine the location of the generated.tfvars file
|
|
60
58
|
terraform_dir = args.terraform_dir or os.getenv('MULTIVAC_TERRAFORM_DIR')
|
|
61
59
|
if terraform_dir is None:
|
|
62
|
-
|
|
60
|
+
console.print("[SKIP] To auto-generate terraform code, must specify a --terraform_dir or use the MULTIVAC_TERRAFORM_DIR environment variable")
|
|
61
|
+
return
|
|
63
62
|
|
|
64
63
|
tfvars_file = os.path.join(terraform_dir, 'generated.tfvars')
|
|
65
64
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .stream_voice import StreamingTTS
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from google.cloud import texttospeech
|
|
3
|
+
except ImportError:
|
|
4
|
+
texttospeech = None
|
|
5
|
+
try:
|
|
6
|
+
import sounddevice as sd
|
|
7
|
+
except ImportError:
|
|
8
|
+
sd = None
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import numpy as np
|
|
12
|
+
except ImportError:
|
|
13
|
+
np = None
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from rich import console
|
|
17
|
+
console = console.Console()
|
|
18
|
+
except ImportError:
|
|
19
|
+
console = None
|
|
20
|
+
|
|
21
|
+
from ..custom_logging import log
|
|
22
|
+
import queue
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
from typing import Optional
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
class StreamingTTS:
|
|
34
|
+
"""
|
|
35
|
+
# Example usage
|
|
36
|
+
def sample_text_stream():
|
|
37
|
+
sentences = [
|
|
38
|
+
"Hello, this is a test of streaming text to speech.",
|
|
39
|
+
"Each sentence will be converted to audio separately.",
|
|
40
|
+
"This allows for lower latency in long-form text to speech conversion."
|
|
41
|
+
]
|
|
42
|
+
for sentence in sentences:
|
|
43
|
+
yield sentence
|
|
44
|
+
time.sleep(0.5) # Simulate delay between text chunks
|
|
45
|
+
|
|
46
|
+
# Initialize and run
|
|
47
|
+
tts = StreamingTTS()
|
|
48
|
+
tts.process_text_stream(sample_text_stream())
|
|
49
|
+
"""
|
|
50
|
+
def __init__(self):
|
|
51
|
+
if texttospeech is None or sd is None or np is None:
|
|
52
|
+
raise ImportError(f"StreamingTTS requires imports via pip install sunholo[tts] - {texttospeech=} {sd=} {np=}")
|
|
53
|
+
|
|
54
|
+
log.info("Initializing StreamingTTS...")
|
|
55
|
+
self.client = texttospeech.TextToSpeechClient()
|
|
56
|
+
self.audio_queue = queue.Queue()
|
|
57
|
+
self.is_playing = False
|
|
58
|
+
self.sample_rate = 24000 # Google's default sample rate
|
|
59
|
+
self.language_code = "en-GB"
|
|
60
|
+
self.voice_gender = texttospeech.SsmlVoiceGender.NEUTRAL
|
|
61
|
+
self.voice_name = "en-GB-Journey-D"
|
|
62
|
+
# Audio processing parameters
|
|
63
|
+
self.fade_duration = 0.1 # 10ms fade in/out
|
|
64
|
+
self._initialize_audio_device()
|
|
65
|
+
|
|
66
|
+
def set_voice(self, voice_name: str):
|
|
67
|
+
"""
|
|
68
|
+
Set the language for text-to-speech conversion.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
language_code: Language code in BCP-47 format (e.g., 'en-US', 'es-ES', 'fr-FR')
|
|
72
|
+
"""
|
|
73
|
+
log.info(f"Setting voice to {voice_name}")
|
|
74
|
+
self.voice_name = voice_name
|
|
75
|
+
|
|
76
|
+
def set_language(self, language_code: str):
|
|
77
|
+
"""
|
|
78
|
+
Set the language for text-to-speech conversion.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
language_code: Language code in BCP-47 format (e.g., 'en-US', 'es-ES', 'fr-FR')
|
|
82
|
+
"""
|
|
83
|
+
log.info(f"Setting language to {language_code}")
|
|
84
|
+
self.language_code = language_code
|
|
85
|
+
|
|
86
|
+
def set_voice_gender(self, gender: str):
|
|
87
|
+
"""
|
|
88
|
+
Set the voice gender for text-to-speech conversion.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
gender: One of 'NEUTRAL', 'MALE', or 'FEMALE'
|
|
92
|
+
"""
|
|
93
|
+
gender_map = {
|
|
94
|
+
'NEUTRAL': texttospeech.SsmlVoiceGender.NEUTRAL,
|
|
95
|
+
'MALE': texttospeech.SsmlVoiceGender.MALE,
|
|
96
|
+
'FEMALE': texttospeech.SsmlVoiceGender.FEMALE
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if gender not in gender_map:
|
|
100
|
+
raise ValueError(f"Invalid gender '{gender}'. Must be one of: {', '.join(gender_map.keys())}")
|
|
101
|
+
|
|
102
|
+
log.info(f"Setting voice gender to {gender}")
|
|
103
|
+
self.voice_gender = gender_map[gender]
|
|
104
|
+
|
|
105
|
+
def text_to_audio(self, text):
|
|
106
|
+
"""Convert text chunk to audio bytes using Google Cloud TTS."""
|
|
107
|
+
log.info(f"TTS: {text=}")
|
|
108
|
+
synthesis_input = texttospeech.SynthesisInput(text=text)
|
|
109
|
+
|
|
110
|
+
voice = texttospeech.VoiceSelectionParams(
|
|
111
|
+
language_code=self.language_code,
|
|
112
|
+
ssml_gender=self.voice_gender,
|
|
113
|
+
name=self.voice_name
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
audio_config = texttospeech.AudioConfig(
|
|
117
|
+
audio_encoding=texttospeech.AudioEncoding.LINEAR16,
|
|
118
|
+
sample_rate_hertz=self.sample_rate
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
response = self.client.synthesize_speech(
|
|
122
|
+
input=synthesis_input,
|
|
123
|
+
voice=voice,
|
|
124
|
+
audio_config=audio_config
|
|
125
|
+
)
|
|
126
|
+
log.info("Got response from TTS")
|
|
127
|
+
|
|
128
|
+
# Convert audio bytes to numpy array for playback
|
|
129
|
+
audio_np = np.frombuffer(response.audio_content, dtype=np.int16)
|
|
130
|
+
return audio_np
|
|
131
|
+
|
|
132
|
+
def _initialize_audio_device(self):
|
|
133
|
+
"""Initialize audio device with proper settings."""
|
|
134
|
+
try:
|
|
135
|
+
# Set default device settings
|
|
136
|
+
sd.default.samplerate = self.sample_rate
|
|
137
|
+
sd.default.channels = 1
|
|
138
|
+
sd.default.dtype = np.int16
|
|
139
|
+
|
|
140
|
+
# Start and stop the stream once to "warm up" the audio device
|
|
141
|
+
dummy_audio = np.zeros(int(self.sample_rate * 1), dtype=np.int16)
|
|
142
|
+
with sd.OutputStream(
|
|
143
|
+
samplerate=self.sample_rate,
|
|
144
|
+
channels=1,
|
|
145
|
+
dtype=np.int16,
|
|
146
|
+
latency='low'
|
|
147
|
+
) as stream:
|
|
148
|
+
stream.write(dummy_audio)
|
|
149
|
+
|
|
150
|
+
log.info("Audio device initialized successfully")
|
|
151
|
+
except Exception as e:
|
|
152
|
+
log.error(f"Error initializing audio device: {e}")
|
|
153
|
+
raise
|
|
154
|
+
|
|
155
|
+
def _make_fade(x, in_length, out_length=None, type='l', copy=True):
|
|
156
|
+
"""Apply fade in/out to a signal.
|
|
157
|
+
|
|
158
|
+
If `x` is two-dimenstional, this works along the columns (= first
|
|
159
|
+
axis).
|
|
160
|
+
|
|
161
|
+
This is based on the *fade* effect of SoX, see:
|
|
162
|
+
http://sox.sourceforge.net/sox.html
|
|
163
|
+
|
|
164
|
+
The C implementation can be found here:
|
|
165
|
+
http://sourceforge.net/p/sox/code/ci/master/tree/src/fade.c
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
x : array_like
|
|
170
|
+
Input signal.
|
|
171
|
+
in_length : int
|
|
172
|
+
Length of fade-in in samples (contrary to SoX, where this is
|
|
173
|
+
specified in seconds).
|
|
174
|
+
out_length : int, optional
|
|
175
|
+
Length of fade-out in samples. If not specified, `fade_in` is
|
|
176
|
+
used also for the fade-out.
|
|
177
|
+
type : {'t', 'q', 'h', 'l', 'p'}, optional
|
|
178
|
+
Select the shape of the fade curve: 'q' for quarter of a sine
|
|
179
|
+
wave, 'h' for half a sine wave, 't' for linear ("triangular")
|
|
180
|
+
slope, 'l' for logarithmic, and 'p' for inverted parabola.
|
|
181
|
+
The default is logarithmic.
|
|
182
|
+
copy : bool, optional
|
|
183
|
+
If `False`, the fade is applied in-place and a reference to
|
|
184
|
+
`x` is returned.
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
x = np.array(x, copy=copy)
|
|
188
|
+
|
|
189
|
+
if out_length is None:
|
|
190
|
+
out_length = in_length
|
|
191
|
+
|
|
192
|
+
def make_fade(length, type):
|
|
193
|
+
fade = np.arange(length) / length
|
|
194
|
+
if type == 't': # triangle
|
|
195
|
+
pass
|
|
196
|
+
elif type == 'q': # quarter of sinewave
|
|
197
|
+
fade = np.sin(fade * np.pi / 2)
|
|
198
|
+
elif type == 'h': # half of sinewave... eh cosine wave
|
|
199
|
+
fade = (1 - np.cos(fade * np.pi)) / 2
|
|
200
|
+
elif type == 'l': # logarithmic
|
|
201
|
+
fade = np.power(0.1, (1 - fade) * 5) # 5 means 100 db attenuation
|
|
202
|
+
elif type == 'p': # inverted parabola
|
|
203
|
+
fade = (1 - (1 - fade)**2)
|
|
204
|
+
else:
|
|
205
|
+
raise ValueError("Unknown fade type {0!r}".format(type))
|
|
206
|
+
return fade
|
|
207
|
+
|
|
208
|
+
# Using .T w/o [:] causes error: https://github.com/numpy/numpy/issues/2667
|
|
209
|
+
x[:in_length].T[:] *= make_fade(in_length, type)
|
|
210
|
+
x[len(x) - out_length:].T[:] *= make_fade(out_length, type)[::-1]
|
|
211
|
+
return x
|
|
212
|
+
|
|
213
|
+
def _apply_fade(self, audio: np.ndarray, fade_in: bool = True, fade_out: bool = True) -> np.ndarray:
|
|
214
|
+
"""Apply fade in/out to audio to prevent clicks."""
|
|
215
|
+
fade_length = int(self.fade_duration * self.sample_rate)
|
|
216
|
+
audio = audio.astype(np.float32) # Convert to float for fade calculation
|
|
217
|
+
|
|
218
|
+
if fade_in:
|
|
219
|
+
fade_in_curve = np.linspace(0, 1, fade_length)
|
|
220
|
+
audio[:fade_length] *= fade_in_curve
|
|
221
|
+
|
|
222
|
+
if fade_out:
|
|
223
|
+
fade_out_curve = np.linspace(1, 0, fade_length)
|
|
224
|
+
audio[-fade_length:] *= fade_out_curve
|
|
225
|
+
|
|
226
|
+
return audio.astype(np.int16) # Convert back to int16
|
|
227
|
+
|
|
228
|
+
def _play_audio_chunk(self, audio_chunk: np.ndarray):
|
|
229
|
+
"""Play a single audio chunk with proper device handling."""
|
|
230
|
+
try:
|
|
231
|
+
# Add small silence padding and apply fades
|
|
232
|
+
padding = np.zeros(int(1 * self.sample_rate), dtype=np.int16)
|
|
233
|
+
audio_with_padding = np.concatenate([padding, audio_chunk, padding])
|
|
234
|
+
processed_audio = self._apply_fade(audio_with_padding)
|
|
235
|
+
|
|
236
|
+
# Use context manager for proper stream handling
|
|
237
|
+
with sd.OutputStream(
|
|
238
|
+
samplerate=self.sample_rate,
|
|
239
|
+
channels=1,
|
|
240
|
+
dtype=np.int16,
|
|
241
|
+
latency='low'
|
|
242
|
+
) as stream:
|
|
243
|
+
stream.write(processed_audio)
|
|
244
|
+
stream.write(np.zeros(int(0.1 * self.sample_rate), dtype=np.int16))
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
log.error(f"Error during audio playback: {e}")
|
|
248
|
+
raise
|
|
249
|
+
|
|
250
|
+
def audio_player(self):
|
|
251
|
+
"""Continuously play audio chunks from the queue."""
|
|
252
|
+
log.info("Audio player started")
|
|
253
|
+
while self.is_playing or not self.audio_queue.empty():
|
|
254
|
+
if not self.audio_queue.empty():
|
|
255
|
+
audio_chunk = self.audio_queue.get()
|
|
256
|
+
self._play_audio_chunk(audio_chunk)
|
|
257
|
+
time.sleep(0.1)
|
|
258
|
+
|
|
259
|
+
def process_text_stream(self, text_generator):
|
|
260
|
+
"""Process incoming text stream and convert to audio."""
|
|
261
|
+
self.is_playing = True
|
|
262
|
+
|
|
263
|
+
# Start audio playback thread
|
|
264
|
+
player_thread = threading.Thread(target=self.audio_player)
|
|
265
|
+
player_thread.start()
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
# Process text chunks in parallel
|
|
269
|
+
with ThreadPoolExecutor(max_workers=3) as executor:
|
|
270
|
+
futures = []
|
|
271
|
+
for text_chunk in text_generator:
|
|
272
|
+
future = executor.submit(self.text_to_audio, text_chunk)
|
|
273
|
+
futures.append(future)
|
|
274
|
+
|
|
275
|
+
# Process results as they complete
|
|
276
|
+
for future in futures:
|
|
277
|
+
audio_chunk = future.result()
|
|
278
|
+
self.audio_queue.put(audio_chunk)
|
|
279
|
+
finally:
|
|
280
|
+
self.is_playing = False
|
|
281
|
+
player_thread.join()
|
|
282
|
+
|
|
283
|
+
def save_to_file(self, text_generator, output_path):
|
|
284
|
+
"""Save the audio to a WAV file instead of playing it."""
|
|
285
|
+
import wave
|
|
286
|
+
|
|
287
|
+
all_audio = []
|
|
288
|
+
for text_chunk in text_generator:
|
|
289
|
+
audio_chunk = self.text_to_audio(text_chunk)
|
|
290
|
+
processed_chunk = self._apply_fade(audio_chunk)
|
|
291
|
+
all_audio.append(processed_chunk)
|
|
292
|
+
|
|
293
|
+
# Add small silence between chunks and at ends
|
|
294
|
+
silence = np.zeros(int(0.1 * self.sample_rate), dtype=np.int16)
|
|
295
|
+
final_audio = silence
|
|
296
|
+
for chunk in all_audio:
|
|
297
|
+
final_audio = np.concatenate([final_audio, chunk, silence])
|
|
298
|
+
|
|
299
|
+
with wave.open(output_path, 'wb') as wav_file:
|
|
300
|
+
wav_file.setnchannels(1)
|
|
301
|
+
wav_file.setsampwidth(2) # 16-bit audio
|
|
302
|
+
wav_file.setframerate(self.sample_rate)
|
|
303
|
+
wav_file.writeframes(final_audio.tobytes())
|
|
304
|
+
|
|
305
|
+
def tts_command(args):
|
|
306
|
+
"""
|
|
307
|
+
Executes the TTS command based on parsed arguments.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
args: The parsed command-line arguments.
|
|
311
|
+
"""
|
|
312
|
+
if console is None:
|
|
313
|
+
raise ImportError("Need cli tools to use TTS commands - install via `pip install sunholo[cli,tts]`")
|
|
314
|
+
|
|
315
|
+
from rich.panel import Panel
|
|
316
|
+
|
|
317
|
+
def text_generator(input_source: str, is_file: bool = False):
|
|
318
|
+
"""Generate text from either a file or direct input."""
|
|
319
|
+
if is_file:
|
|
320
|
+
try:
|
|
321
|
+
with open(input_source, 'r') as f:
|
|
322
|
+
for line in f:
|
|
323
|
+
line = line.strip()
|
|
324
|
+
if line: # Skip empty lines
|
|
325
|
+
yield line
|
|
326
|
+
except FileNotFoundError:
|
|
327
|
+
console.print(f"Error: The input file '{input_source}' was not found.")
|
|
328
|
+
sys.exit(1)
|
|
329
|
+
else:
|
|
330
|
+
yield input_source
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
tts = StreamingTTS()
|
|
334
|
+
|
|
335
|
+
# Configure TTS based on arguments
|
|
336
|
+
if args.language:
|
|
337
|
+
tts.set_language(args.language)
|
|
338
|
+
if args.voice_gender:
|
|
339
|
+
tts.set_voice_gender(args.voice_gender)
|
|
340
|
+
if args.sample_rate:
|
|
341
|
+
tts.sample_rate = args.sample_rate
|
|
342
|
+
if args.voice_name:
|
|
343
|
+
tts.set_voice(args.voice_name)
|
|
344
|
+
|
|
345
|
+
# Process the text
|
|
346
|
+
if args.action == 'speak':
|
|
347
|
+
console.print(
|
|
348
|
+
Panel((
|
|
349
|
+
f"Saying: {args.text}"
|
|
350
|
+
),
|
|
351
|
+
title=f"Text to Speech",
|
|
352
|
+
subtitle=f"{tts.voice_name} is talking"),
|
|
353
|
+
)
|
|
354
|
+
tts.process_text_stream(
|
|
355
|
+
text_generator(args.text, is_file=args.file)
|
|
356
|
+
)
|
|
357
|
+
elif args.action == 'save':
|
|
358
|
+
if not args.output:
|
|
359
|
+
console.print("Error: Output file path is required for save action")
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
tts.save_to_file(
|
|
363
|
+
text_generator(args.text, is_file=args.file),
|
|
364
|
+
args.output
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
console.rule("Successfully processed text-to-speech request.")
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
console.print(f"[bold red]Error processing text-to-speech: {str(e)}[/bold red]")
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
def setup_tts_subparser(subparsers):
|
|
374
|
+
"""
|
|
375
|
+
Sets up an argparse subparser for the 'tts' command.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
subparsers: The subparsers object from argparse.ArgumentParser().
|
|
379
|
+
"""
|
|
380
|
+
# TTS main parser
|
|
381
|
+
tts_parser = subparsers.add_parser('tts', help='Text-to-Speech conversion utilities')
|
|
382
|
+
tts_subparsers = tts_parser.add_subparsers(dest='action', help='TTS subcommands')
|
|
383
|
+
|
|
384
|
+
# Common arguments for both speak and save commands
|
|
385
|
+
common_args = argparse.ArgumentParser(add_help=False)
|
|
386
|
+
common_args.add_argument('text', help='Text to convert to speech (or file path if --file is used)')
|
|
387
|
+
common_args.add_argument('--file', action='store_true',
|
|
388
|
+
help='Treat the text argument as a file path')
|
|
389
|
+
common_args.add_argument('--language', default='en-GB',
|
|
390
|
+
help='Language code (e.g., en-US, es-ES)')
|
|
391
|
+
common_args.add_argument('--voice-gender', choices=['NEUTRAL', 'MALE', 'FEMALE'],
|
|
392
|
+
default='NEUTRAL', help='Voice gender to use')
|
|
393
|
+
common_args.add_argument('--sample-rate', type=int, default=24000,
|
|
394
|
+
help='Audio sample rate in Hz')
|
|
395
|
+
common_args.add_argument('--voice_name', default='en-GB-Journey-D', help='A voice name from supported list at https://cloud.google.com/text-to-speech/docs/voices')
|
|
396
|
+
|
|
397
|
+
# Speak command - converts text to speech and plays it
|
|
398
|
+
speak_parser = tts_subparsers.add_parser('speak',
|
|
399
|
+
help='Convert text to speech and play it',
|
|
400
|
+
parents=[common_args])
|
|
401
|
+
speak_parser.set_defaults(func=tts_command)
|
|
402
|
+
|
|
403
|
+
# Save command - converts text to speech and saves to file
|
|
404
|
+
save_parser = tts_subparsers.add_parser('save',
|
|
405
|
+
help='Convert text to speech and save to file',
|
|
406
|
+
parents=[common_args])
|
|
407
|
+
save_parser.add_argument('--output', default='audio.wav',
|
|
408
|
+
help='Output audio file path (.wav)')
|
|
409
|
+
save_parser.set_defaults(func=tts_command)
|
|
410
|
+
|
|
411
|
+
# Set the default function for the TTS parser
|
|
412
|
+
tts_parser.set_defaults(func=lambda args: tts_parser.print_help())
|
|
413
|
+
|
|
414
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
try:
|
|
2
|
-
import
|
|
2
|
+
from hcl2 import load as hcl2_load
|
|
3
3
|
except ImportError:
|
|
4
|
-
|
|
4
|
+
hcl2_load = None
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
7
|
import subprocess
|
|
@@ -66,8 +66,8 @@ class TerraformVarsEditor:
|
|
|
66
66
|
-------
|
|
67
67
|
editor = TerraformVarsEditor('example.tfvars', '/path/to/terraform/config')
|
|
68
68
|
"""
|
|
69
|
-
if
|
|
70
|
-
raise ImportError('hcl2 is required for parsing terraform files, install via `pip install sunholo[iac]`')
|
|
69
|
+
if hcl2_load is None:
|
|
70
|
+
raise ImportError('hcl2.load is required for parsing terraform files, install via `pip install sunholo"[iac]"`')
|
|
71
71
|
|
|
72
72
|
# Check for the MULTIVAC_TERRAFORM_DIR environment variable
|
|
73
73
|
if terraform_dir == '.' and 'MULTIVAC_TERRAFORM_DIR' in os.environ:
|
|
@@ -100,7 +100,7 @@ class TerraformVarsEditor:
|
|
|
100
100
|
data = self._load_tfvars()
|
|
101
101
|
"""
|
|
102
102
|
with open(self.tfvars_file, 'r') as file:
|
|
103
|
-
return
|
|
103
|
+
return hcl2_load(file)
|
|
104
104
|
|
|
105
105
|
def _save_tfvars(self) -> None:
|
|
106
106
|
"""
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sunholo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.106.0
|
|
4
4
|
Summary: Large Language Model DevOps - a package to help deploy LLMs to the Cloud.
|
|
5
5
|
Home-page: https://github.com/sunholo-data/sunholo-py
|
|
6
|
-
Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.
|
|
6
|
+
Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.106.0.tar.gz
|
|
7
7
|
Author: Holosun ApS
|
|
8
8
|
Author-email: multivac@sunholo.com
|
|
9
9
|
License: Apache License, Version 2.0
|
|
@@ -43,6 +43,7 @@ Requires-Dist: google-cloud-logging ; extra == 'all'
|
|
|
43
43
|
Requires-Dist: google-cloud-storage ; extra == 'all'
|
|
44
44
|
Requires-Dist: google-cloud-pubsub ; extra == 'all'
|
|
45
45
|
Requires-Dist: google-cloud-discoveryengine ; extra == 'all'
|
|
46
|
+
Requires-Dist: google-cloud-texttospeech ; extra == 'all'
|
|
46
47
|
Requires-Dist: google-generativeai >=0.7.1 ; extra == 'all'
|
|
47
48
|
Requires-Dist: gunicorn ; extra == 'all'
|
|
48
49
|
Requires-Dist: httpcore ; extra == 'all'
|
|
@@ -58,6 +59,7 @@ Requires-Dist: langchain-google-alloydb-pg ; extra == 'all'
|
|
|
58
59
|
Requires-Dist: langchain-anthropic ==0.1.23 ; extra == 'all'
|
|
59
60
|
Requires-Dist: langchain-google-vertexai ; extra == 'all'
|
|
60
61
|
Requires-Dist: langfuse ; extra == 'all'
|
|
62
|
+
Requires-Dist: numpy ; extra == 'all'
|
|
61
63
|
Requires-Dist: pg8000 ; extra == 'all'
|
|
62
64
|
Requires-Dist: pgvector ; extra == 'all'
|
|
63
65
|
Requires-Dist: pillow ; extra == 'all'
|
|
@@ -69,6 +71,7 @@ Requires-Dist: python-hcl2 ; extra == 'all'
|
|
|
69
71
|
Requires-Dist: python-socketio ; extra == 'all'
|
|
70
72
|
Requires-Dist: pytesseract ; extra == 'all'
|
|
71
73
|
Requires-Dist: rich ; extra == 'all'
|
|
74
|
+
Requires-Dist: sounddevice ; extra == 'all'
|
|
72
75
|
Requires-Dist: supabase ; extra == 'all'
|
|
73
76
|
Requires-Dist: tabulate ; extra == 'all'
|
|
74
77
|
Requires-Dist: tantivy ; extra == 'all'
|
|
@@ -111,6 +114,7 @@ Requires-Dist: google-cloud-storage ; extra == 'gcp'
|
|
|
111
114
|
Requires-Dist: google-cloud-logging ; extra == 'gcp'
|
|
112
115
|
Requires-Dist: google-cloud-pubsub ; extra == 'gcp'
|
|
113
116
|
Requires-Dist: google-cloud-discoveryengine ; extra == 'gcp'
|
|
117
|
+
Requires-Dist: google-cloud-texttospeech ; extra == 'gcp'
|
|
114
118
|
Requires-Dist: google-generativeai >=0.7.1 ; extra == 'gcp'
|
|
115
119
|
Requires-Dist: langchain-google-genai ==1.0.10 ; extra == 'gcp'
|
|
116
120
|
Requires-Dist: langchain-google-alloydb-pg >=0.2.2 ; extra == 'gcp'
|
|
@@ -142,6 +146,10 @@ Requires-Dist: unstructured[local-inference] ==0.14.9 ; extra == 'pipeline'
|
|
|
142
146
|
Provides-Extra: tools
|
|
143
147
|
Requires-Dist: openapi-spec-validator ; extra == 'tools'
|
|
144
148
|
Requires-Dist: playwright ; extra == 'tools'
|
|
149
|
+
Provides-Extra: tts
|
|
150
|
+
Requires-Dist: google-cloud-texttospeech ; extra == 'tts'
|
|
151
|
+
Requires-Dist: numpy ; extra == 'tts'
|
|
152
|
+
Requires-Dist: sounddevice ; extra == 'tts'
|
|
145
153
|
|
|
146
154
|
## Introduction
|
|
147
155
|
This is the Sunholo Python project, a comprehensive toolkit for working with language models and vector stores on Google Cloud Platform. It provides a wide range of functionalities and utilities to facilitate the development and deployment of language model applications.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sunholo/__init__.py,sha256=
|
|
1
|
+
sunholo/__init__.py,sha256=LV-oCDt_Q01UM8N_dua8m6bD5IrrZL1mZLWKkYfjOeQ,1178
|
|
2
2
|
sunholo/custom_logging.py,sha256=YfIN1oP3dOEkkYkyRBU8BGS3uJFGwUDsFCl8mIVbwvE,12225
|
|
3
3
|
sunholo/agents/__init__.py,sha256=X2I3pPkGeKWjc3d0QgSpkTyqD8J8JtrEWqwrumf1MMc,391
|
|
4
4
|
sunholo/agents/chat_history.py,sha256=Gph_CdlP2otYnNdR1q1Umyyyvcad2F6K3LxU5yBQ9l0,5387
|
|
@@ -43,8 +43,8 @@ sunholo/chunker/pubsub.py,sha256=48bhuAcszN7LGe3-ksPSLHHhq0uKxiXOrizck5qpcP0,101
|
|
|
43
43
|
sunholo/chunker/splitter.py,sha256=QLAEsJOpEYFZr9-UGZUuAlNVyjfCWb8jvzCHg0rVShE,6751
|
|
44
44
|
sunholo/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
45
|
sunholo/cli/chat_vac.py,sha256=sYPzUDwwwebJvIobv3GRW_xbQQ4BTy9G-WHdarGCHB0,23705
|
|
46
|
-
sunholo/cli/cli.py,sha256=
|
|
47
|
-
sunholo/cli/cli_init.py,sha256=
|
|
46
|
+
sunholo/cli/cli.py,sha256=Bhyrs8GEtJTbsvPYufEY184ra13eusATXAnJClJ_LGY,4474
|
|
47
|
+
sunholo/cli/cli_init.py,sha256=ITM-GeCRia1kXXQekzkYqYmfcaByCwr1ZWfAA3qCaVk,8507
|
|
48
48
|
sunholo/cli/configs.py,sha256=QUM9DvKOdZmEQRM5uI3Nh887T0YDiSMr7O240zTLqws,4546
|
|
49
49
|
sunholo/cli/deploy.py,sha256=zxdwUsRTRMC8U5vyRv0JiKBLFn84Ug_Tc88-_h9hJSs,1609
|
|
50
50
|
sunholo/cli/embedder.py,sha256=v-FKiSPHaQzB6ctClclYueIf3bf3CqYtC1oRgPfT4dY,5566
|
|
@@ -88,7 +88,7 @@ sunholo/gcs/metadata.py,sha256=oQLcXi4brsZ74aegWyC1JZmhlaEV270HS5_UWtAYYWE,898
|
|
|
88
88
|
sunholo/genai/__init__.py,sha256=dBl6IA3-Fx6-Vx81r0XqxHlUq6WeW1iDX188dpChu8s,115
|
|
89
89
|
sunholo/genai/images.py,sha256=EyjsDqt6XQw99pZUQamomCpMOoIah9bp3XY94WPU7Ms,1678
|
|
90
90
|
sunholo/genai/init.py,sha256=yG8E67TduFCTQPELo83OJuWfjwTnGZsyACospahyEaY,687
|
|
91
|
-
sunholo/genai/process_funcs_cls.py,sha256=
|
|
91
|
+
sunholo/genai/process_funcs_cls.py,sha256=SnonJh4CqvJFSSvlwDJK6tqXbaYF_651IopCl37ZVOs,31578
|
|
92
92
|
sunholo/genai/safety.py,sha256=mkFDO_BeEgiKjQd9o2I4UxB6XI7a9U-oOFjZ8LGRUC4,1238
|
|
93
93
|
sunholo/invoke/__init__.py,sha256=o1RhwBGOtVK0MIdD55fAIMCkJsxTksi8GD5uoqVKI-8,184
|
|
94
94
|
sunholo/invoke/async_class.py,sha256=G8vD2H94fpBc37mSJSQODEKJ67P2mPQEHabtDaLOvxE,8033
|
|
@@ -115,6 +115,8 @@ sunholo/pubsub/pubsub_manager.py,sha256=19w_N0LiG-wgVWvgJ13b8BUeN8ZzgSPXAhPmL1HR
|
|
|
115
115
|
sunholo/qna/__init__.py,sha256=F8q1uR_HreoSX0IfmKY1qoSwIgXhO2Q8kuDSxh9_-EE,28
|
|
116
116
|
sunholo/qna/parsers.py,sha256=YpOaK5S_LxJ6FbliSYDc3AVOJ62RVduayoNnzi_p8CM,2494
|
|
117
117
|
sunholo/qna/retry.py,sha256=yMw7RTkw-RXCzfENPJOt8c32mXlpvOR589EGkvK-6yI,2028
|
|
118
|
+
sunholo/senses/__init__.py,sha256=fbWqVwwzkV5uRSb8lQzo4pn0ja_VYVWbUYapurSowBs,39
|
|
119
|
+
sunholo/senses/stream_voice.py,sha256=Q5kxf19oJdDRoE_0ifkPrkeTDgVaKiHepH65TY2Kd3E,15682
|
|
118
120
|
sunholo/streaming/__init__.py,sha256=MpbydI2UYo_adttPQFkxNM33b-QRyNEbrKJx0C2AGPc,241
|
|
119
121
|
sunholo/streaming/content_buffer.py,sha256=0LHMwH4ctq5kjhIgMFNH0bA1RL0jMISlLVzzLcFrvv4,12766
|
|
120
122
|
sunholo/streaming/langserve.py,sha256=hi7q8WY8DPKrALl9m_dOMxWOdE-iEuk7YW05SVDFIX8,6514
|
|
@@ -123,7 +125,7 @@ sunholo/streaming/streaming.py,sha256=gSxLuwK-5-t5D1AjcHf838BY-L4jvdkdn_xePl-DK3
|
|
|
123
125
|
sunholo/summarise/__init__.py,sha256=MZk3dblUMODcPb1crq4v-Z508NrFIpkSWNf9FIO8BcU,38
|
|
124
126
|
sunholo/summarise/summarise.py,sha256=95A-6PXFGanjona8DvZPnnIHLbzZ2ip5hO0wOAJQhfw,3791
|
|
125
127
|
sunholo/terraform/__init__.py,sha256=yixxEltc3n9UpZaVi05GlgS-YRq_DVGjUc37I9ajeP4,76
|
|
126
|
-
sunholo/terraform/tfvars_editor.py,sha256
|
|
128
|
+
sunholo/terraform/tfvars_editor.py,sha256=-TBBWbALYb5HLFYwD2s70Kp27ys6fzIyreBFOT5kqqY,13142
|
|
127
129
|
sunholo/tools/__init__.py,sha256=5NuYpwwTX81qGUWvgwfItoSLXteNnp7KjgD7IPZUFjI,53
|
|
128
130
|
sunholo/tools/web_browser.py,sha256=8Gdf02F4zCOeSnijnfaL6jzk4oaSI0cj48o-esoWzwE,29086
|
|
129
131
|
sunholo/utils/__init__.py,sha256=Hv02T5L2zYWvCso5hzzwm8FQogwBq0OgtUbN_7Quzqc,89
|
|
@@ -147,9 +149,9 @@ sunholo/vertex/init.py,sha256=1OQwcPBKZYBTDPdyU7IM4X4OmiXLdsNV30C-fee2scQ,2875
|
|
|
147
149
|
sunholo/vertex/memory_tools.py,sha256=tBZxqVZ4InTmdBvLlOYwoSEWu4-kGquc-gxDwZCC4FA,7667
|
|
148
150
|
sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
|
|
149
151
|
sunholo/vertex/type_dict_to_json.py,sha256=uTzL4o9tJRao4u-gJOFcACgWGkBOtqACmb6ihvCErL8,4694
|
|
150
|
-
sunholo-0.
|
|
151
|
-
sunholo-0.
|
|
152
|
-
sunholo-0.
|
|
153
|
-
sunholo-0.
|
|
154
|
-
sunholo-0.
|
|
155
|
-
sunholo-0.
|
|
152
|
+
sunholo-0.106.0.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
|
|
153
|
+
sunholo-0.106.0.dist-info/METADATA,sha256=qDrBZX6mvVhBpOZHSd9t9v6FFQLEoquh0jiPppsFLeM,8670
|
|
154
|
+
sunholo-0.106.0.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
|
155
|
+
sunholo-0.106.0.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
|
|
156
|
+
sunholo-0.106.0.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
|
|
157
|
+
sunholo-0.106.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|