sunholo 0.105.8__tar.gz → 0.106.1__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.
- {sunholo-0.105.8 → sunholo-0.106.1}/PKG-INFO +10 -2
- {sunholo-0.105.8 → sunholo-0.106.1}/setup.py +11 -1
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/__init__.py +2 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/cli.py +3 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/cli_init.py +16 -17
- sunholo-0.106.1/sunholo/senses/__init__.py +1 -0
- sunholo-0.106.1/sunholo/senses/stream_voice.py +440 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/terraform/tfvars_editor.py +5 -5
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo.egg-info/PKG-INFO +10 -2
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo.egg-info/SOURCES.txt +2 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo.egg-info/requires.txt +9 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/LICENSE.txt +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/MANIFEST.in +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/README.md +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/setup.cfg +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/chat_history.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/dispatch_to_qa.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/fastapi/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/fastapi/base.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/fastapi/qna_routes.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/flask/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/flask/base.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/flask/qna_routes.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/flask/vac_routes.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/langserve.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/pubsub.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/route.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/special_commands.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/agents/swagger.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/archive/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/archive/archive.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/auth/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/auth/gcloud.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/auth/refresh.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/auth/run.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/azure/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/azure/auth.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/azure/blobs.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/azure/event_grid.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/bots/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/bots/discord.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/bots/github_webhook.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/bots/webapp.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/azure.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/doc_handling.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/encode_metadata.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/images.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/loaders.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/message_data.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/pdfs.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/process_chunker_data.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/publish.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/pubsub.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/chunker/splitter.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/chat_vac.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/configs.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/deploy.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/embedder.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/merge_texts.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/run_proxy.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/sun_rich.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/swagger.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/cli/vertex.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/components/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/components/llm.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/components/retriever.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/components/vectorstore.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/custom_logging.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/alloydb.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/alloydb_client.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/database.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/lancedb.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/sql/sb/create_function.sql +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/sql/sb/create_function_time.sql +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/sql/sb/create_table.sql +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/sql/sb/delete_source_row.sql +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/sql/sb/return_sources.sql +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/sql/sb/setup.sql +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/static_dbs.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/database/uuid.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/discovery_engine/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/discovery_engine/chunker_handler.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/discovery_engine/create_new.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/discovery_engine/discovery_engine_client.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/discovery_engine/get_ai_search_chunks.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/embedder/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/embedder/embed_chunk.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/excel/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/excel/plugin.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/gcs/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/gcs/add_file.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/gcs/download_folder.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/gcs/download_url.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/gcs/extract_and_sign.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/gcs/metadata.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/genai/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/genai/images.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/genai/init.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/genai/process_funcs_cls.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/genai/safety.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/invoke/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/invoke/async_class.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/invoke/direct_vac_func.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/invoke/invoke_vac_utils.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/langfuse/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/langfuse/callback.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/langfuse/evals.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/langfuse/prompts.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/llamaindex/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/llamaindex/get_files.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/llamaindex/import_files.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/llamaindex/llamaindex_class.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/llamaindex/user_history.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/lookup/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/lookup/model_lookup.yaml +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/patches/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/patches/langchain/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/patches/langchain/lancedb.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/patches/langchain/vertexai.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/pubsub/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/pubsub/process_pubsub.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/pubsub/pubsub_manager.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/qna/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/qna/parsers.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/qna/retry.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/streaming/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/streaming/content_buffer.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/streaming/langserve.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/streaming/stream_lookup.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/streaming/streaming.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/summarise/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/summarise/summarise.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/terraform/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/tools/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/tools/web_browser.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/api_key.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/big_context.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/config.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/config_class.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/config_schema.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/gcp.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/gcp_project.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/mime.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/parsers.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/timedelta.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/user_ids.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/utils/version.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/vertex/__init__.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/vertex/extensions_call.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/vertex/extensions_class.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/vertex/genai_functions.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/vertex/init.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/vertex/memory_tools.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/vertex/safety.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo/vertex/type_dict_to_json.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo.egg-info/dependency_links.txt +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo.egg-info/entry_points.txt +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/sunholo.egg-info/top_level.txt +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/tests/test_async.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/tests/test_chat_history.py +0 -0
- {sunholo-0.105.8 → sunholo-0.106.1}/tests/test_config.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sunholo
|
|
3
|
-
Version: 0.
|
|
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.
|
|
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"
|
|
@@ -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.
|
|
3
|
+
version = '0.106.1'
|
|
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"
|
|
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,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
|
|
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
|
"""
|