solace-agent-mesh 1.6.1__py3-none-any.whl → 1.13.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/agent/adk/alembic/README +74 -0
- solace_agent_mesh/agent/adk/alembic/env.py +77 -0
- solace_agent_mesh/agent/adk/alembic/script.py.mako +28 -0
- solace_agent_mesh/agent/adk/alembic/versions/e2902798564d_adk_session_db_upgrade.py +52 -0
- solace_agent_mesh/agent/adk/alembic.ini +112 -0
- solace_agent_mesh/agent/adk/app_llm_agent.py +26 -0
- solace_agent_mesh/agent/adk/artifacts/filesystem_artifact_service.py +165 -1
- solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +163 -0
- solace_agent_mesh/agent/adk/callbacks.py +852 -109
- solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +234 -36
- solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +52 -5
- solace_agent_mesh/agent/adk/mcp_content_processor.py +1 -1
- solace_agent_mesh/agent/adk/models/lite_llm.py +77 -21
- solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +24 -137
- solace_agent_mesh/agent/adk/runner.py +85 -20
- solace_agent_mesh/agent/adk/schema_migration.py +88 -0
- solace_agent_mesh/agent/adk/services.py +94 -18
- solace_agent_mesh/agent/adk/setup.py +281 -65
- solace_agent_mesh/agent/adk/stream_parser.py +231 -37
- solace_agent_mesh/agent/adk/tool_wrapper.py +3 -0
- solace_agent_mesh/agent/protocol/event_handlers.py +472 -137
- solace_agent_mesh/agent/proxies/a2a/app.py +3 -2
- solace_agent_mesh/agent/proxies/a2a/component.py +572 -75
- solace_agent_mesh/agent/proxies/a2a/config.py +80 -4
- solace_agent_mesh/agent/proxies/base/app.py +3 -2
- solace_agent_mesh/agent/proxies/base/component.py +188 -22
- solace_agent_mesh/agent/proxies/base/proxy_task_context.py +3 -1
- solace_agent_mesh/agent/sac/app.py +91 -3
- solace_agent_mesh/agent/sac/component.py +591 -157
- solace_agent_mesh/agent/sac/patch_adk.py +8 -16
- solace_agent_mesh/agent/sac/task_execution_context.py +146 -4
- solace_agent_mesh/agent/tools/__init__.py +3 -0
- solace_agent_mesh/agent/tools/audio_tools.py +3 -3
- solace_agent_mesh/agent/tools/builtin_artifact_tools.py +710 -171
- solace_agent_mesh/agent/tools/deep_research_tools.py +2161 -0
- solace_agent_mesh/agent/tools/dynamic_tool.py +2 -0
- solace_agent_mesh/agent/tools/peer_agent_tool.py +82 -15
- solace_agent_mesh/agent/tools/time_tools.py +126 -0
- solace_agent_mesh/agent/tools/tool_config_types.py +57 -2
- solace_agent_mesh/agent/tools/web_search_tools.py +279 -0
- solace_agent_mesh/agent/tools/web_tools.py +125 -17
- solace_agent_mesh/agent/utils/artifact_helpers.py +248 -6
- solace_agent_mesh/agent/utils/context_helpers.py +17 -0
- solace_agent_mesh/assets/docs/404.html +6 -6
- solace_agent_mesh/assets/docs/assets/css/{styles.906a1503.css → styles.8162edfb.css} +1 -1
- solace_agent_mesh/assets/docs/assets/js/05749d90.19ac4f35.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/15ba94aa.e186750d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/15e40e79.434bb30f.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/17896441.e612dfb4.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2279.550aa580.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/{17896441.a5e82f9b.js.LICENSE.txt → 2279.550aa580.js.LICENSE.txt} +6 -0
- solace_agent_mesh/assets/docs/assets/js/240a0364.83e37aa8.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2987107d.a80604f9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2e32b5e0.2f0db237.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3a6c6137.7e61915d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3ac1795d.7f7ab1c1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3ff0015d.e53c9b78.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/41adc471.0e95b87c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4667dc50.bf2ad456.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/49eed117.493d6f99.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{509e993c.4c7a1a6d.js → 509e993c.a1fbf45a.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/547e15cc.8e6da617.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/55b7b518.29d6e75d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5b8d9c11.d4eb37b8.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5c2bd65f.1ee87753.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/60702c0e.a8bdd79b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/631738c7.fa471607.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/64195356.09dbd087.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/66d4869e.30340bd3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6a520c9d.b6e3f2ce.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6aaedf65.7253541d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.a5b36a60.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6d84eae0.fd23ba4a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/71da7b71.374b9d54.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/729898df.7249e9fd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/7e294c01.7c5f6906.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8024126c.e3467286.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/81a99df0.7ed65d45.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/82fbfb93.161823a5.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8b032486.91a91afc.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/924ffdeb.975e428a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/94e8668d.16083b3f.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9bb13469.4523ae20.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/a7d42657.a956689d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/a94703ab.3e5fbcb3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ab9708a8.3e563275.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ad87452a.9d73dad6.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/c93cbaa0.0e0d8baf.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cab03b5b.6a073091.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cbe2e9ea.07e170dd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/da0b5bad.b62f7b08.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/dd817ffc.c37a755e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/dd81e2b8.b682e9c2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/de915948.44a432bc.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e04b235d.06d23db6.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e1b6eeb4.deb2b62e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e3d9abda.1476f570.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e6f9706b.acc800d3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e92d0134.c147a429.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ee0c2fe7.94d0a351.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f284c35a.cc97854c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.74710fc1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/main.d634009f.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/runtime~main.27bb82a7.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +68 -68
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +50 -50
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +42 -42
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +55 -55
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +82 -68
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/image-tools/index.html +81 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +67 -50
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/research-tools/index.html +136 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +178 -144
- solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +43 -42
- solace_agent_mesh/assets/docs/docs/documentation/components/index.html +20 -18
- solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +23 -23
- solace_agent_mesh/assets/docs/docs/documentation/components/platform-service/index.html +33 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +45 -45
- solace_agent_mesh/assets/docs/docs/documentation/components/projects/index.html +182 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/prompts/index.html +147 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +208 -125
- solace_agent_mesh/assets/docs/docs/documentation/components/speech/index.html +52 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +28 -49
- solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +29 -30
- solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +14 -14
- solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/index.html +47 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/kubernetes-deployment-guide/index.html +197 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +90 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +17 -16
- solace_agent_mesh/assets/docs/docs/documentation/deploying/proxy_configuration/index.html +49 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +38 -38
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +162 -171
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +67 -49
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +17 -17
- solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +51 -51
- solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +22 -22
- solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +27 -27
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +135 -135
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +66 -66
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +51 -51
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +50 -38
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +86 -86
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +51 -51
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +24 -24
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +30 -30
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +44 -44
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/teams-integration/index.html +115 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/agent-builder/index.html +86 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/connectors/index.html +67 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +23 -19
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +40 -37
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/openapi-tools/index.html +324 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +112 -87
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/secure-user-delegated-access/index.html +440 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +87 -64
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/wheel-installation/index.html +62 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +44 -44
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +39 -37
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +30 -30
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +18 -18
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/vibe_coding/index.html +62 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/artifact-storage/index.html +311 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +39 -42
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +14 -14
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +27 -25
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +69 -69
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +72 -72
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/session-storage/index.html +251 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/user-feedback/index.html +88 -0
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +42 -42
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +20 -20
- solace_agent_mesh/assets/docs/docs/documentation/migrations/platform-service-split/index.html +85 -0
- solace_agent_mesh/assets/docs/lunr-index-1768329217460.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1768329217460.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/assets/docs/sitemap.xml +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/add_cmd/__init__.py +3 -1
- solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +6 -1
- solace_agent_mesh/cli/commands/add_cmd/proxy_cmd.py +100 -0
- solace_agent_mesh/cli/commands/docs_cmd.py +4 -1
- solace_agent_mesh/cli/commands/eval_cmd.py +1 -1
- solace_agent_mesh/cli/commands/init_cmd/__init__.py +15 -0
- solace_agent_mesh/cli/commands/init_cmd/directory_step.py +1 -1
- solace_agent_mesh/cli/commands/init_cmd/env_step.py +30 -3
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +3 -4
- solace_agent_mesh/cli/commands/init_cmd/platform_service_step.py +85 -0
- solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +16 -3
- solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +2 -1
- solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +1 -0
- solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +3 -3
- solace_agent_mesh/cli/commands/run_cmd.py +64 -49
- solace_agent_mesh/cli/commands/tools_cmd.py +315 -0
- solace_agent_mesh/cli/main.py +15 -0
- solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-BTf6dqwp.js → authCallback-KnKMP_vb.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/client-DpBL2stg.js +25 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-Cd498TV2.js +435 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-rSf8Vu29.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/vendor-CGk8Suyh.js +565 -0
- solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
- solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
- solace_agent_mesh/client/webui/frontend/static/mockServiceWorker.js +336 -0
- solace_agent_mesh/client/webui/frontend/static/ui-version.json +6 -0
- solace_agent_mesh/common/a2a/events.py +2 -1
- solace_agent_mesh/common/a2a/protocol.py +5 -0
- solace_agent_mesh/common/a2a/types.py +2 -1
- solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +23 -6
- solace_agent_mesh/common/a2a_spec/schemas/feedback_event.json +51 -0
- solace_agent_mesh/common/agent_registry.py +38 -11
- solace_agent_mesh/common/data_parts.py +144 -4
- solace_agent_mesh/common/error_handlers.py +83 -0
- solace_agent_mesh/common/exceptions.py +24 -0
- solace_agent_mesh/common/oauth/__init__.py +17 -0
- solace_agent_mesh/common/oauth/oauth_client.py +408 -0
- solace_agent_mesh/common/oauth/utils.py +50 -0
- solace_agent_mesh/common/rag_dto.py +156 -0
- solace_agent_mesh/common/sac/sam_component_base.py +97 -19
- solace_agent_mesh/common/sam_events/event_service.py +2 -2
- solace_agent_mesh/common/services/employee_service.py +1 -1
- solace_agent_mesh/common/utils/embeds/constants.py +1 -0
- solace_agent_mesh/common/utils/embeds/converter.py +1 -8
- solace_agent_mesh/common/utils/embeds/modifiers.py +4 -28
- solace_agent_mesh/common/utils/embeds/resolver.py +152 -31
- solace_agent_mesh/common/utils/embeds/types.py +9 -0
- solace_agent_mesh/common/utils/log_formatters.py +20 -0
- solace_agent_mesh/common/utils/mime_helpers.py +12 -5
- solace_agent_mesh/common/utils/pydantic_utils.py +90 -3
- solace_agent_mesh/common/utils/rbac_utils.py +69 -0
- solace_agent_mesh/common/utils/templates/__init__.py +8 -0
- solace_agent_mesh/common/utils/templates/liquid_renderer.py +210 -0
- solace_agent_mesh/common/utils/templates/template_resolver.py +161 -0
- solace_agent_mesh/config_portal/backend/common.py +12 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-CljP4_mv.js +103 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/{components-Rk0n-9cK.js → components-CaC6hG8d.js} +22 -22
- solace_agent_mesh/config_portal/frontend/static/client/assets/{entry.client-mvZjNKiz.js → entry.client-H_TM0YBt.js} +3 -3
- solace_agent_mesh/config_portal/frontend/static/client/assets/{index-DzNKzXrc.js → index-CnFykb2v.js} +16 -16
- solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-f8439d40.js +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-BIMqslJB.css +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-mJmTIdIk.js +10 -0
- solace_agent_mesh/config_portal/frontend/static/client/index.html +3 -3
- solace_agent_mesh/core_a2a/service.py +3 -2
- solace_agent_mesh/gateway/adapter/__init__.py +1 -0
- solace_agent_mesh/gateway/adapter/base.py +170 -0
- solace_agent_mesh/gateway/adapter/types.py +230 -0
- solace_agent_mesh/gateway/base/app.py +39 -2
- solace_agent_mesh/gateway/base/auth_interface.py +103 -0
- solace_agent_mesh/gateway/base/component.py +1027 -151
- solace_agent_mesh/gateway/generic/__init__.py +1 -0
- solace_agent_mesh/gateway/generic/app.py +50 -0
- solace_agent_mesh/gateway/generic/component.py +894 -0
- solace_agent_mesh/gateway/http_sse/alembic/env.py +0 -7
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_project_users_table.py +72 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_soft_delete_and_search.py +109 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_default_agent_to_projects.py +26 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_projects_table.py +135 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251108_create_prompt_tables_with_sharing.py +154 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251115_add_parent_task_id.py +32 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251126_add_background_task_fields.py +47 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251202_add_versioned_fields_to_prompts.py +52 -0
- solace_agent_mesh/gateway/http_sse/alembic.ini +0 -36
- solace_agent_mesh/gateway/http_sse/app.py +40 -11
- solace_agent_mesh/gateway/http_sse/component.py +285 -160
- solace_agent_mesh/gateway/http_sse/dependencies.py +149 -114
- solace_agent_mesh/gateway/http_sse/main.py +68 -450
- solace_agent_mesh/gateway/http_sse/repository/__init__.py +19 -1
- solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +2 -2
- solace_agent_mesh/gateway/http_sse/repository/entities/project.py +81 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/project_user.py +47 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/session.py +26 -3
- solace_agent_mesh/gateway/http_sse/repository/entities/task.py +7 -0
- solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +47 -0
- solace_agent_mesh/gateway/http_sse/repository/interfaces.py +114 -6
- solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +13 -0
- solace_agent_mesh/gateway/http_sse/repository/models/project_model.py +51 -0
- solace_agent_mesh/gateway/http_sse/repository/models/project_user_model.py +75 -0
- solace_agent_mesh/gateway/http_sse/repository/models/prompt_model.py +159 -0
- solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +8 -2
- solace_agent_mesh/gateway/http_sse/repository/models/task_model.py +8 -1
- solace_agent_mesh/gateway/http_sse/repository/project_repository.py +172 -0
- solace_agent_mesh/gateway/http_sse/repository/project_user_repository.py +186 -0
- solace_agent_mesh/gateway/http_sse/repository/session_repository.py +177 -11
- solace_agent_mesh/gateway/http_sse/repository/task_repository.py +86 -2
- solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +38 -7
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +256 -58
- solace_agent_mesh/gateway/http_sse/routers/auth.py +168 -134
- solace_agent_mesh/gateway/http_sse/routers/config.py +302 -8
- solace_agent_mesh/gateway/http_sse/routers/dto/project_dto.py +69 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/prompt_dto.py +255 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/project_requests.py +48 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +14 -1
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/base_responses.py +1 -1
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/project_responses.py +31 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +5 -2
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/version_responses.py +31 -0
- solace_agent_mesh/gateway/http_sse/routers/feedback.py +133 -2
- solace_agent_mesh/gateway/http_sse/routers/people.py +2 -2
- solace_agent_mesh/gateway/http_sse/routers/projects.py +768 -0
- solace_agent_mesh/gateway/http_sse/routers/prompts.py +1416 -0
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +167 -7
- solace_agent_mesh/gateway/http_sse/routers/speech.py +355 -0
- solace_agent_mesh/gateway/http_sse/routers/sse.py +131 -8
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +670 -18
- solace_agent_mesh/gateway/http_sse/routers/users.py +1 -1
- solace_agent_mesh/gateway/http_sse/routers/version.py +343 -0
- solace_agent_mesh/gateway/http_sse/routers/visualization.py +92 -9
- solace_agent_mesh/gateway/http_sse/services/audio_service.py +1227 -0
- solace_agent_mesh/gateway/http_sse/services/background_task_monitor.py +186 -0
- solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +1 -1
- solace_agent_mesh/gateway/http_sse/services/feedback_service.py +1 -1
- solace_agent_mesh/gateway/http_sse/services/project_service.py +930 -0
- solace_agent_mesh/gateway/http_sse/services/prompt_builder_assistant.py +303 -0
- solace_agent_mesh/gateway/http_sse/services/session_service.py +361 -12
- solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +354 -4
- solace_agent_mesh/gateway/http_sse/session_manager.py +15 -15
- solace_agent_mesh/gateway/http_sse/sse_manager.py +286 -166
- solace_agent_mesh/gateway/http_sse/utils/artifact_copy_utils.py +370 -0
- solace_agent_mesh/gateway/http_sse/utils/stim_utils.py +41 -1
- solace_agent_mesh/services/__init__.py +0 -0
- solace_agent_mesh/services/platform/__init__.py +29 -0
- solace_agent_mesh/services/platform/alembic/env.py +85 -0
- solace_agent_mesh/services/platform/alembic/script.py.mako +28 -0
- solace_agent_mesh/services/platform/alembic.ini +109 -0
- solace_agent_mesh/services/platform/api/__init__.py +3 -0
- solace_agent_mesh/services/platform/api/dependencies.py +154 -0
- solace_agent_mesh/services/platform/api/main.py +314 -0
- solace_agent_mesh/services/platform/api/middleware.py +51 -0
- solace_agent_mesh/services/platform/api/routers/__init__.py +33 -0
- solace_agent_mesh/services/platform/api/routers/health_router.py +31 -0
- solace_agent_mesh/services/platform/app.py +215 -0
- solace_agent_mesh/services/platform/component.py +777 -0
- solace_agent_mesh/shared/__init__.py +14 -0
- solace_agent_mesh/shared/api/__init__.py +42 -0
- solace_agent_mesh/shared/auth/__init__.py +26 -0
- solace_agent_mesh/shared/auth/dependencies.py +204 -0
- solace_agent_mesh/shared/auth/middleware.py +347 -0
- solace_agent_mesh/shared/database/__init__.py +20 -0
- solace_agent_mesh/{gateway/http_sse/shared → shared/database}/base_repository.py +1 -1
- solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_exceptions.py +1 -1
- solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_helpers.py +1 -1
- solace_agent_mesh/shared/exceptions/__init__.py +36 -0
- solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exception_handlers.py +19 -5
- solace_agent_mesh/shared/utils/__init__.py +21 -0
- solace_agent_mesh/templates/logging_config_template.yaml +48 -0
- solace_agent_mesh/templates/main_orchestrator.yaml +12 -1
- solace_agent_mesh/templates/platform.yaml +49 -0
- solace_agent_mesh/templates/plugin_readme_template.md +3 -25
- solace_agent_mesh/templates/plugin_tool_config_template.yaml +109 -0
- solace_agent_mesh/templates/proxy_template.yaml +62 -0
- solace_agent_mesh/templates/webui.yaml +148 -6
- solace_agent_mesh/tools/web_search/__init__.py +18 -0
- solace_agent_mesh/tools/web_search/base.py +84 -0
- solace_agent_mesh/tools/web_search/google_search.py +247 -0
- solace_agent_mesh/tools/web_search/models.py +99 -0
- {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/METADATA +31 -12
- solace_agent_mesh-1.13.2.dist-info/RECORD +591 -0
- {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/WHEEL +1 -1
- solace_agent_mesh/agent/adk/adk_llm.txt +0 -232
- solace_agent_mesh/agent/adk/adk_llm_detail.txt +0 -566
- solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +0 -171
- solace_agent_mesh/agent/adk/models/models_llm.txt +0 -142
- solace_agent_mesh/agent/agent_llm.txt +0 -378
- solace_agent_mesh/agent/agent_llm_detail.txt +0 -1702
- solace_agent_mesh/agent/protocol/protocol_llm.txt +0 -81
- solace_agent_mesh/agent/protocol/protocol_llm_detail.txt +0 -92
- solace_agent_mesh/agent/sac/sac_llm.txt +0 -189
- solace_agent_mesh/agent/sac/sac_llm_detail.txt +0 -200
- solace_agent_mesh/agent/testing/testing_llm.txt +0 -57
- solace_agent_mesh/agent/testing/testing_llm_detail.txt +0 -68
- solace_agent_mesh/agent/tools/tools_llm.txt +0 -263
- solace_agent_mesh/agent/tools/tools_llm_detail.txt +0 -274
- solace_agent_mesh/agent/utils/utils_llm.txt +0 -138
- solace_agent_mesh/agent/utils/utils_llm_detail.txt +0 -149
- solace_agent_mesh/assets/docs/assets/js/15ba94aa.932dd2db.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js +0 -2
- solace_agent_mesh/assets/docs/assets/js/240a0364.7eac6021.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/2e32b5e0.33f5d75b.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/3a6c6137.f5940cfa.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/3ac1795d.76654dd9.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/3ff0015d.2be20244.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/547e15cc.2cbb060a.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/55b7b518.f2b1d1ba.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/631738c7.a8b1ef8b.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/6a520c9d.ba015d81.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/6d84eae0.4a5fbf39.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/8024126c.56e59919.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/81a99df0.07034dd9.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/82fbfb93.139a1a1f.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/94e8668d.b5ddb7a1.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/9bb13469.dd1c9b54.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/a94703ab.0438dbc2.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/ab9708a8.3e6dd091.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/c93cbaa0.eaff365e.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/da0b5bad.d08a9466.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/dd817ffc.0aa9630a.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/dd81e2b8.d590bc9e.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e3d9abda.6b9493d0.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e92d0134.cf6d6522.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/main.b12eac43.js +0 -2
- solace_agent_mesh/assets/docs/assets/js/runtime~main.e268214e.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1761248203150.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1761248203150.json +0 -1
- solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +0 -250
- solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +0 -365
- solace_agent_mesh/cli/commands/plugin_cmd/plugin_cmd_llm.txt +0 -305
- solace_agent_mesh/client/webui/frontend/static/assets/client-CaY59VuC.js +0 -25
- solace_agent_mesh/client/webui/frontend/static/assets/main-B32noGmR.js +0 -342
- solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/vendor-BEmvJSYz.js +0 -405
- solace_agent_mesh/common/a2a/a2a_llm.txt +0 -182
- solace_agent_mesh/common/a2a/a2a_llm_detail.txt +0 -193
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +0 -407
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm_detail.txt +0 -736
- solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +0 -313
- solace_agent_mesh/common/common_llm.txt +0 -251
- solace_agent_mesh/common/common_llm_detail.txt +0 -2562
- solace_agent_mesh/common/middleware/middleware_llm.txt +0 -174
- solace_agent_mesh/common/middleware/middleware_llm_detail.txt +0 -185
- solace_agent_mesh/common/sac/sac_llm.txt +0 -71
- solace_agent_mesh/common/sac/sac_llm_detail.txt +0 -82
- solace_agent_mesh/common/sam_events/sam_events_llm.txt +0 -104
- solace_agent_mesh/common/sam_events/sam_events_llm_detail.txt +0 -115
- solace_agent_mesh/common/services/providers/providers_llm.txt +0 -80
- solace_agent_mesh/common/services/services_llm.txt +0 -363
- solace_agent_mesh/common/services/services_llm_detail.txt +0 -459
- solace_agent_mesh/common/utils/embeds/embeds_llm.txt +0 -220
- solace_agent_mesh/common/utils/utils_llm.txt +0 -336
- solace_agent_mesh/common/utils/utils_llm_detail.txt +0 -572
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +0 -98
- solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-61038fc6.js +0 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-BWvk5-gF.js +0 -10
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-DxRwaWiE.css +0 -1
- solace_agent_mesh/core_a2a/core_a2a_llm.txt +0 -90
- solace_agent_mesh/core_a2a/core_a2a_llm_detail.txt +0 -101
- solace_agent_mesh/gateway/base/base_llm.txt +0 -224
- solace_agent_mesh/gateway/base/base_llm_detail.txt +0 -235
- solace_agent_mesh/gateway/gateway_llm.txt +0 -373
- solace_agent_mesh/gateway/gateway_llm_detail.txt +0 -3885
- solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +0 -295
- solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +0 -155
- solace_agent_mesh/gateway/http_sse/components/components_llm.txt +0 -105
- solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +0 -299
- solace_agent_mesh/gateway/http_sse/http_sse_llm_detail.txt +0 -3278
- solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +0 -263
- solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +0 -266
- solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +0 -340
- solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +0 -346
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +0 -83
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +0 -107
- solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +0 -314
- solace_agent_mesh/gateway/http_sse/services/services_llm.txt +0 -297
- solace_agent_mesh/gateway/http_sse/shared/__init__.py +0 -146
- solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +0 -285
- solace_agent_mesh/gateway/http_sse/utils/utils_llm.txt +0 -47
- solace_agent_mesh/llm.txt +0 -228
- solace_agent_mesh/llm_detail.txt +0 -2835
- solace_agent_mesh/solace_agent_mesh_llm.txt +0 -362
- solace_agent_mesh/solace_agent_mesh_llm_detail.txt +0 -8599
- solace_agent_mesh/templates/logging_config_template.ini +0 -45
- solace_agent_mesh/templates/templates_llm.txt +0 -147
- solace_agent_mesh-1.6.1.dist-info/RECORD +0 -525
- /solace_agent_mesh/assets/docs/assets/js/{main.b12eac43.js.LICENSE.txt → main.d634009f.js.LICENSE.txt} +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/auth_utils.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/pagination.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/response_utils.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/error_dto.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exceptions.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/enums.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/timestamp_utils.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/types.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/utils.py +0 -0
- {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audio Service for Speech-to-Text and Text-to-Speech operations.
|
|
3
|
+
Bridges gateway endpoints and external speech APIs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import io
|
|
8
|
+
import tempfile
|
|
9
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional
|
|
10
|
+
from fastapi import UploadFile, HTTPException
|
|
11
|
+
from solace_ai_connector.common.log import log
|
|
12
|
+
|
|
13
|
+
from ....agent.tools.audio_tools import ALL_AVAILABLE_VOICES
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# AWS Polly Neural Voices (popular subset)
|
|
17
|
+
AWS_POLLY_NEURAL_VOICES = [
|
|
18
|
+
# US English - Neural
|
|
19
|
+
"Joanna",
|
|
20
|
+
"Matthew",
|
|
21
|
+
"Ruth",
|
|
22
|
+
"Stephen",
|
|
23
|
+
"Kendra",
|
|
24
|
+
"Joey",
|
|
25
|
+
"Kimberly",
|
|
26
|
+
"Salli",
|
|
27
|
+
"Ivy",
|
|
28
|
+
# UK English - Neural
|
|
29
|
+
"Amy",
|
|
30
|
+
"Emma",
|
|
31
|
+
"Brian",
|
|
32
|
+
"Arthur",
|
|
33
|
+
# Australian English
|
|
34
|
+
"Olivia",
|
|
35
|
+
# Canadian English
|
|
36
|
+
"Liam",
|
|
37
|
+
# Indian English
|
|
38
|
+
"Kajal",
|
|
39
|
+
"Aria",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Azure Neural Voices (popular subset with HD voices)
|
|
43
|
+
AZURE_NEURAL_VOICES = [
|
|
44
|
+
# US English - HD Voices
|
|
45
|
+
"en-US-Andrew:DragonHDLatestNeural",
|
|
46
|
+
"en-US-Ava:DragonHDLatestNeural",
|
|
47
|
+
"en-US-Brian:DragonHDLatestNeural",
|
|
48
|
+
"en-US-Emma:DragonHDLatestNeural",
|
|
49
|
+
# US English - Standard
|
|
50
|
+
"en-US-JennyNeural",
|
|
51
|
+
"en-US-GuyNeural",
|
|
52
|
+
"en-US-AriaNeural",
|
|
53
|
+
"en-US-DavisNeural",
|
|
54
|
+
"en-US-JaneNeural",
|
|
55
|
+
"en-US-JasonNeural",
|
|
56
|
+
"en-US-NancyNeural",
|
|
57
|
+
"en-US-TonyNeural",
|
|
58
|
+
"en-US-SaraNeural",
|
|
59
|
+
"en-US-AmberNeural",
|
|
60
|
+
"en-US-AnaNeural",
|
|
61
|
+
"en-US-AndrewNeural",
|
|
62
|
+
"en-US-AshleyNeural",
|
|
63
|
+
"en-US-BrandonNeural",
|
|
64
|
+
"en-US-ChristopherNeural",
|
|
65
|
+
"en-US-CoraNeural",
|
|
66
|
+
"en-US-ElizabethNeural",
|
|
67
|
+
"en-US-EricNeural",
|
|
68
|
+
"en-US-JacobNeural",
|
|
69
|
+
"en-US-MichelleNeural",
|
|
70
|
+
"en-US-MonicaNeural",
|
|
71
|
+
"en-US-RogerNeural",
|
|
72
|
+
"en-US-SteffanNeural",
|
|
73
|
+
# UK English
|
|
74
|
+
"en-GB-LibbyNeural",
|
|
75
|
+
"en-GB-RyanNeural",
|
|
76
|
+
"en-GB-SoniaNeural",
|
|
77
|
+
"en-GB-MiaNeural",
|
|
78
|
+
"en-GB-AlfieNeural",
|
|
79
|
+
"en-GB-BellaNeural",
|
|
80
|
+
"en-GB-ElliotNeural",
|
|
81
|
+
"en-GB-EthanNeural",
|
|
82
|
+
"en-GB-HollieNeural",
|
|
83
|
+
"en-GB-OliverNeural",
|
|
84
|
+
"en-GB-OliviaNeural",
|
|
85
|
+
"en-GB-ThomasNeural",
|
|
86
|
+
# Australian English
|
|
87
|
+
"en-AU-NatashaNeural",
|
|
88
|
+
"en-AU-WilliamNeural",
|
|
89
|
+
"en-AU-AnnetteNeural",
|
|
90
|
+
"en-AU-CarlyNeural",
|
|
91
|
+
"en-AU-DarrenNeural",
|
|
92
|
+
"en-AU-DuncanNeural",
|
|
93
|
+
"en-AU-ElsieNeural",
|
|
94
|
+
"en-AU-FreyaNeural",
|
|
95
|
+
"en-AU-JoanneNeural",
|
|
96
|
+
"en-AU-KenNeural",
|
|
97
|
+
"en-AU-KimNeural",
|
|
98
|
+
"en-AU-NeilNeural",
|
|
99
|
+
"en-AU-TimNeural",
|
|
100
|
+
"en-AU-TinaNeural",
|
|
101
|
+
# Canadian English
|
|
102
|
+
"en-CA-ClaraNeural",
|
|
103
|
+
"en-CA-LiamNeural",
|
|
104
|
+
# Indian English
|
|
105
|
+
"en-IN-NeerjaNeural",
|
|
106
|
+
"en-IN-PrabhatNeural",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TranscriptionResult:
|
|
111
|
+
"""Result of audio transcription"""
|
|
112
|
+
def __init__(self, text: str, language: str = "en", duration: float = 0.0):
|
|
113
|
+
self.text = text
|
|
114
|
+
self.language = language
|
|
115
|
+
self.duration = duration
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
118
|
+
return {
|
|
119
|
+
"text": self.text,
|
|
120
|
+
"language": self.language,
|
|
121
|
+
"duration": self.duration
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class AudioService:
|
|
126
|
+
"""
|
|
127
|
+
Service layer for audio operations.
|
|
128
|
+
Bridges gateway endpoints and agent audio tools.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(self, config: Dict[str, Any]):
|
|
132
|
+
"""
|
|
133
|
+
Initialize AudioService with configuration.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
config: Configuration dictionary containing speech settings
|
|
137
|
+
"""
|
|
138
|
+
self.config = config
|
|
139
|
+
self.speech_config = config.get("speech", {})
|
|
140
|
+
|
|
141
|
+
async def transcribe_audio_openai(
|
|
142
|
+
self,
|
|
143
|
+
temp_path: str,
|
|
144
|
+
user_id: str,
|
|
145
|
+
session_id: str,
|
|
146
|
+
app_name: str = "webui",
|
|
147
|
+
language: Optional[str] = None
|
|
148
|
+
) -> TranscriptionResult:
|
|
149
|
+
"""
|
|
150
|
+
Transcribe audio using OpenAI Whisper API.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
temp_path: Path to temporary audio file
|
|
154
|
+
user_id: User identifier
|
|
155
|
+
session_id: Session identifier
|
|
156
|
+
app_name: Application name
|
|
157
|
+
language: Optional language code (e.g., "en", "es", "fr")
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
TranscriptionResult with transcribed text
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
HTTPException: If transcription fails
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
import httpx
|
|
167
|
+
import os
|
|
168
|
+
|
|
169
|
+
stt_config = self.speech_config.get("stt", {})
|
|
170
|
+
openai_config = stt_config.get("openai", stt_config) # Fallback to root for backward compat
|
|
171
|
+
|
|
172
|
+
api_url = openai_config.get("url", "https://api.openai.com/v1/audio/transcriptions")
|
|
173
|
+
api_key = openai_config.get("api_key", "")
|
|
174
|
+
model = openai_config.get("model", "whisper-1")
|
|
175
|
+
|
|
176
|
+
if not api_key:
|
|
177
|
+
raise HTTPException(500, "OpenAI STT API key not configured")
|
|
178
|
+
|
|
179
|
+
# Read the audio file
|
|
180
|
+
with open(temp_path, "rb") as audio_file:
|
|
181
|
+
audio_data = audio_file.read()
|
|
182
|
+
|
|
183
|
+
# Prepare multipart form data
|
|
184
|
+
files = {
|
|
185
|
+
"file": (os.path.basename(temp_path), audio_data, "audio/webm"),
|
|
186
|
+
}
|
|
187
|
+
data = {
|
|
188
|
+
"model": model,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Add language parameter if provided (OpenAI expects ISO-639-1 code like "en", "es")
|
|
192
|
+
if language:
|
|
193
|
+
# Convert language code from "en-US" format to "en" format for OpenAI
|
|
194
|
+
lang_code = language.split("-")[0] if "-" in language else language
|
|
195
|
+
data["language"] = lang_code
|
|
196
|
+
|
|
197
|
+
headers = {
|
|
198
|
+
"Authorization": f"Bearer {api_key}"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
202
|
+
response = await client.post(api_url, headers=headers, files=files, data=data)
|
|
203
|
+
response.raise_for_status()
|
|
204
|
+
result = response.json()
|
|
205
|
+
|
|
206
|
+
transcription_text = result.get("text", "").strip()
|
|
207
|
+
|
|
208
|
+
if not transcription_text:
|
|
209
|
+
log.warning("[AudioService] Empty transcription - no speech detected in audio")
|
|
210
|
+
raise HTTPException(400, "No speech detected in audio. Please try speaking louder or longer.")
|
|
211
|
+
|
|
212
|
+
return TranscriptionResult(
|
|
213
|
+
text=transcription_text,
|
|
214
|
+
language="en",
|
|
215
|
+
duration=0.0
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
except HTTPException:
|
|
219
|
+
raise
|
|
220
|
+
except Exception as e:
|
|
221
|
+
log.exception("[AudioService] OpenAI STT error: %s", e)
|
|
222
|
+
raise HTTPException(500, f"OpenAI STT failed: {str(e)}")
|
|
223
|
+
|
|
224
|
+
async def transcribe_audio_azure(
|
|
225
|
+
self,
|
|
226
|
+
temp_path: str,
|
|
227
|
+
user_id: str,
|
|
228
|
+
session_id: str,
|
|
229
|
+
app_name: str = "webui",
|
|
230
|
+
language: Optional[str] = None
|
|
231
|
+
) -> TranscriptionResult:
|
|
232
|
+
"""
|
|
233
|
+
Transcribe audio using Azure Speech SDK.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
temp_path: Path to temporary audio file
|
|
237
|
+
user_id: User identifier
|
|
238
|
+
session_id: Session identifier
|
|
239
|
+
app_name: Application name
|
|
240
|
+
language: Optional language code (e.g., "en-US", "es-ES")
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
TranscriptionResult with transcribed text
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
HTTPException: If transcription fails
|
|
247
|
+
"""
|
|
248
|
+
wav_temp_path = None
|
|
249
|
+
try:
|
|
250
|
+
# Import Azure SDK
|
|
251
|
+
try:
|
|
252
|
+
import azure.cognitiveservices.speech as speechsdk
|
|
253
|
+
except ImportError:
|
|
254
|
+
raise HTTPException(
|
|
255
|
+
500,
|
|
256
|
+
"Azure Speech SDK not installed. Run: pip install azure-cognitiveservices-speech"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Get Azure configuration
|
|
260
|
+
stt_config = self.speech_config.get("stt", {})
|
|
261
|
+
azure_config = stt_config.get("azure", {})
|
|
262
|
+
|
|
263
|
+
api_key = azure_config.get("api_key", "")
|
|
264
|
+
region = azure_config.get("region", "")
|
|
265
|
+
# Use provided language or fall back to config
|
|
266
|
+
final_language = language or azure_config.get("language", "en-US")
|
|
267
|
+
|
|
268
|
+
if not api_key or not region:
|
|
269
|
+
raise HTTPException(
|
|
270
|
+
500,
|
|
271
|
+
"Azure STT not configured. Please set speech.stt.azure.api_key and region, or speech.tts.azure if using shared config."
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Convert audio to WAV format (Azure SDK requires WAV)
|
|
275
|
+
# WebM/OGG/MP4 need to be converted
|
|
276
|
+
import os
|
|
277
|
+
from pydub import AudioSegment
|
|
278
|
+
|
|
279
|
+
file_ext = os.path.splitext(temp_path)[1].lower()
|
|
280
|
+
|
|
281
|
+
if file_ext not in ['.wav']:
|
|
282
|
+
# Convert to WAV
|
|
283
|
+
# Load audio file
|
|
284
|
+
if file_ext == '.webm':
|
|
285
|
+
audio = await asyncio.to_thread(AudioSegment.from_file, temp_path, format="webm")
|
|
286
|
+
elif file_ext == '.ogg':
|
|
287
|
+
audio = await asyncio.to_thread(AudioSegment.from_ogg, temp_path)
|
|
288
|
+
elif file_ext in ['.mp3', '.mp4']:
|
|
289
|
+
audio = await asyncio.to_thread(AudioSegment.from_file, temp_path)
|
|
290
|
+
else:
|
|
291
|
+
audio = await asyncio.to_thread(AudioSegment.from_file, temp_path)
|
|
292
|
+
|
|
293
|
+
# Create temp WAV file
|
|
294
|
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as wav_temp:
|
|
295
|
+
wav_temp_path = wav_temp.name
|
|
296
|
+
|
|
297
|
+
# Export as WAV (16kHz, mono, 16-bit PCM - Azure's preferred format)
|
|
298
|
+
await asyncio.to_thread(
|
|
299
|
+
audio.set_frame_rate(16000).set_channels(1).export,
|
|
300
|
+
wav_temp_path,
|
|
301
|
+
format="wav"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
audio_path = wav_temp_path
|
|
305
|
+
else:
|
|
306
|
+
audio_path = temp_path
|
|
307
|
+
|
|
308
|
+
# Create speech config
|
|
309
|
+
speech_config = speechsdk.SpeechConfig(
|
|
310
|
+
subscription=api_key,
|
|
311
|
+
region=region
|
|
312
|
+
)
|
|
313
|
+
speech_config.speech_recognition_language = final_language
|
|
314
|
+
|
|
315
|
+
# Create audio config from file
|
|
316
|
+
audio_config = speechsdk.AudioConfig(filename=audio_path)
|
|
317
|
+
|
|
318
|
+
# Create speech recognizer
|
|
319
|
+
recognizer = speechsdk.SpeechRecognizer(
|
|
320
|
+
speech_config=speech_config,
|
|
321
|
+
audio_config=audio_config
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
transcribed_texts = []
|
|
325
|
+
done = asyncio.Event()
|
|
326
|
+
|
|
327
|
+
def recognized_handler(evt):
|
|
328
|
+
if evt.result.reason == speechsdk.ResultReason.RecognizedSpeech:
|
|
329
|
+
text = evt.result.text.strip()
|
|
330
|
+
if text:
|
|
331
|
+
transcribed_texts.append(text)
|
|
332
|
+
|
|
333
|
+
def canceled_handler(evt):
|
|
334
|
+
if evt.cancellation_details.error_details:
|
|
335
|
+
log.error(f"[AudioService] Recognition error: {evt.cancellation_details.error_details}")
|
|
336
|
+
done.set()
|
|
337
|
+
|
|
338
|
+
def stopped_handler(evt):
|
|
339
|
+
done.set()
|
|
340
|
+
|
|
341
|
+
# Connect callbacks
|
|
342
|
+
recognizer.recognized.connect(recognized_handler)
|
|
343
|
+
recognizer.canceled.connect(canceled_handler)
|
|
344
|
+
recognizer.session_stopped.connect(stopped_handler)
|
|
345
|
+
|
|
346
|
+
# Start continuous recognition
|
|
347
|
+
await asyncio.to_thread(recognizer.start_continuous_recognition)
|
|
348
|
+
|
|
349
|
+
# Wait for recognition to complete
|
|
350
|
+
await done.wait()
|
|
351
|
+
|
|
352
|
+
# Stop recognition
|
|
353
|
+
await asyncio.to_thread(recognizer.stop_continuous_recognition)
|
|
354
|
+
|
|
355
|
+
# Combine all recognized text
|
|
356
|
+
if not transcribed_texts:
|
|
357
|
+
log.warning("[AudioService] No speech could be recognized")
|
|
358
|
+
raise HTTPException(400, "No speech detected in audio. Please try speaking louder or longer.")
|
|
359
|
+
|
|
360
|
+
full_transcription = " ".join(transcribed_texts).strip()
|
|
361
|
+
|
|
362
|
+
return TranscriptionResult(
|
|
363
|
+
text=full_transcription,
|
|
364
|
+
language=final_language,
|
|
365
|
+
duration=0.0 # Duration not available in continuous mode
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
except HTTPException:
|
|
369
|
+
raise
|
|
370
|
+
except Exception as e:
|
|
371
|
+
log.exception("[AudioService] Azure STT error: %s", e)
|
|
372
|
+
raise HTTPException(500, f"Azure STT failed: {str(e)}")
|
|
373
|
+
finally:
|
|
374
|
+
# Clean up WAV temp file if created
|
|
375
|
+
if wav_temp_path:
|
|
376
|
+
try:
|
|
377
|
+
import os
|
|
378
|
+
os.unlink(wav_temp_path)
|
|
379
|
+
except Exception as e:
|
|
380
|
+
log.warning("[AudioService] Failed to delete WAV temp file: %s", e)
|
|
381
|
+
|
|
382
|
+
async def transcribe_audio(
|
|
383
|
+
self,
|
|
384
|
+
audio_file: UploadFile,
|
|
385
|
+
user_id: str,
|
|
386
|
+
session_id: str,
|
|
387
|
+
app_name: str = "webui",
|
|
388
|
+
provider: Optional[str] = None,
|
|
389
|
+
language: Optional[str] = None
|
|
390
|
+
) -> TranscriptionResult:
|
|
391
|
+
"""
|
|
392
|
+
Transcribe audio file to text using configured STT service.
|
|
393
|
+
Routes to appropriate provider (OpenAI, Azure, etc.).
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
audio_file: Uploaded audio file
|
|
397
|
+
user_id: User identifier
|
|
398
|
+
session_id: Session identifier
|
|
399
|
+
app_name: Application name
|
|
400
|
+
provider: Optional provider override (openai, azure)
|
|
401
|
+
language: Optional language code (e.g., "en-US", "es-ES")
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
TranscriptionResult with transcribed text
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
HTTPException: If transcription fails
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
# Validate file
|
|
412
|
+
if not audio_file.filename:
|
|
413
|
+
raise HTTPException(400, "No filename provided")
|
|
414
|
+
|
|
415
|
+
# Check file size before reading (max 25MB) to prevent OOM
|
|
416
|
+
if audio_file.size and audio_file.size > 25 * 1024 * 1024:
|
|
417
|
+
raise HTTPException(413, "Audio file too large (max 25MB)")
|
|
418
|
+
|
|
419
|
+
# Read content after size check
|
|
420
|
+
content = await audio_file.read()
|
|
421
|
+
|
|
422
|
+
# Double-check size after reading (in case size wasn't available before)
|
|
423
|
+
if len(content) > 25 * 1024 * 1024:
|
|
424
|
+
raise HTTPException(413, "Audio file too large (max 25MB)")
|
|
425
|
+
|
|
426
|
+
# Save to temporary file
|
|
427
|
+
with tempfile.NamedTemporaryFile(
|
|
428
|
+
suffix=self._get_file_extension(audio_file.filename),
|
|
429
|
+
delete=False
|
|
430
|
+
) as temp_file:
|
|
431
|
+
temp_file.write(content)
|
|
432
|
+
temp_path = temp_file.name
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
# Get STT configuration
|
|
436
|
+
stt_config = self.speech_config.get("stt", {})
|
|
437
|
+
if not stt_config:
|
|
438
|
+
raise HTTPException(
|
|
439
|
+
500,
|
|
440
|
+
"STT not configured. Please add speech.stt configuration."
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Determine provider - use request provider if provided, otherwise use config
|
|
444
|
+
final_provider = provider or stt_config.get("provider", "openai")
|
|
445
|
+
|
|
446
|
+
log.info(
|
|
447
|
+
"[AudioService] Transcribing audio for user=%s, session=%s, provider=%s, language=%s",
|
|
448
|
+
user_id, session_id, final_provider, language
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Route to appropriate provider
|
|
452
|
+
if final_provider == "azure":
|
|
453
|
+
return await self.transcribe_audio_azure(
|
|
454
|
+
temp_path, user_id, session_id, app_name, language
|
|
455
|
+
)
|
|
456
|
+
elif final_provider == "openai":
|
|
457
|
+
return await self.transcribe_audio_openai(
|
|
458
|
+
temp_path, user_id, session_id, app_name, language
|
|
459
|
+
)
|
|
460
|
+
else:
|
|
461
|
+
raise HTTPException(500, f"Unknown STT provider: {final_provider}")
|
|
462
|
+
|
|
463
|
+
finally:
|
|
464
|
+
# Clean up temp file
|
|
465
|
+
import os
|
|
466
|
+
try:
|
|
467
|
+
os.unlink(temp_path)
|
|
468
|
+
except Exception as e:
|
|
469
|
+
log.warning("[AudioService] Failed to delete temp file: %s", e)
|
|
470
|
+
|
|
471
|
+
except HTTPException:
|
|
472
|
+
raise
|
|
473
|
+
except Exception as e:
|
|
474
|
+
log.exception("[AudioService] Transcription error: %s", e)
|
|
475
|
+
raise HTTPException(500, f"Transcription failed: {str(e)}")
|
|
476
|
+
def _generate_azure_ssml(self, text: str, voice: str) -> str:
|
|
477
|
+
"""
|
|
478
|
+
Generate SSML for Azure TTS with proper XML escaping.
|
|
479
|
+
Handles both standard and HD voice formats.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
text: Text to convert to speech
|
|
483
|
+
voice: Azure voice name (e.g., "en-US-JennyNeural" or "en-US-Ava:DragonHDLatestNeural")
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
SSML string
|
|
487
|
+
"""
|
|
488
|
+
# Escape XML special characters in text
|
|
489
|
+
escaped_text = (text
|
|
490
|
+
.replace("&", "&")
|
|
491
|
+
.replace("<", "<")
|
|
492
|
+
.replace(">", ">")
|
|
493
|
+
.replace('"', """)
|
|
494
|
+
.replace("'", "'"))
|
|
495
|
+
|
|
496
|
+
# Escape XML special characters in voice name to prevent injection
|
|
497
|
+
escaped_voice = (voice
|
|
498
|
+
.replace("&", "&")
|
|
499
|
+
.replace("<", "<")
|
|
500
|
+
.replace(">", ">")
|
|
501
|
+
.replace('"', """)
|
|
502
|
+
.replace("'", "'"))
|
|
503
|
+
|
|
504
|
+
# For HD voices, the format is "locale-Name:DragonHDLatestNeural"
|
|
505
|
+
# Azure expects just the voice name in SSML, not the :DragonHDLatestNeural suffix
|
|
506
|
+
# The HD quality is specified via the voice name itself
|
|
507
|
+
|
|
508
|
+
return f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
|
|
509
|
+
<voice name="{escaped_voice}">
|
|
510
|
+
<prosody rate="medium" pitch="default">
|
|
511
|
+
{escaped_text}
|
|
512
|
+
</prosody>
|
|
513
|
+
</voice>
|
|
514
|
+
</speak>"""
|
|
515
|
+
|
|
516
|
+
async def generate_speech_azure(
|
|
517
|
+
self,
|
|
518
|
+
text: str,
|
|
519
|
+
voice: Optional[str],
|
|
520
|
+
user_id: str,
|
|
521
|
+
session_id: str,
|
|
522
|
+
app_name: str = "webui",
|
|
523
|
+
message_id: Optional[str] = None
|
|
524
|
+
) -> bytes:
|
|
525
|
+
"""
|
|
526
|
+
Generate speech using Azure Neural Voices.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
text: Text to convert to speech
|
|
530
|
+
voice: Azure voice name (e.g., "en-US-JennyNeural")
|
|
531
|
+
user_id: User identifier
|
|
532
|
+
session_id: Session identifier
|
|
533
|
+
app_name: Application name
|
|
534
|
+
message_id: Optional message ID for caching
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Audio data as bytes (MP3 format)
|
|
538
|
+
|
|
539
|
+
Raises:
|
|
540
|
+
HTTPException: If generation fails
|
|
541
|
+
"""
|
|
542
|
+
|
|
543
|
+
try:
|
|
544
|
+
|
|
545
|
+
# Import Azure SDK
|
|
546
|
+
try:
|
|
547
|
+
import azure.cognitiveservices.speech as speechsdk
|
|
548
|
+
except ImportError as e:
|
|
549
|
+
log.error(f"[AudioService] Azure SDK not installed: {e}")
|
|
550
|
+
raise HTTPException(
|
|
551
|
+
500,
|
|
552
|
+
"Azure Speech SDK not installed. Run: pip install azure-cognitiveservices-speech"
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Get Azure configuration
|
|
556
|
+
tts_config = self.speech_config.get("tts", {})
|
|
557
|
+
azure_config = tts_config.get("azure", {})
|
|
558
|
+
|
|
559
|
+
api_key = azure_config.get("api_key", "")
|
|
560
|
+
region = azure_config.get("region", "")
|
|
561
|
+
|
|
562
|
+
if not api_key or not region:
|
|
563
|
+
log.error("[AudioService] Azure TTS missing api_key or region")
|
|
564
|
+
raise HTTPException(
|
|
565
|
+
500,
|
|
566
|
+
"Azure TTS not configured. Please set speech.tts.azure.api_key and region."
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Set voice - use default if provided voice is not an Azure voice
|
|
570
|
+
requested_voice = voice or azure_config.get("default_voice", "en-US-JennyNeural")
|
|
571
|
+
|
|
572
|
+
# Check if requested voice is an Azure voice
|
|
573
|
+
# Azure voices contain "Neural" or "DragonHD" and have locale prefix (e.g., "en-US-")
|
|
574
|
+
is_azure_voice = (
|
|
575
|
+
("Neural" in requested_voice or "DragonHD" in requested_voice)
|
|
576
|
+
and ("-" in requested_voice)
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
if is_azure_voice:
|
|
580
|
+
final_voice = requested_voice
|
|
581
|
+
else:
|
|
582
|
+
# Not an Azure voice, use default
|
|
583
|
+
final_voice = azure_config.get("default_voice", "en-US-JennyNeural")
|
|
584
|
+
|
|
585
|
+
# Create speech config
|
|
586
|
+
speech_config = speechsdk.SpeechConfig(
|
|
587
|
+
subscription=api_key,
|
|
588
|
+
region=region
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
# Set output format to MP3
|
|
592
|
+
speech_config.set_speech_synthesis_output_format(
|
|
593
|
+
speechsdk.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
# Generate SSML
|
|
597
|
+
ssml = self._generate_azure_ssml(text, final_voice)
|
|
598
|
+
|
|
599
|
+
log.debug(f"[AudioService] Generated SSML: {ssml[:200]}...")
|
|
600
|
+
|
|
601
|
+
# Create synthesizer (None for in-memory output)
|
|
602
|
+
synthesizer = speechsdk.SpeechSynthesizer(
|
|
603
|
+
speech_config=speech_config,
|
|
604
|
+
audio_config=None
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
# Synthesize speech (run in thread pool to avoid blocking)
|
|
608
|
+
result = await asyncio.to_thread(
|
|
609
|
+
lambda: synthesizer.speak_ssml_async(ssml).get()
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Check result
|
|
613
|
+
if result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted:
|
|
614
|
+
audio_data = result.audio_data
|
|
615
|
+
return audio_data
|
|
616
|
+
elif result.reason == speechsdk.ResultReason.Canceled:
|
|
617
|
+
cancellation = result.cancellation_details
|
|
618
|
+
error_msg = f"Azure TTS canceled: {cancellation.reason}"
|
|
619
|
+
if cancellation.error_details:
|
|
620
|
+
error_msg += f" - {cancellation.error_details}"
|
|
621
|
+
log.error(f"[AudioService] {error_msg}")
|
|
622
|
+
raise HTTPException(500, error_msg)
|
|
623
|
+
else:
|
|
624
|
+
raise HTTPException(500, f"Azure TTS failed with reason: {result.reason}")
|
|
625
|
+
|
|
626
|
+
except HTTPException:
|
|
627
|
+
raise
|
|
628
|
+
except Exception as e:
|
|
629
|
+
log.exception("[AudioService] Azure TTS generation error: %s", e)
|
|
630
|
+
raise HTTPException(500, f"Azure TTS generation failed: {str(e)}")
|
|
631
|
+
|
|
632
|
+
async def generate_speech_gemini(
|
|
633
|
+
self,
|
|
634
|
+
text: str,
|
|
635
|
+
voice: Optional[str],
|
|
636
|
+
user_id: str,
|
|
637
|
+
session_id: str,
|
|
638
|
+
app_name: str = "webui",
|
|
639
|
+
message_id: Optional[str] = None
|
|
640
|
+
) -> bytes:
|
|
641
|
+
"""
|
|
642
|
+
Generate speech using Gemini TTS (original implementation).
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
text: Text to convert to speech
|
|
646
|
+
voice: Voice name to use
|
|
647
|
+
user_id: User identifier
|
|
648
|
+
session_id: Session identifier
|
|
649
|
+
app_name: Application name
|
|
650
|
+
message_id: Optional message ID for caching
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
Audio data as bytes (MP3 format)
|
|
654
|
+
|
|
655
|
+
Raises:
|
|
656
|
+
HTTPException: If generation fails
|
|
657
|
+
"""
|
|
658
|
+
|
|
659
|
+
try:
|
|
660
|
+
|
|
661
|
+
# Get TTS configuration
|
|
662
|
+
tts_config = self.speech_config.get("tts", {})
|
|
663
|
+
gemini_config = tts_config.get("gemini", tts_config)
|
|
664
|
+
|
|
665
|
+
# Use direct Gemini API call
|
|
666
|
+
from google import genai
|
|
667
|
+
from google.genai import types as adk_types
|
|
668
|
+
import wave
|
|
669
|
+
from pydub import AudioSegment
|
|
670
|
+
import os
|
|
671
|
+
|
|
672
|
+
api_key = gemini_config.get("api_key", "")
|
|
673
|
+
model = gemini_config.get("model", "gemini-2.5-flash-preview-tts")
|
|
674
|
+
final_voice = voice or gemini_config.get("default_voice", "Kore")
|
|
675
|
+
# Gemini requires lowercase voice names
|
|
676
|
+
final_voice = final_voice.lower()
|
|
677
|
+
language = gemini_config.get("language", "en-US")
|
|
678
|
+
|
|
679
|
+
if not api_key:
|
|
680
|
+
log.error("[AudioService] No Gemini API key found")
|
|
681
|
+
raise HTTPException(500, "Gemini TTS API key not configured")
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
# Create Gemini client
|
|
685
|
+
client = genai.Client(api_key=api_key)
|
|
686
|
+
|
|
687
|
+
# Create voice config
|
|
688
|
+
voice_config = adk_types.VoiceConfig(
|
|
689
|
+
prebuilt_voice_config=adk_types.PrebuiltVoiceConfig(voice_name=final_voice)
|
|
690
|
+
)
|
|
691
|
+
speech_config = adk_types.SpeechConfig(voice_config=voice_config)
|
|
692
|
+
|
|
693
|
+
# Generate audio
|
|
694
|
+
config = adk_types.GenerateContentConfig(
|
|
695
|
+
response_modalities=["AUDIO"],
|
|
696
|
+
speech_config=speech_config,
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# Retry logic for transient API failures
|
|
700
|
+
max_retries = 2
|
|
701
|
+
last_error = None
|
|
702
|
+
|
|
703
|
+
for attempt in range(max_retries):
|
|
704
|
+
try:
|
|
705
|
+
response = await asyncio.to_thread(
|
|
706
|
+
client.models.generate_content,
|
|
707
|
+
model=model,
|
|
708
|
+
contents=f"Say in a clear voice: {text}",
|
|
709
|
+
config=config
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# Validate response structure
|
|
713
|
+
if not response:
|
|
714
|
+
raise ValueError("Gemini API returned empty response")
|
|
715
|
+
|
|
716
|
+
if not response.candidates or len(response.candidates) == 0:
|
|
717
|
+
raise ValueError("Gemini API returned no candidates")
|
|
718
|
+
|
|
719
|
+
candidate = response.candidates[0]
|
|
720
|
+
|
|
721
|
+
# Log candidate details for debugging
|
|
722
|
+
log.debug(f"[AudioService] Candidate finish_reason: {getattr(candidate, 'finish_reason', 'unknown')}")
|
|
723
|
+
log.debug(f"[AudioService] Candidate has content: {candidate.content is not None}")
|
|
724
|
+
|
|
725
|
+
if not candidate.content:
|
|
726
|
+
# Check if there's a finish_reason that explains why
|
|
727
|
+
finish_reason = getattr(candidate, 'finish_reason', None)
|
|
728
|
+
if finish_reason:
|
|
729
|
+
raise ValueError(f"Gemini API returned candidate with no content (finish_reason: {finish_reason})")
|
|
730
|
+
raise ValueError("Gemini API returned candidate with no content")
|
|
731
|
+
|
|
732
|
+
# Success - break retry loop
|
|
733
|
+
break
|
|
734
|
+
|
|
735
|
+
except ValueError as e:
|
|
736
|
+
last_error = e
|
|
737
|
+
if attempt < max_retries - 1:
|
|
738
|
+
log.warning(f"[AudioService] TTS attempt {attempt + 1} failed: {e}, retrying...")
|
|
739
|
+
await asyncio.sleep(0.5) # Brief delay before retry
|
|
740
|
+
else:
|
|
741
|
+
log.error(f"[AudioService] TTS failed after {max_retries} attempts: {e}")
|
|
742
|
+
raise HTTPException(500, f"TTS generation failed after {max_retries} attempts: {str(e)}")
|
|
743
|
+
|
|
744
|
+
if not candidate.content.parts or len(candidate.content.parts) == 0:
|
|
745
|
+
raise HTTPException(500, "Gemini API returned no audio parts")
|
|
746
|
+
|
|
747
|
+
part = candidate.content.parts[0]
|
|
748
|
+
if not hasattr(part, 'inline_data') or not part.inline_data:
|
|
749
|
+
raise HTTPException(500, "Gemini API returned part with no inline_data")
|
|
750
|
+
|
|
751
|
+
wav_data = part.inline_data.data
|
|
752
|
+
if not wav_data:
|
|
753
|
+
raise HTTPException(500, "No audio data received from Gemini API")
|
|
754
|
+
|
|
755
|
+
# Convert WAV to MP3
|
|
756
|
+
def create_wav_file(filename: str, pcm_data: bytes):
|
|
757
|
+
with wave.open(filename, "wb") as wf:
|
|
758
|
+
wf.setnchannels(1)
|
|
759
|
+
wf.setsampwidth(2)
|
|
760
|
+
wf.setframerate(24000)
|
|
761
|
+
wf.writeframes(pcm_data)
|
|
762
|
+
|
|
763
|
+
wav_temp_path = None
|
|
764
|
+
mp3_temp_path = None
|
|
765
|
+
|
|
766
|
+
try:
|
|
767
|
+
# Create temp WAV file
|
|
768
|
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as wav_temp:
|
|
769
|
+
wav_temp_path = wav_temp.name
|
|
770
|
+
|
|
771
|
+
await asyncio.to_thread(create_wav_file, wav_temp_path, wav_data)
|
|
772
|
+
|
|
773
|
+
# Create temp MP3 file
|
|
774
|
+
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as mp3_temp:
|
|
775
|
+
mp3_temp_path = mp3_temp.name
|
|
776
|
+
|
|
777
|
+
# Convert to MP3
|
|
778
|
+
audio = await asyncio.to_thread(AudioSegment.from_wav, wav_temp_path)
|
|
779
|
+
await asyncio.to_thread(audio.export, mp3_temp_path, format="mp3")
|
|
780
|
+
|
|
781
|
+
# Read MP3 data
|
|
782
|
+
with open(mp3_temp_path, "rb") as mp3_file:
|
|
783
|
+
mp3_data = mp3_file.read()
|
|
784
|
+
|
|
785
|
+
return mp3_data
|
|
786
|
+
|
|
787
|
+
finally:
|
|
788
|
+
# Clean up temp files
|
|
789
|
+
if wav_temp_path:
|
|
790
|
+
try:
|
|
791
|
+
os.remove(wav_temp_path)
|
|
792
|
+
except:
|
|
793
|
+
pass
|
|
794
|
+
if mp3_temp_path:
|
|
795
|
+
try:
|
|
796
|
+
os.remove(mp3_temp_path)
|
|
797
|
+
except:
|
|
798
|
+
pass
|
|
799
|
+
|
|
800
|
+
except HTTPException:
|
|
801
|
+
raise
|
|
802
|
+
except Exception as e:
|
|
803
|
+
log.exception("[AudioService] Gemini TTS generation error: %s", e)
|
|
804
|
+
raise HTTPException(500, f"Gemini TTS generation failed: {str(e)}")
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
async def generate_speech_polly(
|
|
808
|
+
self,
|
|
809
|
+
text: str,
|
|
810
|
+
voice: Optional[str],
|
|
811
|
+
user_id: str,
|
|
812
|
+
session_id: str,
|
|
813
|
+
app_name: str = "webui",
|
|
814
|
+
message_id: Optional[str] = None
|
|
815
|
+
) -> bytes:
|
|
816
|
+
"""
|
|
817
|
+
Generate speech using AWS Polly.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
text: Text to convert to speech
|
|
821
|
+
voice: Polly voice ID (e.g., "Joanna", "Matthew")
|
|
822
|
+
user_id: User identifier
|
|
823
|
+
session_id: Session identifier
|
|
824
|
+
app_name: Application name
|
|
825
|
+
message_id: Optional message ID for caching
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
Audio data as bytes (MP3 format)
|
|
829
|
+
|
|
830
|
+
Raises:
|
|
831
|
+
HTTPException: If generation fails
|
|
832
|
+
"""
|
|
833
|
+
|
|
834
|
+
try:
|
|
835
|
+
|
|
836
|
+
# Import boto3
|
|
837
|
+
try:
|
|
838
|
+
import boto3
|
|
839
|
+
from botocore.exceptions import ClientError as BotoClientError, BotoCoreError
|
|
840
|
+
except ImportError as e:
|
|
841
|
+
log.error(f"[AudioService] boto3 not installed: {e}")
|
|
842
|
+
raise HTTPException(
|
|
843
|
+
500,
|
|
844
|
+
"AWS boto3 SDK not installed. Run: pip install boto3"
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
# Get Polly configuration
|
|
848
|
+
tts_config = self.speech_config.get("tts", {})
|
|
849
|
+
polly_config = tts_config.get("polly", {})
|
|
850
|
+
|
|
851
|
+
aws_access_key_id = polly_config.get("aws_access_key_id", "")
|
|
852
|
+
aws_secret_access_key = polly_config.get("aws_secret_access_key", "")
|
|
853
|
+
region = polly_config.get("region", "us-east-1")
|
|
854
|
+
engine = polly_config.get("engine", "neural") # 'neural' or 'standard'
|
|
855
|
+
|
|
856
|
+
if not aws_access_key_id or not aws_secret_access_key:
|
|
857
|
+
log.error("[AudioService] AWS Polly missing credentials")
|
|
858
|
+
raise HTTPException(
|
|
859
|
+
500,
|
|
860
|
+
"AWS Polly not configured. Please set speech.tts.polly.aws_access_key_id and aws_secret_access_key."
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
# Set voice - use default if provided voice is not a Polly voice
|
|
864
|
+
requested_voice = voice or polly_config.get("default_voice", "Joanna")
|
|
865
|
+
|
|
866
|
+
# Polly voices are simple names (e.g., "Joanna", "Matthew")
|
|
867
|
+
# Validate it's a reasonable voice name (alphanumeric)
|
|
868
|
+
is_polly_voice = requested_voice.isalpha()
|
|
869
|
+
|
|
870
|
+
if is_polly_voice:
|
|
871
|
+
final_voice = requested_voice
|
|
872
|
+
else:
|
|
873
|
+
# Not a valid Polly voice, use default
|
|
874
|
+
final_voice = polly_config.get("default_voice", "Joanna")
|
|
875
|
+
log.warning(f"[AudioService] Invalid Polly voice '{requested_voice}', using default '{final_voice}'")
|
|
876
|
+
|
|
877
|
+
# Create Polly client
|
|
878
|
+
try:
|
|
879
|
+
polly_client = boto3.client(
|
|
880
|
+
'polly',
|
|
881
|
+
aws_access_key_id=aws_access_key_id,
|
|
882
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
883
|
+
region_name=region
|
|
884
|
+
)
|
|
885
|
+
except Exception as e:
|
|
886
|
+
log.error(f"[AudioService] Failed to create Polly client: {e}")
|
|
887
|
+
raise HTTPException(500, f"Failed to create AWS Polly client: {str(e)}")
|
|
888
|
+
|
|
889
|
+
# Synthesize speech
|
|
890
|
+
log.debug(f"[AudioService] Synthesizing with voice={final_voice}, engine={engine}, text_len={len(text)}")
|
|
891
|
+
|
|
892
|
+
try:
|
|
893
|
+
response = await asyncio.to_thread(
|
|
894
|
+
polly_client.synthesize_speech,
|
|
895
|
+
Text=text,
|
|
896
|
+
OutputFormat='mp3',
|
|
897
|
+
VoiceId=final_voice,
|
|
898
|
+
Engine=engine
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
# Read audio stream
|
|
902
|
+
if 'AudioStream' in response:
|
|
903
|
+
audio_data = response['AudioStream'].read()
|
|
904
|
+
return audio_data
|
|
905
|
+
else:
|
|
906
|
+
raise HTTPException(500, "No audio stream in Polly response")
|
|
907
|
+
|
|
908
|
+
except BotoClientError as e:
|
|
909
|
+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
910
|
+
error_msg = e.response.get('Error', {}).get('Message', str(e))
|
|
911
|
+
log.error(f"[AudioService] Polly API error: {error_code} - {error_msg}")
|
|
912
|
+
|
|
913
|
+
if error_code == 'InvalidParameterValue':
|
|
914
|
+
raise HTTPException(400, f"Invalid Polly parameter: {error_msg}")
|
|
915
|
+
elif error_code in ['AccessDeniedException', 'UnauthorizedException']:
|
|
916
|
+
raise HTTPException(403, f"AWS Polly authentication failed: {error_msg}")
|
|
917
|
+
else:
|
|
918
|
+
raise HTTPException(500, f"AWS Polly error ({error_code}): {error_msg}")
|
|
919
|
+
|
|
920
|
+
except BotoCoreError as e:
|
|
921
|
+
log.error(f"[AudioService] Boto core error: {e}")
|
|
922
|
+
raise HTTPException(500, f"AWS SDK error: {str(e)}")
|
|
923
|
+
|
|
924
|
+
except HTTPException:
|
|
925
|
+
raise
|
|
926
|
+
except Exception as e:
|
|
927
|
+
log.exception("[AudioService] AWS Polly TTS generation error: %s", e)
|
|
928
|
+
raise HTTPException(500, f"AWS Polly TTS generation failed: {str(e)}")
|
|
929
|
+
|
|
930
|
+
async def generate_speech(
|
|
931
|
+
self,
|
|
932
|
+
text: str,
|
|
933
|
+
voice: Optional[str],
|
|
934
|
+
user_id: str,
|
|
935
|
+
session_id: str,
|
|
936
|
+
app_name: str = "webui",
|
|
937
|
+
message_id: Optional[str] = None,
|
|
938
|
+
provider: Optional[str] = None # NEW: Allow provider override from request
|
|
939
|
+
) -> bytes:
|
|
940
|
+
"""
|
|
941
|
+
Generate speech audio from text using configured TTS service.
|
|
942
|
+
Routes to appropriate provider (Azure, Gemini, etc.).
|
|
943
|
+
|
|
944
|
+
Args:
|
|
945
|
+
text: Text to convert to speech
|
|
946
|
+
voice: Voice name to use
|
|
947
|
+
user_id: User identifier
|
|
948
|
+
session_id: Session identifier
|
|
949
|
+
app_name: Application name
|
|
950
|
+
message_id: Optional message ID for caching
|
|
951
|
+
provider: Optional provider override (azure, gemini)
|
|
952
|
+
|
|
953
|
+
Returns:
|
|
954
|
+
Audio data as bytes (MP3 format)
|
|
955
|
+
|
|
956
|
+
Raises:
|
|
957
|
+
HTTPException: If generation fails
|
|
958
|
+
"""
|
|
959
|
+
log.info(
|
|
960
|
+
"[AudioService] Generating speech for user=%s, session=%s, voice=%s, text_len=%d, provider=%s",
|
|
961
|
+
user_id, session_id, voice, len(text), provider
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
try:
|
|
965
|
+
tts_config = self.speech_config.get("tts", {}) if self.speech_config else {}
|
|
966
|
+
|
|
967
|
+
if not tts_config:
|
|
968
|
+
log.error("[AudioService] TTS not configured in speech.tts")
|
|
969
|
+
log.error(f"[AudioService] Available config keys: {list(self.config.keys())}")
|
|
970
|
+
log.error(f"[AudioService] Speech config value: {self.speech_config}")
|
|
971
|
+
raise HTTPException(
|
|
972
|
+
500,
|
|
973
|
+
"TTS not configured. Please add speech.tts configuration to gateway YAML under app_config."
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
# Determine provider - use request provider if provided, otherwise use config
|
|
977
|
+
final_provider = provider or tts_config.get("provider", "gemini")
|
|
978
|
+
|
|
979
|
+
# Route to appropriate provider
|
|
980
|
+
if final_provider == "azure":
|
|
981
|
+
return await self.generate_speech_azure(
|
|
982
|
+
text, voice, user_id, session_id, app_name, message_id
|
|
983
|
+
)
|
|
984
|
+
elif final_provider == "gemini":
|
|
985
|
+
return await self.generate_speech_gemini(
|
|
986
|
+
text, voice, user_id, session_id, app_name, message_id
|
|
987
|
+
)
|
|
988
|
+
elif final_provider == "polly":
|
|
989
|
+
return await self.generate_speech_polly(
|
|
990
|
+
text, voice, user_id, session_id, app_name, message_id
|
|
991
|
+
)
|
|
992
|
+
else:
|
|
993
|
+
raise HTTPException(500, f"Unknown TTS provider: {final_provider}")
|
|
994
|
+
|
|
995
|
+
except HTTPException:
|
|
996
|
+
raise
|
|
997
|
+
except Exception as e:
|
|
998
|
+
log.exception("[AudioService] TTS generation error: %s", e)
|
|
999
|
+
raise HTTPException(500, f"TTS generation failed: {str(e)}")
|
|
1000
|
+
|
|
1001
|
+
async def stream_speech(
|
|
1002
|
+
self,
|
|
1003
|
+
text: str,
|
|
1004
|
+
voice: Optional[str],
|
|
1005
|
+
user_id: str,
|
|
1006
|
+
session_id: str,
|
|
1007
|
+
app_name: str = "webui",
|
|
1008
|
+
provider: Optional[str] = None
|
|
1009
|
+
) -> AsyncGenerator[bytes, None]:
|
|
1010
|
+
"""
|
|
1011
|
+
Stream speech audio for long text with intelligent sentence-based chunking.
|
|
1012
|
+
Generates and yields audio chunks immediately for reduced latency.
|
|
1013
|
+
|
|
1014
|
+
Args:
|
|
1015
|
+
text: Text to convert to speech
|
|
1016
|
+
voice: Voice name to use
|
|
1017
|
+
user_id: User identifier
|
|
1018
|
+
session_id: Session identifier
|
|
1019
|
+
app_name: Application name
|
|
1020
|
+
provider: Optional provider override (azure, gemini, polly)
|
|
1021
|
+
|
|
1022
|
+
Yields:
|
|
1023
|
+
Audio data chunks as bytes
|
|
1024
|
+
"""
|
|
1025
|
+
|
|
1026
|
+
# Split text into sentence-based chunks for more natural audio boundaries
|
|
1027
|
+
import re
|
|
1028
|
+
|
|
1029
|
+
# Split on sentence boundaries (., !, ?, newlines)
|
|
1030
|
+
sentences = re.split(r'(?<=[.!?\n])\s+', text)
|
|
1031
|
+
|
|
1032
|
+
# Group sentences into smaller chunks for faster initial playback
|
|
1033
|
+
MAX_CHUNK_SIZE = 300 # Reduced from 500 for faster first chunk
|
|
1034
|
+
chunks = []
|
|
1035
|
+
current_chunk = ""
|
|
1036
|
+
|
|
1037
|
+
for sentence in sentences:
|
|
1038
|
+
if len(current_chunk) + len(sentence) > MAX_CHUNK_SIZE and current_chunk:
|
|
1039
|
+
chunks.append(current_chunk.strip())
|
|
1040
|
+
current_chunk = sentence
|
|
1041
|
+
else:
|
|
1042
|
+
current_chunk += " " + sentence if current_chunk else sentence
|
|
1043
|
+
|
|
1044
|
+
if current_chunk:
|
|
1045
|
+
chunks.append(current_chunk.strip())
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
# Generate and yield chunks immediately (no buffering)
|
|
1049
|
+
for i, chunk in enumerate(chunks):
|
|
1050
|
+
log.debug("[AudioService] Generating chunk %d/%d (len=%d)", i+1, len(chunks), len(chunk))
|
|
1051
|
+
|
|
1052
|
+
try:
|
|
1053
|
+
audio_data = await self.generate_speech(
|
|
1054
|
+
text=chunk,
|
|
1055
|
+
voice=voice,
|
|
1056
|
+
user_id=user_id,
|
|
1057
|
+
session_id=session_id,
|
|
1058
|
+
app_name=app_name,
|
|
1059
|
+
message_id=f"chunk_{i}",
|
|
1060
|
+
provider=provider # Pass provider to generate_speech
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
if audio_data:
|
|
1064
|
+
log.debug("[AudioService] Yielding chunk %d (%d bytes)", i+1, len(audio_data))
|
|
1065
|
+
yield audio_data
|
|
1066
|
+
|
|
1067
|
+
except Exception as e:
|
|
1068
|
+
log.error("[AudioService] Error generating chunk %d: %s", i, e)
|
|
1069
|
+
# Continue with next chunk instead of failing completely
|
|
1070
|
+
continue
|
|
1071
|
+
|
|
1072
|
+
async def get_available_voices(self, provider: Optional[str] = None) -> List[str]:
|
|
1073
|
+
"""
|
|
1074
|
+
Get list of available TTS voices from configuration.
|
|
1075
|
+
|
|
1076
|
+
Args:
|
|
1077
|
+
provider: Optional provider filter (azure, gemini)
|
|
1078
|
+
|
|
1079
|
+
Returns:
|
|
1080
|
+
List of voice names
|
|
1081
|
+
"""
|
|
1082
|
+
tts_config = self.speech_config.get("tts", {})
|
|
1083
|
+
# Use provided provider or fall back to config
|
|
1084
|
+
final_provider = provider or tts_config.get("provider", "gemini")
|
|
1085
|
+
|
|
1086
|
+
if final_provider == "azure":
|
|
1087
|
+
azure_config = tts_config.get("azure", {})
|
|
1088
|
+
voices = azure_config.get("voices", AZURE_NEURAL_VOICES)
|
|
1089
|
+
elif final_provider == "gemini":
|
|
1090
|
+
gemini_config = tts_config.get("gemini", tts_config) # Fallback to root for backward compat
|
|
1091
|
+
voices = gemini_config.get("voices", ALL_AVAILABLE_VOICES)
|
|
1092
|
+
elif final_provider == "polly":
|
|
1093
|
+
polly_config = tts_config.get("polly", {})
|
|
1094
|
+
voices = polly_config.get("voices", AWS_POLLY_NEURAL_VOICES)
|
|
1095
|
+
else:
|
|
1096
|
+
voices = []
|
|
1097
|
+
|
|
1098
|
+
log.debug("[AudioService] Available voices for provider %s: %d", final_provider, len(voices))
|
|
1099
|
+
return voices
|
|
1100
|
+
|
|
1101
|
+
def _is_valid_api_key(self, value: Any) -> bool:
|
|
1102
|
+
"""
|
|
1103
|
+
Check if a value is a valid API key (non-empty string that's not an unresolved env var).
|
|
1104
|
+
|
|
1105
|
+
Args:
|
|
1106
|
+
value: The value to check
|
|
1107
|
+
|
|
1108
|
+
Returns:
|
|
1109
|
+
True if the value appears to be a valid API key
|
|
1110
|
+
"""
|
|
1111
|
+
if not value:
|
|
1112
|
+
return False
|
|
1113
|
+
if not isinstance(value, str):
|
|
1114
|
+
return False
|
|
1115
|
+
# Check if it's an unresolved environment variable placeholder
|
|
1116
|
+
if value.startswith("${") or value == "":
|
|
1117
|
+
return False
|
|
1118
|
+
return True
|
|
1119
|
+
|
|
1120
|
+
def get_speech_config(self) -> Dict[str, Any]:
|
|
1121
|
+
"""
|
|
1122
|
+
Get speech configuration for frontend initialization.
|
|
1123
|
+
|
|
1124
|
+
Returns:
|
|
1125
|
+
Configuration dictionary
|
|
1126
|
+
"""
|
|
1127
|
+
stt_config = self.speech_config.get("stt", {})
|
|
1128
|
+
tts_config = self.speech_config.get("tts", {})
|
|
1129
|
+
speech_tab = self.speech_config.get("speechTab", {})
|
|
1130
|
+
|
|
1131
|
+
# Check each STT provider individually
|
|
1132
|
+
stt_openai_valid = False
|
|
1133
|
+
stt_azure_valid = False
|
|
1134
|
+
if stt_config:
|
|
1135
|
+
# Check OpenAI - can be nested under 'openai' or at root level for backward compat
|
|
1136
|
+
openai_config = stt_config.get("openai", {})
|
|
1137
|
+
openai_api_key = openai_config.get("api_key") or stt_config.get("api_key")
|
|
1138
|
+
stt_openai_valid = self._is_valid_api_key(openai_api_key)
|
|
1139
|
+
|
|
1140
|
+
azure_config = stt_config.get("azure", {})
|
|
1141
|
+
stt_azure_valid = (
|
|
1142
|
+
self._is_valid_api_key(azure_config.get("api_key")) and
|
|
1143
|
+
self._is_valid_api_key(azure_config.get("region"))
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
stt_configured = stt_openai_valid or stt_azure_valid
|
|
1147
|
+
|
|
1148
|
+
# Check each TTS provider individually
|
|
1149
|
+
tts_gemini_valid = False
|
|
1150
|
+
tts_azure_valid = False
|
|
1151
|
+
tts_polly_valid = False
|
|
1152
|
+
if tts_config:
|
|
1153
|
+
# Check Gemini - can be nested under 'gemini' or at root level for backward compat
|
|
1154
|
+
gemini_nested = tts_config.get("gemini", {})
|
|
1155
|
+
gemini_api_key = gemini_nested.get("api_key") or tts_config.get("api_key")
|
|
1156
|
+
tts_gemini_valid = self._is_valid_api_key(gemini_api_key)
|
|
1157
|
+
|
|
1158
|
+
azure_config = tts_config.get("azure", {})
|
|
1159
|
+
tts_azure_valid = (
|
|
1160
|
+
self._is_valid_api_key(azure_config.get("api_key")) and
|
|
1161
|
+
self._is_valid_api_key(azure_config.get("region"))
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1164
|
+
polly_config = tts_config.get("polly", {})
|
|
1165
|
+
tts_polly_valid = (
|
|
1166
|
+
self._is_valid_api_key(polly_config.get("aws_access_key_id")) and
|
|
1167
|
+
self._is_valid_api_key(polly_config.get("aws_secret_access_key"))
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
tts_configured = tts_gemini_valid or tts_azure_valid or tts_polly_valid
|
|
1171
|
+
|
|
1172
|
+
config = {
|
|
1173
|
+
"sttExternal": stt_configured,
|
|
1174
|
+
"ttsExternal": tts_configured,
|
|
1175
|
+
# Per-provider configuration status
|
|
1176
|
+
"sttProviders": {
|
|
1177
|
+
"openai": stt_openai_valid,
|
|
1178
|
+
"azure": stt_azure_valid,
|
|
1179
|
+
},
|
|
1180
|
+
"ttsProviders": {
|
|
1181
|
+
"gemini": tts_gemini_valid,
|
|
1182
|
+
"azure": tts_azure_valid,
|
|
1183
|
+
"polly": tts_polly_valid,
|
|
1184
|
+
},
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
# Add speech tab settings if configured
|
|
1188
|
+
if speech_tab:
|
|
1189
|
+
config.update({
|
|
1190
|
+
"advancedMode": speech_tab.get("advancedMode", False),
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
# STT settings
|
|
1194
|
+
stt_settings = speech_tab.get("speechToText", {})
|
|
1195
|
+
if stt_settings:
|
|
1196
|
+
config.update({
|
|
1197
|
+
"speechToText": stt_settings.get("speechToText", True),
|
|
1198
|
+
"engineSTT": stt_settings.get("engineSTT", "browser"),
|
|
1199
|
+
"languageSTT": stt_settings.get("languageSTT", "en-US"),
|
|
1200
|
+
"autoSendText": stt_settings.get("autoSendText", -1),
|
|
1201
|
+
"autoTranscribeAudio": stt_settings.get("autoTranscribeAudio", True),
|
|
1202
|
+
"decibelValue": stt_settings.get("decibelValue", -45),
|
|
1203
|
+
})
|
|
1204
|
+
|
|
1205
|
+
# TTS settings
|
|
1206
|
+
tts_settings = speech_tab.get("textToSpeech", {})
|
|
1207
|
+
if tts_settings:
|
|
1208
|
+
config.update({
|
|
1209
|
+
"textToSpeech": tts_settings.get("textToSpeech", True),
|
|
1210
|
+
"engineTTS": tts_settings.get("engineTTS", "browser"),
|
|
1211
|
+
"voice": tts_settings.get("voice", tts_config.get("default_voice", "Kore")),
|
|
1212
|
+
"playbackRate": tts_settings.get("playbackRate", 1.0),
|
|
1213
|
+
"automaticPlayback": tts_settings.get("automaticPlayback", False),
|
|
1214
|
+
"cacheTTS": tts_settings.get("cacheTTS", True),
|
|
1215
|
+
"cloudBrowserVoices": tts_settings.get("cloudBrowserVoices", False),
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
# Conversation mode
|
|
1219
|
+
config["conversationMode"] = speech_tab.get("conversationMode", False)
|
|
1220
|
+
|
|
1221
|
+
log.debug("[AudioService] Speech config: %s", config.keys())
|
|
1222
|
+
return config
|
|
1223
|
+
|
|
1224
|
+
def _get_file_extension(self, filename: str) -> str:
|
|
1225
|
+
"""Get file extension from filename"""
|
|
1226
|
+
import os
|
|
1227
|
+
return os.path.splitext(filename)[1] or ".wav"
|