sunholo 0.105.7__tar.gz → 0.106.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.
Files changed (166) hide show
  1. {sunholo-0.105.7 → sunholo-0.106.0}/PKG-INFO +10 -2
  2. {sunholo-0.105.7 → sunholo-0.106.0}/setup.py +11 -1
  3. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/__init__.py +2 -0
  4. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/cli.py +3 -0
  5. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/cli_init.py +16 -17
  6. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/genai/process_funcs_cls.py +2 -0
  7. sunholo-0.106.0/sunholo/senses/__init__.py +1 -0
  8. sunholo-0.106.0/sunholo/senses/stream_voice.py +414 -0
  9. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/terraform/tfvars_editor.py +5 -5
  10. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo.egg-info/PKG-INFO +10 -2
  11. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo.egg-info/SOURCES.txt +2 -0
  12. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo.egg-info/requires.txt +9 -0
  13. {sunholo-0.105.7 → sunholo-0.106.0}/LICENSE.txt +0 -0
  14. {sunholo-0.105.7 → sunholo-0.106.0}/MANIFEST.in +0 -0
  15. {sunholo-0.105.7 → sunholo-0.106.0}/README.md +0 -0
  16. {sunholo-0.105.7 → sunholo-0.106.0}/setup.cfg +0 -0
  17. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/__init__.py +0 -0
  18. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/chat_history.py +0 -0
  19. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/dispatch_to_qa.py +0 -0
  20. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/fastapi/__init__.py +0 -0
  21. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/fastapi/base.py +0 -0
  22. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/fastapi/qna_routes.py +0 -0
  23. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/flask/__init__.py +0 -0
  24. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/flask/base.py +0 -0
  25. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/flask/qna_routes.py +0 -0
  26. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/flask/vac_routes.py +0 -0
  27. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/langserve.py +0 -0
  28. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/pubsub.py +0 -0
  29. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/route.py +0 -0
  30. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/special_commands.py +0 -0
  31. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/agents/swagger.py +0 -0
  32. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/archive/__init__.py +0 -0
  33. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/archive/archive.py +0 -0
  34. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/auth/__init__.py +0 -0
  35. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/auth/gcloud.py +0 -0
  36. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/auth/refresh.py +0 -0
  37. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/auth/run.py +0 -0
  38. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/azure/__init__.py +0 -0
  39. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/azure/auth.py +0 -0
  40. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/azure/blobs.py +0 -0
  41. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/azure/event_grid.py +0 -0
  42. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/bots/__init__.py +0 -0
  43. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/bots/discord.py +0 -0
  44. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/bots/github_webhook.py +0 -0
  45. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/bots/webapp.py +0 -0
  46. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/__init__.py +0 -0
  47. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/azure.py +0 -0
  48. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/doc_handling.py +0 -0
  49. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/encode_metadata.py +0 -0
  50. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/images.py +0 -0
  51. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/loaders.py +0 -0
  52. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/message_data.py +0 -0
  53. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/pdfs.py +0 -0
  54. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/process_chunker_data.py +0 -0
  55. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/publish.py +0 -0
  56. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/pubsub.py +0 -0
  57. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/chunker/splitter.py +0 -0
  58. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/__init__.py +0 -0
  59. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/chat_vac.py +0 -0
  60. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/configs.py +0 -0
  61. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/deploy.py +0 -0
  62. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/embedder.py +0 -0
  63. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/merge_texts.py +0 -0
  64. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/run_proxy.py +0 -0
  65. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/sun_rich.py +0 -0
  66. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/swagger.py +0 -0
  67. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/cli/vertex.py +0 -0
  68. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/components/__init__.py +0 -0
  69. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/components/llm.py +0 -0
  70. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/components/retriever.py +0 -0
  71. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/components/vectorstore.py +0 -0
  72. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/custom_logging.py +0 -0
  73. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/__init__.py +0 -0
  74. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/alloydb.py +0 -0
  75. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/alloydb_client.py +0 -0
  76. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/database.py +0 -0
  77. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/lancedb.py +0 -0
  78. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/sql/sb/create_function.sql +0 -0
  79. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/sql/sb/create_function_time.sql +0 -0
  80. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/sql/sb/create_table.sql +0 -0
  81. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/sql/sb/delete_source_row.sql +0 -0
  82. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/sql/sb/return_sources.sql +0 -0
  83. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/sql/sb/setup.sql +0 -0
  84. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/static_dbs.py +0 -0
  85. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/database/uuid.py +0 -0
  86. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/discovery_engine/__init__.py +0 -0
  87. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/discovery_engine/chunker_handler.py +0 -0
  88. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/discovery_engine/create_new.py +0 -0
  89. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/discovery_engine/discovery_engine_client.py +0 -0
  90. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/discovery_engine/get_ai_search_chunks.py +0 -0
  91. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/embedder/__init__.py +0 -0
  92. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/embedder/embed_chunk.py +0 -0
  93. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/excel/__init__.py +0 -0
  94. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/excel/plugin.py +0 -0
  95. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/gcs/__init__.py +0 -0
  96. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/gcs/add_file.py +0 -0
  97. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/gcs/download_folder.py +0 -0
  98. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/gcs/download_url.py +0 -0
  99. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/gcs/extract_and_sign.py +0 -0
  100. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/gcs/metadata.py +0 -0
  101. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/genai/__init__.py +0 -0
  102. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/genai/images.py +0 -0
  103. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/genai/init.py +0 -0
  104. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/genai/safety.py +0 -0
  105. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/invoke/__init__.py +0 -0
  106. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/invoke/async_class.py +0 -0
  107. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/invoke/direct_vac_func.py +0 -0
  108. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/invoke/invoke_vac_utils.py +0 -0
  109. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/langfuse/__init__.py +0 -0
  110. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/langfuse/callback.py +0 -0
  111. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/langfuse/evals.py +0 -0
  112. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/langfuse/prompts.py +0 -0
  113. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/llamaindex/__init__.py +0 -0
  114. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/llamaindex/get_files.py +0 -0
  115. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/llamaindex/import_files.py +0 -0
  116. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/llamaindex/llamaindex_class.py +0 -0
  117. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/llamaindex/user_history.py +0 -0
  118. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/lookup/__init__.py +0 -0
  119. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/lookup/model_lookup.yaml +0 -0
  120. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/patches/__init__.py +0 -0
  121. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/patches/langchain/__init__.py +0 -0
  122. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/patches/langchain/lancedb.py +0 -0
  123. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/patches/langchain/vertexai.py +0 -0
  124. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/pubsub/__init__.py +0 -0
  125. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/pubsub/process_pubsub.py +0 -0
  126. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/pubsub/pubsub_manager.py +0 -0
  127. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/qna/__init__.py +0 -0
  128. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/qna/parsers.py +0 -0
  129. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/qna/retry.py +0 -0
  130. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/streaming/__init__.py +0 -0
  131. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/streaming/content_buffer.py +0 -0
  132. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/streaming/langserve.py +0 -0
  133. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/streaming/stream_lookup.py +0 -0
  134. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/streaming/streaming.py +0 -0
  135. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/summarise/__init__.py +0 -0
  136. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/summarise/summarise.py +0 -0
  137. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/terraform/__init__.py +0 -0
  138. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/tools/__init__.py +0 -0
  139. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/tools/web_browser.py +0 -0
  140. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/__init__.py +0 -0
  141. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/api_key.py +0 -0
  142. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/big_context.py +0 -0
  143. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/config.py +0 -0
  144. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/config_class.py +0 -0
  145. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/config_schema.py +0 -0
  146. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/gcp.py +0 -0
  147. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/gcp_project.py +0 -0
  148. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/mime.py +0 -0
  149. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/parsers.py +0 -0
  150. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/timedelta.py +0 -0
  151. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/user_ids.py +0 -0
  152. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/utils/version.py +0 -0
  153. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/vertex/__init__.py +0 -0
  154. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/vertex/extensions_call.py +0 -0
  155. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/vertex/extensions_class.py +0 -0
  156. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/vertex/genai_functions.py +0 -0
  157. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/vertex/init.py +0 -0
  158. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/vertex/memory_tools.py +0 -0
  159. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/vertex/safety.py +0 -0
  160. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo/vertex/type_dict_to_json.py +0 -0
  161. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo.egg-info/dependency_links.txt +0 -0
  162. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo.egg-info/entry_points.txt +0 -0
  163. {sunholo-0.105.7 → sunholo-0.106.0}/sunholo.egg-info/top_level.txt +0 -0
  164. {sunholo-0.105.7 → sunholo-0.106.0}/tests/test_async.py +0 -0
  165. {sunholo-0.105.7 → sunholo-0.106.0}/tests/test_chat_history.py +0 -0
  166. {sunholo-0.105.7 → sunholo-0.106.0}/tests/test_config.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.105.7
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.105.7.tar.gz
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"
@@ -113,6 +116,7 @@ Requires-Dist: google-cloud-storage; extra == "gcp"
113
116
  Requires-Dist: google-cloud-logging; extra == "gcp"
114
117
  Requires-Dist: google-cloud-pubsub; extra == "gcp"
115
118
  Requires-Dist: google-cloud-discoveryengine; extra == "gcp"
119
+ Requires-Dist: google-cloud-texttospeech; extra == "gcp"
116
120
  Requires-Dist: google-generativeai>=0.7.1; extra == "gcp"
117
121
  Requires-Dist: langchain-google-genai==1.0.10; extra == "gcp"
118
122
  Requires-Dist: langchain_google_alloydb_pg>=0.2.2; extra == "gcp"
@@ -142,6 +146,10 @@ Requires-Dist: requests; extra == "excel"
142
146
  Requires-Dist: rich; extra == "excel"
143
147
  Provides-Extra: iac
144
148
  Requires-Dist: python-hcl2; extra == "iac"
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,6 +1,6 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
- version = '0.105.7'
3
+ version = '0.106.0'
4
4
 
5
5
  setup(
6
6
  name='sunholo',
@@ -55,6 +55,8 @@ setup(
55
55
  "google-cloud-storage",
56
56
  "google-cloud-pubsub",
57
57
  "google-cloud-discoveryengine",
58
+ "google-cloud-texttospeech",
59
+
58
60
  "google-generativeai>=0.7.1",
59
61
  "gunicorn",
60
62
  "httpcore",
@@ -70,6 +72,7 @@ setup(
70
72
  "langchain-anthropic==0.1.23",
71
73
  "langchain-google-vertexai",
72
74
  "langfuse",
75
+ "numpy",
73
76
  "pg8000",
74
77
  "pgvector",
75
78
  "pillow",
@@ -81,6 +84,7 @@ setup(
81
84
  "python-socketio",
82
85
  "pytesseract",
83
86
  "rich",
87
+ "sounddevice",
84
88
  "supabase",
85
89
  "tabulate",
86
90
  "tantivy",
@@ -130,6 +134,7 @@ setup(
130
134
  "google-cloud-logging",
131
135
  "google-cloud-pubsub",
132
136
  "google-cloud-discoveryengine",
137
+ "google-cloud-texttospeech",
133
138
  "google-generativeai>=0.7.1",
134
139
  "langchain-google-genai==1.0.10",
135
140
  "langchain_google_alloydb_pg>=0.2.2",
@@ -166,6 +171,11 @@ setup(
166
171
  ],
167
172
  'iac':[
168
173
  'python-hcl2'
174
+ ],
175
+ 'tts':[
176
+ 'google-cloud-texttospeech',
177
+ 'numpy',
178
+ 'sounddevice',
169
179
  ]
170
180
 
171
181
  },
@@ -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',
@@ -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
 
@@ -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
 
@@ -513,6 +513,8 @@ class GenAIFunctionProcessor:
513
513
  else:
514
514
  gen.end(output="No response received") if gen else None
515
515
 
516
+ if not response:
517
+ response = []
516
518
  for chunk in response:
517
519
  if not chunk:
518
520
  continue
@@ -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 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
  """