sunholo 0.105.8__py3-none-any.whl → 0.106.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.
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"[bold red]ERROR: Directory {project_dir} already exists. Please choose a different project name.[/bold red]")
43
- return
44
-
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)
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
- raise ValueError("Must specify a terraform_dir or use the MULTIVAC_TERRAFORM_DIR environment variable")
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,440 @@
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
+ # Separate fade durations for playback and file saving
64
+ self.playback_fade_duration = 0.05 # 50ms fade for real-time playback
65
+ self.file_fade_duration = 0.01 # 10ms fade for file saving
66
+ self.stream = None
67
+ self._initialize_audio_device()
68
+
69
+ def set_voice(self, voice_name: str):
70
+ """
71
+ Set the language for text-to-speech conversion.
72
+
73
+ Args:
74
+ language_code: Language code in BCP-47 format (e.g., 'en-US', 'es-ES', 'fr-FR')
75
+ """
76
+ log.info(f"Setting voice to {voice_name}")
77
+ self.voice_name = voice_name
78
+
79
+ def set_language(self, language_code: str):
80
+ """
81
+ Set the language for text-to-speech conversion.
82
+
83
+ Args:
84
+ language_code: Language code in BCP-47 format (e.g., 'en-US', 'es-ES', 'fr-FR')
85
+ """
86
+ log.info(f"Setting language to {language_code}")
87
+ self.language_code = language_code
88
+
89
+ def set_voice_gender(self, gender: str):
90
+ """
91
+ Set the voice gender for text-to-speech conversion.
92
+
93
+ Args:
94
+ gender: One of 'NEUTRAL', 'MALE', or 'FEMALE'
95
+ """
96
+ gender_map = {
97
+ 'NEUTRAL': texttospeech.SsmlVoiceGender.NEUTRAL,
98
+ 'MALE': texttospeech.SsmlVoiceGender.MALE,
99
+ 'FEMALE': texttospeech.SsmlVoiceGender.FEMALE
100
+ }
101
+
102
+ if gender not in gender_map:
103
+ raise ValueError(f"Invalid gender '{gender}'. Must be one of: {', '.join(gender_map.keys())}")
104
+
105
+ log.info(f"Setting voice gender to {gender}")
106
+ self.voice_gender = gender_map[gender]
107
+
108
+ def text_to_audio(self, text):
109
+ """Convert text chunk to audio bytes using Google Cloud TTS."""
110
+ log.info(f"TTS: {text=}")
111
+ synthesis_input = texttospeech.SynthesisInput(text=text)
112
+
113
+ voice = texttospeech.VoiceSelectionParams(
114
+ language_code=self.language_code,
115
+ ssml_gender=self.voice_gender,
116
+ name=self.voice_name
117
+ )
118
+
119
+ audio_config = texttospeech.AudioConfig(
120
+ audio_encoding=texttospeech.AudioEncoding.LINEAR16,
121
+ sample_rate_hertz=self.sample_rate
122
+ )
123
+
124
+ response = self.client.synthesize_speech(
125
+ input=synthesis_input,
126
+ voice=voice,
127
+ audio_config=audio_config
128
+ )
129
+ log.info("Got response from TTS")
130
+
131
+ # Convert audio bytes to numpy array for playback
132
+ audio_np = np.frombuffer(response.audio_content, dtype=np.int16)
133
+ return audio_np
134
+
135
+ def _initialize_audio_device(self):
136
+ """Initialize audio device with proper settings."""
137
+ try:
138
+ # Set default device settings
139
+ sd.default.samplerate = self.sample_rate
140
+ sd.default.channels = 1
141
+ sd.default.dtype = np.int16
142
+
143
+ # Initialize persistent output stream
144
+ self.stream = sd.OutputStream(
145
+ samplerate=self.sample_rate,
146
+ channels=1,
147
+ dtype=np.int16,
148
+ latency='low'
149
+ )
150
+ self.stream.start()
151
+
152
+ log.info("Audio device initialized successfully")
153
+ except Exception as e:
154
+ log.error(f"Error initializing audio device: {e}")
155
+ raise
156
+
157
+ def _make_fade(self, length: int, fade_type: str='l') -> np.ndarray:
158
+ """Generate a fade curve of specified length and type."""
159
+ fade = np.arange(length, dtype=np.float32) / length
160
+
161
+ if fade_type == 't': # triangle
162
+ pass
163
+ elif fade_type == 'q': # quarter of sinewave
164
+ fade = np.sin(fade * np.pi / 2)
165
+ elif fade_type == 'h': # half of sinewave
166
+ fade = (1 - np.cos(fade * np.pi)) / 2
167
+ elif fade_type == 'l': # logarithmic
168
+ fade = np.power(0.1, (1 - fade) * 5)
169
+ elif fade_type == 'p': # inverted parabola
170
+ fade = (1 - (1 - fade)**2)
171
+ else:
172
+ raise ValueError(f"Unknown fade type {fade_type!r}")
173
+
174
+ return fade
175
+
176
+ def _apply_fade(self, audio: np.ndarray, fade_duration: float, fade_in: bool = True, fade_out: bool = True) -> np.ndarray:
177
+ """Apply fade in/out to audio with specified duration."""
178
+ if audio.ndim != 1:
179
+ raise ValueError("Audio must be 1-dimensional")
180
+
181
+ fade_length = int(fade_duration * self.sample_rate)
182
+ audio = audio.astype(np.float32)
183
+
184
+ if fade_in:
185
+ fade_in_curve = self._make_fade(fade_length, 'l')
186
+ audio[:fade_length] *= fade_in_curve
187
+
188
+ if fade_out:
189
+ fade_out_curve = self._make_fade(fade_length, 'l')
190
+ audio[-fade_length:] *= fade_out_curve[::-1]
191
+
192
+ return audio.astype(np.int16)
193
+
194
+
195
+ def _play_audio_chunk(self, audio_chunk: np.ndarray, is_final_chunk: bool = False):
196
+ """Play a single audio chunk with proper device handling."""
197
+ try:
198
+ # Add longer padding for the final chunk
199
+ padding_duration = 0.1 if is_final_chunk else 0.02
200
+ padding = np.zeros(int(padding_duration * self.sample_rate), dtype=np.int16)
201
+
202
+ if is_final_chunk:
203
+ # For final chunk, add extra padding and longer fade
204
+ audio_with_padding = np.concatenate([
205
+ padding,
206
+ audio_chunk,
207
+ padding,
208
+ np.zeros(int(0.2 * self.sample_rate), dtype=np.int16) # Extra tail padding
209
+ ])
210
+ fade_duration = self.playback_fade_duration * 2 # Longer fade for end
211
+ else:
212
+ audio_with_padding = np.concatenate([padding, audio_chunk, padding])
213
+ fade_duration = self.playback_fade_duration
214
+
215
+ processed_audio = self._apply_fade(
216
+ audio_with_padding,
217
+ fade_duration=fade_duration,
218
+ fade_in=True,
219
+ fade_out=True
220
+ )
221
+
222
+ if self.stream and self.stream.active:
223
+ self.stream.write(processed_audio)
224
+ if is_final_chunk:
225
+ # Write a small buffer of silence at the end
226
+ final_silence = np.zeros(int(0.1 * self.sample_rate), dtype=np.int16)
227
+ self.stream.write(final_silence)
228
+ else:
229
+ with sd.OutputStream(
230
+ samplerate=self.sample_rate,
231
+ channels=1,
232
+ dtype=np.int16,
233
+ latency='low'
234
+ ) as temp_stream:
235
+ temp_stream.write(processed_audio)
236
+ if is_final_chunk:
237
+ temp_stream.write(np.zeros(int(0.1 * self.sample_rate), dtype=np.int16))
238
+
239
+ except Exception as e:
240
+ log.error(f"Error during audio playback: {e}")
241
+ raise
242
+
243
+ def audio_player(self):
244
+ """Continuously play audio chunks from the queue."""
245
+ log.info("Audio player started")
246
+ try:
247
+ while self.is_playing or not self.audio_queue.empty():
248
+ if not self.audio_queue.empty():
249
+ audio_chunk = self.audio_queue.get()
250
+ self._play_audio_chunk(audio_chunk)
251
+ time.sleep(0.005) # Reduced sleep time for more responsive playback
252
+ finally:
253
+ # Ensure stream is properly closed
254
+ if self.stream and self.stream.active:
255
+ self.stream.stop()
256
+ self.stream.close()
257
+ self.stream = None
258
+
259
+ def __del__(self):
260
+ """Cleanup method to ensure stream is closed."""
261
+ if hasattr(self, 'stream') and self.stream and self.stream.active:
262
+ # Write a small silence buffer before closing
263
+ final_silence = np.zeros(int(0.1 * self.sample_rate), dtype=np.int16)
264
+ try:
265
+ self.stream.write(final_silence)
266
+ time.sleep(0.1) # Let the final audio finish playing
267
+ except Exception:
268
+ pass # Ignore errors during cleanup
269
+ self.stream.stop()
270
+ self.stream.close()
271
+
272
+ def process_text_stream(self, text_generator):
273
+ """Process incoming text stream and convert to audio."""
274
+ self.is_playing = True
275
+
276
+ # Start audio playback thread
277
+ player_thread = threading.Thread(target=self.audio_player)
278
+ player_thread.start()
279
+
280
+ try:
281
+ # Process text chunks in parallel
282
+ with ThreadPoolExecutor(max_workers=3) as executor:
283
+ futures = []
284
+ for text_chunk in text_generator:
285
+ future = executor.submit(self.text_to_audio, text_chunk)
286
+ futures.append(future)
287
+
288
+ # Process results as they complete
289
+ for future in futures:
290
+ audio_chunk = future.result()
291
+ self.audio_queue.put(audio_chunk)
292
+ finally:
293
+ self.is_playing = False
294
+ player_thread.join()
295
+
296
+ def save_to_file(self, text_generator, output_path):
297
+ """Save the audio to a WAV file with minimal fading."""
298
+ import wave
299
+
300
+ all_audio = []
301
+ for text_chunk in text_generator:
302
+ audio_chunk = self.text_to_audio(text_chunk)
303
+ # Use shorter fade duration for file saving
304
+ processed_chunk = self._apply_fade(
305
+ audio_chunk,
306
+ fade_duration=self.file_fade_duration
307
+ )
308
+ all_audio.append(processed_chunk)
309
+
310
+ # Add minimal silence between chunks
311
+ silence = np.zeros(int(0.05 * self.sample_rate), dtype=np.int16)
312
+ final_audio = silence
313
+
314
+ for i, chunk in enumerate(all_audio):
315
+ if i == len(all_audio) - 1:
316
+ # For the last chunk, use a slightly longer fade out
317
+ chunk = self._apply_fade(
318
+ chunk,
319
+ fade_duration=self.file_fade_duration * 2,
320
+ fade_in=False,
321
+ fade_out=True
322
+ )
323
+ final_audio = np.concatenate([final_audio, chunk, silence])
324
+
325
+ with wave.open(output_path, 'wb') as wav_file:
326
+ wav_file.setnchannels(1)
327
+ wav_file.setsampwidth(2)
328
+ wav_file.setframerate(self.sample_rate)
329
+ wav_file.writeframes(final_audio.tobytes())
330
+
331
+ def tts_command(args):
332
+ """
333
+ Executes the TTS command based on parsed arguments.
334
+
335
+ Args:
336
+ args: The parsed command-line arguments.
337
+ """
338
+ if console is None:
339
+ raise ImportError("Need cli tools to use TTS commands - install via `pip install sunholo[cli,tts]`")
340
+
341
+ from rich.panel import Panel
342
+
343
+ def text_generator(input_source: str, is_file: bool = False):
344
+ """Generate text from either a file or direct input."""
345
+ if is_file:
346
+ try:
347
+ with open(input_source, 'r') as f:
348
+ for line in f:
349
+ line = line.strip()
350
+ if line: # Skip empty lines
351
+ yield line
352
+ except FileNotFoundError:
353
+ console.print(f"Error: The input file '{input_source}' was not found.")
354
+ sys.exit(1)
355
+ else:
356
+ yield input_source
357
+
358
+ try:
359
+ tts = StreamingTTS()
360
+
361
+ # Configure TTS based on arguments
362
+ if args.language:
363
+ tts.set_language(args.language)
364
+ if args.voice_gender:
365
+ tts.set_voice_gender(args.voice_gender)
366
+ if args.sample_rate:
367
+ tts.sample_rate = args.sample_rate
368
+ if args.voice_name:
369
+ tts.set_voice(args.voice_name)
370
+
371
+ # Process the text
372
+ if args.action == 'speak':
373
+ console.print(
374
+ Panel((
375
+ f"Saying: {args.text}"
376
+ ),
377
+ title=f"Text to Speech",
378
+ subtitle=f"{tts.voice_name} is talking"),
379
+ )
380
+ tts.process_text_stream(
381
+ text_generator(args.text, is_file=args.file)
382
+ )
383
+ elif args.action == 'save':
384
+ if not args.output:
385
+ console.print("Error: Output file path is required for save action")
386
+ return
387
+
388
+ tts.save_to_file(
389
+ text_generator(args.text, is_file=args.file),
390
+ args.output
391
+ )
392
+
393
+ console.rule("Successfully processed text-to-speech request.")
394
+
395
+ except Exception as e:
396
+ console.print(f"[bold red]Error processing text-to-speech: {str(e)}[/bold red]")
397
+ return
398
+
399
+ def setup_tts_subparser(subparsers):
400
+ """
401
+ Sets up an argparse subparser for the 'tts' command.
402
+
403
+ Args:
404
+ subparsers: The subparsers object from argparse.ArgumentParser().
405
+ """
406
+ # TTS main parser
407
+ tts_parser = subparsers.add_parser('tts', help='Text-to-Speech conversion utilities')
408
+ tts_subparsers = tts_parser.add_subparsers(dest='action', help='TTS subcommands')
409
+
410
+ # Common arguments for both speak and save commands
411
+ common_args = argparse.ArgumentParser(add_help=False)
412
+ common_args.add_argument('text', help='Text to convert to speech (or file path if --file is used)')
413
+ common_args.add_argument('--file', action='store_true',
414
+ help='Treat the text argument as a file path')
415
+ common_args.add_argument('--language', default='en-GB',
416
+ help='Language code (e.g., en-US, es-ES)')
417
+ common_args.add_argument('--voice-gender', choices=['NEUTRAL', 'MALE', 'FEMALE'],
418
+ default='NEUTRAL', help='Voice gender to use')
419
+ common_args.add_argument('--sample-rate', type=int, default=24000,
420
+ help='Audio sample rate in Hz')
421
+ 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')
422
+
423
+ # Speak command - converts text to speech and plays it
424
+ speak_parser = tts_subparsers.add_parser('speak',
425
+ help='Convert text to speech and play it',
426
+ parents=[common_args])
427
+ speak_parser.set_defaults(func=tts_command)
428
+
429
+ # Save command - converts text to speech and saves to file
430
+ save_parser = tts_subparsers.add_parser('save',
431
+ help='Convert text to speech and save to file',
432
+ parents=[common_args])
433
+ save_parser.add_argument('--output', default='audio.wav',
434
+ help='Output audio file path (.wav)')
435
+ save_parser.set_defaults(func=tts_command)
436
+
437
+ # Set the default function for the TTS parser
438
+ tts_parser.set_defaults(func=lambda args: tts_parser.print_help())
439
+
440
+
@@ -1,7 +1,7 @@
1
1
  try:
2
- import hcl2
2
+ from hcl2 import load as hcl2_load
3
3
  except ImportError:
4
- hcl2 = None
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 hcl2 is None:
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 hcl2.load(file)
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.105.8
3
+ Version: 0.106.1
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.105.8.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.106.1.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=lLuVyilzmDbTaiAptR8SZzpbUNsgwHFsp4Ejbr5EApI,1136
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=q-ATmnWnc_PPo2q31xhqpp-m3GtOSBoqh0gvMeSo4XU,4374
47
- sunholo/cli/cli_init.py,sha256=9-w-7OW1yl5QRedkKg_W_-LuPia9t79QsSWq_4vUG6A,8298
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
@@ -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=JmHxhfrm97sIPIdb28n5BxcPsAPFgkKZGy6xNbclFtw,16832
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=F0NTtiYrCgYpfhPVcC2C5iNicmUmKHt477mBHZPHeyA,13102
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.105.8.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
151
- sunholo-0.105.8.dist-info/METADATA,sha256=QYG1Tm97SYn07_IJGWxlBdsFTqBkFyXu9ffDi2RZOAs,8312
152
- sunholo-0.105.8.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
153
- sunholo-0.105.8.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
154
- sunholo-0.105.8.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
155
- sunholo-0.105.8.dist-info/RECORD,,
152
+ sunholo-0.106.1.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
153
+ sunholo-0.106.1.dist-info/METADATA,sha256=LT-NrNweknkdB5XPQFE9yEPQ6Z421flnTD4utV8MjEM,8670
154
+ sunholo-0.106.1.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
155
+ sunholo-0.106.1.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
156
+ sunholo-0.106.1.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
157
+ sunholo-0.106.1.dist-info/RECORD,,