rasa-pro 3.10.16__py3-none-any.whl → 3.11.0a1__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 rasa-pro might be problematic. Click here for more details.
- README.md +396 -17
- rasa/api.py +9 -3
- rasa/cli/arguments/default_arguments.py +23 -2
- rasa/cli/arguments/run.py +15 -0
- rasa/cli/arguments/train.py +3 -9
- rasa/cli/e2e_test.py +1 -1
- rasa/cli/evaluate.py +1 -1
- rasa/cli/inspect.py +8 -4
- rasa/cli/llm_fine_tuning.py +12 -15
- rasa/cli/run.py +8 -1
- rasa/cli/studio/studio.py +8 -18
- rasa/cli/train.py +11 -53
- rasa/cli/utils.py +8 -10
- rasa/cli/x.py +1 -1
- rasa/constants.py +1 -1
- rasa/core/actions/action.py +2 -0
- rasa/core/actions/action_hangup.py +29 -0
- rasa/core/agent.py +2 -2
- rasa/core/brokers/kafka.py +3 -1
- rasa/core/brokers/pika.py +3 -1
- rasa/core/channels/__init__.py +8 -6
- rasa/core/channels/channel.py +21 -4
- rasa/core/channels/development_inspector.py +143 -46
- rasa/core/channels/inspector/README.md +1 -1
- rasa/core/channels/inspector/dist/assets/{arc-b6e548fe.js → arc-86942a71.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{c4Diagram-d0fbc5ce-fa03ac9e.js → c4Diagram-d0fbc5ce-b0290676.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{classDiagram-936ed81e-ee67392a.js → classDiagram-936ed81e-f6405f6e.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{classDiagram-v2-c3cb15f1-9b283fae.js → classDiagram-v2-c3cb15f1-ef61ac77.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{createText-62fc7601-8b6fcc2a.js → createText-62fc7601-f0411e58.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{edges-f2ad444c-22e77f4f.js → edges-f2ad444c-7dcc4f3b.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{erDiagram-9d236eb7-60ffc87f.js → erDiagram-9d236eb7-e0c092d7.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDb-1972c806-9dd802e4.js → flowDb-1972c806-fba2e3ce.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDiagram-7ea5b25a-5fa1912f.js → flowDiagram-7ea5b25a-7a70b71a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-24a5f41a.js +1 -0
- rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-abe16c3d-622a1fd2.js → flowchart-elk-definition-abe16c3d-00a59b68.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{ganttDiagram-9b5ea136-e285a63a.js → ganttDiagram-9b5ea136-293c91fa.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-99d0ae7c-f237bdca.js → gitGraphDiagram-99d0ae7c-07b2d68c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-2c4b9a3b-4b03d70e.js → index-2c4b9a3b-bc959fbd.js} +1 -1
- rasa/core/channels/inspector/dist/assets/index-3a8a5a28.js +1317 -0
- rasa/core/channels/inspector/dist/assets/{infoDiagram-736b4530-72a0fa5f.js → infoDiagram-736b4530-4a350f72.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{journeyDiagram-df861f2b-82218c41.js → journeyDiagram-df861f2b-af464fb7.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{layout-78cff630.js → layout-0071f036.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{line-5038b469.js → line-2f73cc83.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{linear-c4fc4098.js → linear-f014b4cc.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{mindmap-definition-beec6740-c33c8ea6.js → mindmap-definition-beec6740-d2426fb6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{pieDiagram-dbbf0591-a8d03059.js → pieDiagram-dbbf0591-776f01a2.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{quadrantDiagram-4d7f4fd6-6a0e56b2.js → quadrantDiagram-4d7f4fd6-82e00b57.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{requirementDiagram-6fc4c22a-2dc7c7bd.js → requirementDiagram-6fc4c22a-ea13c6bb.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sankeyDiagram-8f13d901-2360fe39.js → sankeyDiagram-8f13d901-1feca7e9.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sequenceDiagram-b655622a-41b9f9ad.js → sequenceDiagram-b655622a-070c61d2.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-59f0c015-0aad326f.js → stateDiagram-59f0c015-24f46263.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-2b26beab-9847d984.js → stateDiagram-v2-2b26beab-c9056051.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-080da4f6-564d890e.js → styles-080da4f6-08abc34a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-3dcbcfbf-38957613.js → styles-3dcbcfbf-bc74c25a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-9c745c82-f0fc6921.js → styles-9c745c82-4e5d66de.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{svgDrawCommon-4835440b-ef3c5a77.js → svgDrawCommon-4835440b-849c4517.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{timeline-definition-5b62e21b-bf3e91c1.js → timeline-definition-5b62e21b-d0fb1598.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{xychartDiagram-2b33534f-4d4026c0.js → xychartDiagram-2b33534f-04d115e2.js} +1 -1
- rasa/core/channels/inspector/dist/index.html +18 -17
- rasa/core/channels/inspector/index.html +17 -16
- rasa/core/channels/inspector/package.json +5 -1
- rasa/core/channels/inspector/src/App.tsx +117 -67
- rasa/core/channels/inspector/src/components/Chat.tsx +95 -0
- rasa/core/channels/inspector/src/components/DiagramFlow.tsx +11 -10
- rasa/core/channels/inspector/src/components/DialogueStack.tsx +10 -25
- rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +1 -1
- rasa/core/channels/inspector/src/helpers/formatters.test.ts +10 -0
- rasa/core/channels/inspector/src/helpers/formatters.ts +107 -41
- rasa/core/channels/inspector/src/helpers/utils.ts +92 -7
- rasa/core/channels/inspector/src/types.ts +21 -1
- rasa/core/channels/inspector/yarn.lock +94 -1
- rasa/core/channels/rest.py +51 -46
- rasa/core/channels/socketio.py +22 -0
- rasa/core/channels/{audiocodes.py → voice_ready/audiocodes.py} +110 -68
- rasa/core/channels/{voice_aware → voice_ready}/jambonz.py +11 -4
- rasa/core/channels/{voice_aware → voice_ready}/jambonz_protocol.py +57 -5
- rasa/core/channels/{twilio_voice.py → voice_ready/twilio_voice.py} +58 -7
- rasa/core/channels/{voice_aware → voice_ready}/utils.py +16 -0
- rasa/core/channels/voice_stream/asr/__init__.py +0 -0
- rasa/core/channels/voice_stream/asr/asr_engine.py +71 -0
- rasa/core/channels/voice_stream/asr/asr_event.py +13 -0
- rasa/core/channels/voice_stream/asr/deepgram.py +77 -0
- rasa/core/channels/voice_stream/audio_bytes.py +7 -0
- rasa/core/channels/voice_stream/tts/__init__.py +0 -0
- rasa/core/channels/voice_stream/tts/azure.py +100 -0
- rasa/core/channels/voice_stream/tts/cartesia.py +114 -0
- rasa/core/channels/voice_stream/tts/tts_cache.py +27 -0
- rasa/core/channels/voice_stream/tts/tts_engine.py +48 -0
- rasa/core/channels/voice_stream/twilio_media_streams.py +164 -0
- rasa/core/channels/voice_stream/util.py +57 -0
- rasa/core/channels/voice_stream/voice_channel.py +247 -0
- rasa/core/featurizers/single_state_featurizer.py +1 -22
- rasa/core/featurizers/tracker_featurizers.py +18 -115
- rasa/core/nlg/contextual_response_rephraser.py +11 -2
- rasa/{nlu → core}/persistor.py +16 -38
- rasa/core/policies/enterprise_search_policy.py +12 -15
- rasa/core/policies/flows/flow_executor.py +8 -18
- rasa/core/policies/intentless_policy.py +10 -15
- rasa/core/policies/ted_policy.py +33 -58
- rasa/core/policies/unexpected_intent_policy.py +7 -15
- rasa/core/processor.py +13 -64
- rasa/core/run.py +11 -1
- rasa/core/secrets_manager/constants.py +4 -0
- rasa/core/secrets_manager/factory.py +8 -0
- rasa/core/secrets_manager/vault.py +11 -1
- rasa/core/training/interactive.py +1 -1
- rasa/core/utils.py +1 -11
- rasa/dialogue_understanding/coexistence/llm_based_router.py +10 -10
- rasa/dialogue_understanding/commands/__init__.py +2 -0
- rasa/dialogue_understanding/commands/change_flow_command.py +0 -6
- rasa/dialogue_understanding/commands/session_end_command.py +61 -0
- rasa/dialogue_understanding/generator/flow_retrieval.py +0 -7
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +12 -3
- rasa/dialogue_understanding/generator/llm_command_generator.py +1 -1
- rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +3 -28
- rasa/dialogue_understanding/generator/nlu_command_adapter.py +1 -19
- rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +4 -37
- rasa/e2e_test/aggregate_test_stats_calculator.py +1 -11
- rasa/e2e_test/assertions.py +6 -48
- rasa/e2e_test/e2e_test_runner.py +6 -9
- rasa/e2e_test/utils/e2e_yaml_utils.py +1 -1
- rasa/e2e_test/utils/io.py +1 -3
- rasa/engine/graph.py +3 -10
- rasa/engine/recipes/config_files/default_config.yml +0 -3
- rasa/engine/recipes/default_recipe.py +0 -1
- rasa/engine/recipes/graph_recipe.py +0 -1
- rasa/engine/runner/dask.py +2 -2
- rasa/engine/storage/local_model_storage.py +12 -42
- rasa/engine/storage/storage.py +1 -5
- rasa/engine/validation.py +1 -78
- rasa/keys +1 -0
- rasa/model_training.py +13 -16
- rasa/nlu/classifiers/diet_classifier.py +25 -38
- rasa/nlu/classifiers/logistic_regression_classifier.py +9 -22
- rasa/nlu/classifiers/sklearn_intent_classifier.py +16 -37
- rasa/nlu/extractors/crf_entity_extractor.py +50 -93
- rasa/nlu/featurizers/sparse_featurizer/count_vectors_featurizer.py +16 -45
- rasa/nlu/featurizers/sparse_featurizer/lexical_syntactic_featurizer.py +17 -52
- rasa/nlu/featurizers/sparse_featurizer/regex_featurizer.py +3 -5
- rasa/server.py +1 -1
- rasa/shared/constants.py +3 -12
- rasa/shared/core/constants.py +4 -0
- rasa/shared/core/domain.py +101 -47
- rasa/shared/core/events.py +29 -0
- rasa/shared/core/flows/flows_list.py +20 -11
- rasa/shared/core/flows/validation.py +25 -0
- rasa/shared/core/flows/yaml_flows_io.py +3 -24
- rasa/shared/importers/importer.py +40 -39
- rasa/shared/importers/multi_project.py +23 -11
- rasa/shared/importers/rasa.py +7 -2
- rasa/shared/importers/remote_importer.py +196 -0
- rasa/shared/importers/utils.py +3 -1
- rasa/shared/nlu/training_data/features.py +2 -120
- rasa/shared/nlu/training_data/training_data.py +18 -19
- rasa/shared/providers/_configs/azure_openai_client_config.py +3 -5
- rasa/shared/providers/embedding/_base_litellm_embedding_client.py +1 -6
- rasa/shared/providers/llm/_base_litellm_client.py +11 -31
- rasa/shared/providers/llm/self_hosted_llm_client.py +3 -15
- rasa/shared/utils/common.py +3 -22
- rasa/shared/utils/io.py +0 -1
- rasa/shared/utils/llm.py +30 -27
- rasa/shared/utils/schemas/events.py +2 -0
- rasa/shared/utils/schemas/model_config.yml +0 -10
- rasa/shared/utils/yaml.py +44 -0
- rasa/studio/auth.py +5 -3
- rasa/studio/config.py +4 -13
- rasa/studio/constants.py +0 -1
- rasa/studio/data_handler.py +3 -10
- rasa/studio/upload.py +8 -17
- rasa/tracing/instrumentation/attribute_extractors.py +1 -1
- rasa/utils/io.py +66 -0
- rasa/utils/tensorflow/model_data.py +193 -2
- rasa/validator.py +0 -12
- rasa/version.py +1 -1
- rasa_pro-3.11.0a1.dist-info/METADATA +576 -0
- {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0a1.dist-info}/RECORD +181 -164
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-1844e5a5.js +0 -1
- rasa/core/channels/inspector/dist/assets/index-a5d3e69d.js +0 -1040
- rasa/utils/tensorflow/feature_array.py +0 -366
- rasa_pro-3.10.16.dist-info/METADATA +0 -196
- /rasa/core/channels/{voice_aware → voice_ready}/__init__.py +0 -0
- /rasa/core/channels/{voice_native → voice_stream}/__init__.py +0 -0
- {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0a1.dist-info}/NOTICE +0 -0
- {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0a1.dist-info}/WHEEL +0 -0
- {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0a1.dist-info}/entry_points.txt +0 -0
|
@@ -2,11 +2,12 @@ from typing import Any, Awaitable, Callable, Dict, Optional, Text
|
|
|
2
2
|
|
|
3
3
|
import structlog
|
|
4
4
|
from rasa.core.channels.channel import InputChannel, OutputChannel, UserMessage
|
|
5
|
-
from rasa.core.channels.
|
|
5
|
+
from rasa.core.channels.voice_ready.jambonz_protocol import (
|
|
6
6
|
send_ws_text_message,
|
|
7
7
|
websocket_message_handler,
|
|
8
|
+
send_ws_hangup_message,
|
|
8
9
|
)
|
|
9
|
-
from rasa.core.channels.
|
|
10
|
+
from rasa.core.channels.voice_ready.utils import validate_voice_license_scope
|
|
10
11
|
from rasa.shared.exceptions import RasaException
|
|
11
12
|
from sanic import Blueprint, response, Websocket # type: ignore[attr-defined]
|
|
12
13
|
from sanic.request import Request
|
|
@@ -19,8 +20,10 @@ structlogger = structlog.get_logger()
|
|
|
19
20
|
|
|
20
21
|
CHANNEL_NAME = "jambonz"
|
|
21
22
|
|
|
23
|
+
DEFAULT_HANGUP_DELAY_SECONDS = 1
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
|
|
26
|
+
class JambonzVoiceReadyInput(InputChannel):
|
|
24
27
|
"""Connector for the Jambonz platform."""
|
|
25
28
|
|
|
26
29
|
@classmethod
|
|
@@ -32,7 +35,7 @@ class JambonzVoiceAwareInput(InputChannel):
|
|
|
32
35
|
return cls()
|
|
33
36
|
|
|
34
37
|
def __init__(self) -> None:
|
|
35
|
-
"""Initializes the
|
|
38
|
+
"""Initializes the JambonzVoiceReadyInput channel."""
|
|
36
39
|
mark_as_experimental_feature("Jambonz Channel")
|
|
37
40
|
validate_voice_license_scope()
|
|
38
41
|
|
|
@@ -101,3 +104,7 @@ class JambonzWebsocketOutput(OutputChannel):
|
|
|
101
104
|
) -> None:
|
|
102
105
|
"""Send an activity."""
|
|
103
106
|
await self.add_message(json_message)
|
|
107
|
+
|
|
108
|
+
async def hangup(self, recipient_id: Text, **kwargs: Any) -> None:
|
|
109
|
+
"""Indicate that the conversation should be ended."""
|
|
110
|
+
await send_ws_hangup_message(DEFAULT_HANGUP_DELAY_SECONDS, self.ws)
|
|
@@ -5,6 +5,8 @@ from typing import Any, Awaitable, Callable, Dict, List, Text
|
|
|
5
5
|
|
|
6
6
|
import structlog
|
|
7
7
|
from rasa.core.channels.channel import UserMessage
|
|
8
|
+
from rasa.core.channels.voice_ready.utils import CallParameters
|
|
9
|
+
from dataclasses import asdict
|
|
8
10
|
from sanic import Websocket # type: ignore[attr-defined]
|
|
9
11
|
|
|
10
12
|
|
|
@@ -17,12 +19,20 @@ class NewSessionMessage:
|
|
|
17
19
|
|
|
18
20
|
call_sid: str
|
|
19
21
|
message_id: str
|
|
22
|
+
call_params: CallParameters
|
|
20
23
|
|
|
21
24
|
@staticmethod
|
|
22
25
|
def from_message(message: Dict[str, Any]) -> "NewSessionMessage":
|
|
26
|
+
structlogger.debug("jambonz.websocket.message.new_session", message=message)
|
|
27
|
+
call_params = CallParameters(
|
|
28
|
+
call_id=message.get("call_sid"),
|
|
29
|
+
user_phone=message.get("data", {}).get("from"),
|
|
30
|
+
bot_phone=message.get("data", {}).get("to"),
|
|
31
|
+
)
|
|
23
32
|
return NewSessionMessage(
|
|
24
33
|
message.get("call_sid"),
|
|
25
34
|
message.get("msgid"),
|
|
35
|
+
call_params,
|
|
26
36
|
)
|
|
27
37
|
|
|
28
38
|
|
|
@@ -82,6 +92,10 @@ class CallStatusChanged:
|
|
|
82
92
|
|
|
83
93
|
@staticmethod
|
|
84
94
|
def from_message(message: Dict[str, Any]) -> "CallStatusChanged":
|
|
95
|
+
structlogger.debug(
|
|
96
|
+
"jambonz.websocket.message.call_status_changed",
|
|
97
|
+
message=message,
|
|
98
|
+
)
|
|
85
99
|
return CallStatusChanged(
|
|
86
100
|
message.get("call_sid"), message.get("data", {}).get("call_status")
|
|
87
101
|
)
|
|
@@ -145,7 +159,7 @@ async def websocket_message_handler(
|
|
|
145
159
|
await handle_session_reconnect(session_reconnect)
|
|
146
160
|
elif message.get("type") == "call:status":
|
|
147
161
|
call_status = CallStatusChanged.from_message(message)
|
|
148
|
-
await handle_call_status(call_status)
|
|
162
|
+
await handle_call_status(call_status, on_new_message, ws)
|
|
149
163
|
elif message.get("type") == "verb:hook" and message.get("hook") == "/gather":
|
|
150
164
|
hook_trigger_reason = message.get("data", {}).get("reason")
|
|
151
165
|
|
|
@@ -184,7 +198,7 @@ async def handle_new_session(
|
|
|
184
198
|
ws: Websocket,
|
|
185
199
|
) -> None:
|
|
186
200
|
"""Handle new session message."""
|
|
187
|
-
from rasa.core.channels.
|
|
201
|
+
from rasa.core.channels.voice_ready.jambonz import JambonzWebsocketOutput
|
|
188
202
|
|
|
189
203
|
structlogger.debug("jambonz.websocket.message.new_call", call_sid=message.call_sid)
|
|
190
204
|
output_channel = JambonzWebsocketOutput(ws, message.call_sid)
|
|
@@ -192,7 +206,7 @@ async def handle_new_session(
|
|
|
192
206
|
text="/session_start",
|
|
193
207
|
output_channel=output_channel,
|
|
194
208
|
sender_id=message.call_sid,
|
|
195
|
-
metadata=
|
|
209
|
+
metadata=asdict(message.call_params),
|
|
196
210
|
)
|
|
197
211
|
await send_config_ack(message.message_id, ws)
|
|
198
212
|
await on_new_message(user_msg)
|
|
@@ -208,7 +222,7 @@ async def handle_gather_completed(
|
|
|
208
222
|
|
|
209
223
|
This includes results of gather calles with their transcription.
|
|
210
224
|
"""
|
|
211
|
-
from rasa.core.channels.
|
|
225
|
+
from rasa.core.channels.voice_ready.jambonz import JambonzWebsocketOutput
|
|
212
226
|
|
|
213
227
|
if not transcript_result.is_final:
|
|
214
228
|
# in case of a non final transcript, we are going to wait for the final
|
|
@@ -256,7 +270,11 @@ async def handle_gather_timeout(gather_timeout: GatherTimeout, ws: Websocket) ->
|
|
|
256
270
|
await send_gather_input(ws)
|
|
257
271
|
|
|
258
272
|
|
|
259
|
-
async def handle_call_status(
|
|
273
|
+
async def handle_call_status(
|
|
274
|
+
call_status: CallStatusChanged,
|
|
275
|
+
on_new_message: Callable[[UserMessage], Awaitable[Any]],
|
|
276
|
+
ws: Websocket,
|
|
277
|
+
) -> None:
|
|
260
278
|
"""Handle changes in the call status."""
|
|
261
279
|
structlogger.debug(
|
|
262
280
|
"jambonz.websocket.message.call_status_changed",
|
|
@@ -264,6 +282,19 @@ async def handle_call_status(call_status: CallStatusChanged) -> None:
|
|
|
264
282
|
message=call_status.status,
|
|
265
283
|
)
|
|
266
284
|
|
|
285
|
+
if call_status.status == "completed":
|
|
286
|
+
structlogger.debug("jambonz.websocket.message.call_completed")
|
|
287
|
+
from rasa.core.channels.voice_ready.jambonz import JambonzWebsocketOutput
|
|
288
|
+
|
|
289
|
+
output_channel = JambonzWebsocketOutput(ws, call_status.call_sid)
|
|
290
|
+
user_msg = UserMessage(
|
|
291
|
+
text="/session_end",
|
|
292
|
+
output_channel=output_channel,
|
|
293
|
+
sender_id=call_status.call_sid,
|
|
294
|
+
metadata={},
|
|
295
|
+
)
|
|
296
|
+
await on_new_message(user_msg)
|
|
297
|
+
|
|
267
298
|
|
|
268
299
|
async def handle_session_reconnect(session_reconnect: SessionReconnect) -> None:
|
|
269
300
|
"""Handle session reconnect message."""
|
|
@@ -301,6 +332,7 @@ async def send_config_ack(message_id: str, ws: Websocket) -> None:
|
|
|
301
332
|
|
|
302
333
|
async def send_gather_input(ws: Websocket) -> None:
|
|
303
334
|
"""Send a gather input command to jambonz."""
|
|
335
|
+
structlogger.debug("jambonz.websocket.send.gather")
|
|
304
336
|
await ws.send(
|
|
305
337
|
json.dumps(
|
|
306
338
|
{
|
|
@@ -342,3 +374,23 @@ async def send_ws_text_message(ws: Websocket, text: Text) -> None:
|
|
|
342
374
|
}
|
|
343
375
|
)
|
|
344
376
|
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
async def send_ws_hangup_message(hangup_delay_seconds: int, ws: Websocket) -> None:
|
|
380
|
+
"""Send a hangup message to the websocket using the jambonz interface."""
|
|
381
|
+
structlogger.debug("jambonz.websocket.send.hangup")
|
|
382
|
+
await ws.send(
|
|
383
|
+
json.dumps(
|
|
384
|
+
{
|
|
385
|
+
"type": "command",
|
|
386
|
+
"command": "redirect",
|
|
387
|
+
"queueCommand": True,
|
|
388
|
+
"data": [
|
|
389
|
+
{"pause": {"length": hangup_delay_seconds}},
|
|
390
|
+
{
|
|
391
|
+
"hangup": {},
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
}
|
|
395
|
+
)
|
|
396
|
+
)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from sanic import Blueprint, response
|
|
2
|
-
from sanic.request import Request
|
|
2
|
+
from sanic.request import Request, RequestParameters
|
|
3
3
|
from sanic.response import HTTPResponse
|
|
4
4
|
from twilio.twiml.voice_response import VoiceResponse, Gather
|
|
5
5
|
from typing import Text, Callable, Awaitable, List, Any, Dict, Optional
|
|
6
|
+
from dataclasses import asdict
|
|
6
7
|
|
|
8
|
+
import structlog
|
|
7
9
|
import rasa.utils.io
|
|
8
10
|
import rasa.shared.utils.io
|
|
9
11
|
from rasa.shared.core.events import BotUttered
|
|
@@ -13,6 +15,19 @@ from rasa.core.channels.channel import (
|
|
|
13
15
|
CollectingOutputChannel,
|
|
14
16
|
UserMessage,
|
|
15
17
|
)
|
|
18
|
+
from rasa.core.channels.voice_ready.utils import CallParameters
|
|
19
|
+
|
|
20
|
+
logger = structlog.get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def map_call_params(form: RequestParameters) -> CallParameters:
|
|
24
|
+
"""Map the Audiocodes parameters to the CallParameters dataclass."""
|
|
25
|
+
return CallParameters(
|
|
26
|
+
call_id=form.get("CallSid"),
|
|
27
|
+
user_phone=form.get("Caller"),
|
|
28
|
+
bot_phone=form.get("Called"),
|
|
29
|
+
direction=form.get("Direction"),
|
|
30
|
+
)
|
|
16
31
|
|
|
17
32
|
|
|
18
33
|
class TwilioVoiceInput(InputChannel):
|
|
@@ -105,7 +120,6 @@ class TwilioVoiceInput(InputChannel):
|
|
|
105
120
|
credentials = credentials or {}
|
|
106
121
|
|
|
107
122
|
return cls(
|
|
108
|
-
credentials.get("initial_prompt", "hello"),
|
|
109
123
|
credentials.get(
|
|
110
124
|
"reprompt_fallback_phrase",
|
|
111
125
|
"I'm sorry I didn't get that could you rephrase.",
|
|
@@ -118,7 +132,6 @@ class TwilioVoiceInput(InputChannel):
|
|
|
118
132
|
|
|
119
133
|
def __init__(
|
|
120
134
|
self,
|
|
121
|
-
initial_prompt: Optional[Text],
|
|
122
135
|
reprompt_fallback_phrase: Optional[Text],
|
|
123
136
|
assistant_voice: Optional[Text],
|
|
124
137
|
speech_timeout: Text = "5",
|
|
@@ -128,14 +141,12 @@ class TwilioVoiceInput(InputChannel):
|
|
|
128
141
|
"""Creates a connection to Twilio voice.
|
|
129
142
|
|
|
130
143
|
Args:
|
|
131
|
-
initial_prompt: text to use to prompt a conversation when call is answered.
|
|
132
144
|
reprompt_fallback_phrase: phrase to use if no user response.
|
|
133
145
|
assistant_voice: name of the assistant voice to use.
|
|
134
146
|
speech_timeout: how long to pause when user finished speaking.
|
|
135
147
|
speech_model: type of transcription model to use from Twilio.
|
|
136
148
|
enhanced: toggle to use Twilio's premium speech transcription model.
|
|
137
149
|
"""
|
|
138
|
-
self.initial_prompt = initial_prompt
|
|
139
150
|
self.reprompt_fallback_phrase = reprompt_fallback_phrase
|
|
140
151
|
self.assistant_voice = assistant_voice
|
|
141
152
|
self.speech_timeout = speech_timeout
|
|
@@ -239,22 +250,43 @@ class TwilioVoiceInput(InputChannel):
|
|
|
239
250
|
text = request.form.get("SpeechResult")
|
|
240
251
|
input_channel = self.name()
|
|
241
252
|
call_status = request.form.get("CallStatus")
|
|
253
|
+
metadata = {}
|
|
242
254
|
|
|
243
255
|
collector = TwilioVoiceCollectingOutputChannel()
|
|
244
256
|
|
|
257
|
+
logger.debug(
|
|
258
|
+
"twilio_voice.webhook",
|
|
259
|
+
sender_id=sender_id,
|
|
260
|
+
text=text,
|
|
261
|
+
call_status=call_status,
|
|
262
|
+
)
|
|
245
263
|
# Provide an initial greeting to answer the user's call.
|
|
246
264
|
if (text is None) and (call_status == "ringing"):
|
|
247
|
-
text =
|
|
265
|
+
text = "/session_start"
|
|
266
|
+
metadata = asdict(map_call_params(request.form))
|
|
267
|
+
|
|
268
|
+
# when call is disconnected
|
|
269
|
+
if call_status == "completed":
|
|
270
|
+
text = "/session_end"
|
|
271
|
+
metadata = {"reason": "user disconnected"}
|
|
248
272
|
|
|
249
273
|
# determine the response.
|
|
250
274
|
if text is not None:
|
|
275
|
+
logger.info("twilio_voice.webhook.text_not_none", sender_id=sender_id)
|
|
251
276
|
await on_new_message(
|
|
252
|
-
UserMessage(
|
|
277
|
+
UserMessage(
|
|
278
|
+
text,
|
|
279
|
+
collector,
|
|
280
|
+
sender_id,
|
|
281
|
+
input_channel=input_channel,
|
|
282
|
+
metadata=metadata,
|
|
283
|
+
)
|
|
253
284
|
)
|
|
254
285
|
|
|
255
286
|
twilio_response = self._build_twilio_voice_response(collector.messages)
|
|
256
287
|
# If the user doesn't respond resend the last message.
|
|
257
288
|
else:
|
|
289
|
+
logger.info("twilio_voice.webhook.text_none", sender_id=sender_id)
|
|
258
290
|
# Get last user utterance from tracker.
|
|
259
291
|
tracker = await request.app.ctx.agent.tracker_store.retrieve(sender_id)
|
|
260
292
|
last_response = None
|
|
@@ -285,6 +317,7 @@ class TwilioVoiceInput(InputChannel):
|
|
|
285
317
|
self, messages: List[Dict[Text, Any]]
|
|
286
318
|
) -> VoiceResponse:
|
|
287
319
|
"""Builds the Twilio Voice Response object."""
|
|
320
|
+
logger.debug("twilio_voice.build_twilio_voice_response", messages=messages)
|
|
288
321
|
voice_response = VoiceResponse()
|
|
289
322
|
gather = Gather(
|
|
290
323
|
input="speech",
|
|
@@ -299,6 +332,11 @@ class TwilioVoiceInput(InputChannel):
|
|
|
299
332
|
# Add a listener to the last message to listen for user response.
|
|
300
333
|
for i, message in enumerate(messages):
|
|
301
334
|
msg_text = message["text"]
|
|
335
|
+
# Check if the message is a hangup message.
|
|
336
|
+
if message.get("custom", {}).get("hangup"):
|
|
337
|
+
voice_response.hangup()
|
|
338
|
+
break
|
|
339
|
+
|
|
302
340
|
if i + 1 == len(messages):
|
|
303
341
|
gather.say(msg_text, voice=self.assistant_voice)
|
|
304
342
|
voice_response.append(gather)
|
|
@@ -365,3 +403,16 @@ class TwilioVoiceCollectingOutputChannel(CollectingOutputChannel):
|
|
|
365
403
|
"with a visual elements such as images and emojis "
|
|
366
404
|
"that are used in your voice channel."
|
|
367
405
|
)
|
|
406
|
+
|
|
407
|
+
async def hangup(self, recipient_id: Text, **kwargs: Any) -> None:
|
|
408
|
+
"""
|
|
409
|
+
Indicate that the conversation should be ended.
|
|
410
|
+
|
|
411
|
+
Parent class is a collecting output channel, so we don't actually hang up
|
|
412
|
+
but we add a custom message to the list of messages to be sent.
|
|
413
|
+
This message will be picked up by _build_twilio_voice_response
|
|
414
|
+
which will hang up the call.
|
|
415
|
+
"""
|
|
416
|
+
await self._persist_message(
|
|
417
|
+
self._message(recipient_id, custom={"hangup": True})
|
|
418
|
+
)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import structlog
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
2
4
|
|
|
3
5
|
from rasa.utils.licensing import (
|
|
4
6
|
PRODUCT_AREA,
|
|
@@ -18,3 +20,17 @@ def validate_voice_license_scope() -> None:
|
|
|
18
20
|
|
|
19
21
|
voice_product_scope = PRODUCT_AREA + " " + VOICE_SCOPE
|
|
20
22
|
validate_license_from_env(product_area=voice_product_scope)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CallParameters:
|
|
27
|
+
"""Standardized call parameters for voice channels."""
|
|
28
|
+
|
|
29
|
+
call_id: str
|
|
30
|
+
user_phone: str
|
|
31
|
+
bot_phone: str
|
|
32
|
+
user_name: Optional[str] = None
|
|
33
|
+
user_host: Optional[str] = None
|
|
34
|
+
bot_host: Optional[str] = None
|
|
35
|
+
direction: Optional[str] = None
|
|
36
|
+
stream_id: Optional[str] = None
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Dict, AsyncIterator, Any, Generic, Optional, Type, TypeVar
|
|
3
|
+
|
|
4
|
+
from websockets.legacy.client import WebSocketClientProtocol
|
|
5
|
+
|
|
6
|
+
from rasa.core.channels.voice_stream.asr.asr_event import ASREvent
|
|
7
|
+
from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
|
|
8
|
+
from rasa.core.channels.voice_stream.util import MergeableConfig
|
|
9
|
+
from rasa.shared.exceptions import ConnectionException
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound="ASREngineConfig")
|
|
12
|
+
E = TypeVar("E", bound="ASREngine")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ASREngineConfig(MergeableConfig):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ASREngine(Generic[T]):
|
|
21
|
+
def __init__(self, config: Optional[T] = None):
|
|
22
|
+
self.config = self.get_default_config().merge(config)
|
|
23
|
+
self.asr_socket: Optional[WebSocketClientProtocol] = None
|
|
24
|
+
|
|
25
|
+
async def connect(self) -> None:
|
|
26
|
+
self.asr_socket = await self.open_websocket_connection()
|
|
27
|
+
|
|
28
|
+
async def open_websocket_connection(self) -> WebSocketClientProtocol:
|
|
29
|
+
"""Connect to the ASR system."""
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_config_dict(cls: Type[E], config: Dict) -> E:
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
async def close_connection(self) -> None:
|
|
37
|
+
if self.asr_socket:
|
|
38
|
+
await self.asr_socket.close()
|
|
39
|
+
|
|
40
|
+
async def signal_audio_done(self) -> None:
|
|
41
|
+
"""Signal to the ASR Api that you are done sending data."""
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
async def send_audio_chunks(self, chunk: RasaAudioBytes) -> None:
|
|
45
|
+
"""Send audio chunks to the ASR system via the websocket."""
|
|
46
|
+
if self.asr_socket is None:
|
|
47
|
+
raise ConnectionException("Websocket not connected.")
|
|
48
|
+
engine_bytes = self.rasa_audio_bytes_to_engine_bytes(chunk)
|
|
49
|
+
await self.asr_socket.send(engine_bytes)
|
|
50
|
+
|
|
51
|
+
def rasa_audio_bytes_to_engine_bytes(self, chunk: RasaAudioBytes) -> bytes:
|
|
52
|
+
"""Convert RasaAudioBytes to bytes usable by this engine."""
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
|
|
55
|
+
async def stream_asr_events(self) -> AsyncIterator[ASREvent]:
|
|
56
|
+
"""Stream the events returned by the ASR system as it is fed audio bytes."""
|
|
57
|
+
if self.asr_socket is None:
|
|
58
|
+
raise ConnectionException("Websocket not connected.")
|
|
59
|
+
async for message in self.asr_socket:
|
|
60
|
+
asr_event = self.engine_event_to_asr_event(message)
|
|
61
|
+
if asr_event:
|
|
62
|
+
yield asr_event
|
|
63
|
+
|
|
64
|
+
def engine_event_to_asr_event(self, e: Any) -> Optional[ASREvent]:
|
|
65
|
+
"""Translate an engine event to a common ASREvent."""
|
|
66
|
+
raise NotImplementedError
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def get_default_config() -> T:
|
|
70
|
+
"""Get the default config for this component."""
|
|
71
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import websockets
|
|
7
|
+
from websockets.legacy.client import WebSocketClientProtocol
|
|
8
|
+
|
|
9
|
+
from rasa.core.channels.voice_stream.asr.asr_engine import ASREngine, ASREngineConfig
|
|
10
|
+
from rasa.core.channels.voice_stream.asr.asr_event import ASREvent, NewTranscript
|
|
11
|
+
from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
|
|
12
|
+
|
|
13
|
+
DEEPGRAM_API_KEY = "DEEPGRAM_API_KEY"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class DeepgramASRConfig(ASREngineConfig):
|
|
18
|
+
endpoint: Optional[str] = None
|
|
19
|
+
# number of miliseconds of silence to determine end of speech
|
|
20
|
+
endpointing: Optional[int] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DeepgramASR(ASREngine[DeepgramASRConfig]):
|
|
24
|
+
def __init__(self, config: Optional[DeepgramASRConfig] = None):
|
|
25
|
+
super().__init__(config)
|
|
26
|
+
self.accumulated_transcript = ""
|
|
27
|
+
|
|
28
|
+
async def open_websocket_connection(self) -> WebSocketClientProtocol:
|
|
29
|
+
"""Connect to the ASR system."""
|
|
30
|
+
deepgram_api_key = os.environ.get(DEEPGRAM_API_KEY)
|
|
31
|
+
extra_headers = {"Authorization": f"Token {deepgram_api_key}"}
|
|
32
|
+
api_url = self._get_api_url()
|
|
33
|
+
query_params = self._get_query_params()
|
|
34
|
+
return await websockets.connect( # type: ignore
|
|
35
|
+
api_url + query_params,
|
|
36
|
+
extra_headers=extra_headers,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def _get_api_url(self) -> str:
|
|
40
|
+
return f"wss://{self.config.endpoint}/v1/listen?"
|
|
41
|
+
|
|
42
|
+
def _get_query_params(self) -> str:
|
|
43
|
+
return (
|
|
44
|
+
f"encoding=mulaw&sample_rate=8000&endpointing={self.config.endpointing}"
|
|
45
|
+
f"&vad_events=true"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async def signal_audio_done(self) -> None:
|
|
49
|
+
"""Signal to the ASR Api that you are done sending data."""
|
|
50
|
+
if self.asr_socket is None:
|
|
51
|
+
raise AttributeError("Websocket not connected.")
|
|
52
|
+
await self.asr_socket.send(json.dumps({"type": "CloseStream"}))
|
|
53
|
+
|
|
54
|
+
def rasa_audio_bytes_to_engine_bytes(self, chunk: RasaAudioBytes) -> bytes:
|
|
55
|
+
"""Convert RasaAudioBytes to bytes usable by this engine."""
|
|
56
|
+
return chunk
|
|
57
|
+
|
|
58
|
+
def engine_event_to_asr_event(self, e: Any) -> Optional[ASREvent]:
|
|
59
|
+
"""Translate an engine event to a common ASREvent."""
|
|
60
|
+
data = json.loads(e)
|
|
61
|
+
if data.get("is_final"):
|
|
62
|
+
transcript = data["channel"]["alternatives"][0]["transcript"]
|
|
63
|
+
if data.get("speech_final"):
|
|
64
|
+
full_transcript = self.accumulated_transcript + transcript
|
|
65
|
+
self.accumulated_transcript = ""
|
|
66
|
+
return NewTranscript(full_transcript)
|
|
67
|
+
else:
|
|
68
|
+
self.accumulated_transcript += transcript
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def get_default_config() -> DeepgramASRConfig:
|
|
73
|
+
return DeepgramASRConfig("api.deepgram.com", 400)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_config_dict(cls, config: Dict) -> "DeepgramASR":
|
|
77
|
+
return DeepgramASR(DeepgramASRConfig.from_dict(config))
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from typing import NewType
|
|
2
|
+
|
|
3
|
+
# a common intermediate audio byte format that acts as a common data format,
|
|
4
|
+
# to prevent quadratic complexity between formats of channels, asr engines,
|
|
5
|
+
# and tts engines
|
|
6
|
+
# currently corresponds to raw wave, 8khz, 8bit, mono channel, mulaw encoding
|
|
7
|
+
RasaAudioBytes = NewType("RasaAudioBytes", bytes)
|
|
File without changes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import AsyncIterator, Dict, Optional
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
import structlog
|
|
7
|
+
from aiohttp import ClientConnectorError
|
|
8
|
+
|
|
9
|
+
from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
|
|
10
|
+
from rasa.core.channels.voice_stream.tts.tts_engine import (
|
|
11
|
+
TTSEngine,
|
|
12
|
+
TTSEngineConfig,
|
|
13
|
+
TTSError,
|
|
14
|
+
)
|
|
15
|
+
from rasa.shared.exceptions import ConnectionException
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
structlogger = structlog.get_logger()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class AzureTTSConfig(TTSEngineConfig):
|
|
23
|
+
speech_region: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AzureTTS(TTSEngine[AzureTTSConfig]):
|
|
27
|
+
session: Optional[aiohttp.ClientSession] = None
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: Optional[AzureTTSConfig] = None):
|
|
30
|
+
super().__init__(config)
|
|
31
|
+
# Have to create this class-shared session lazily at run time otherwise
|
|
32
|
+
# the async event loop doesn't work
|
|
33
|
+
if self.__class__.session is None or self.__class__.session.closed:
|
|
34
|
+
self.__class__.session = aiohttp.ClientSession()
|
|
35
|
+
|
|
36
|
+
async def synthesize(
|
|
37
|
+
self, text: str, config: Optional[AzureTTSConfig] = None
|
|
38
|
+
) -> AsyncIterator[RasaAudioBytes]:
|
|
39
|
+
"""Generate speech from text using a remote TTS system."""
|
|
40
|
+
config = self.config.merge(config)
|
|
41
|
+
azure_speech_url = self.get_tts_endpoint(config)
|
|
42
|
+
headers = self.get_request_headers()
|
|
43
|
+
body = self.create_request_body(text, config)
|
|
44
|
+
if self.session is None:
|
|
45
|
+
raise ConnectionException("Client session is not initialized")
|
|
46
|
+
try:
|
|
47
|
+
async with self.session.post(
|
|
48
|
+
azure_speech_url, headers=headers, data=body, chunked=True
|
|
49
|
+
) as response:
|
|
50
|
+
if 200 <= response.status < 300:
|
|
51
|
+
async for data in response.content.iter_chunked(1024):
|
|
52
|
+
yield self.engine_bytes_to_rasa_audio_bytes(data)
|
|
53
|
+
return
|
|
54
|
+
else:
|
|
55
|
+
structlogger.error(
|
|
56
|
+
"azure.synthesize.rest.failed",
|
|
57
|
+
status_code=response.status,
|
|
58
|
+
msg=response.text(),
|
|
59
|
+
)
|
|
60
|
+
raise TTSError(f"TTS failed: {response.text()}")
|
|
61
|
+
except ClientConnectorError as e:
|
|
62
|
+
raise TTSError(e)
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def get_request_headers() -> dict[str, str]:
|
|
66
|
+
azure_speech_api_key = os.environ["AZURE_SPEECH_API_KEY"]
|
|
67
|
+
return {
|
|
68
|
+
"Ocp-Apim-Subscription-Key": azure_speech_api_key,
|
|
69
|
+
"Content-Type": "application/ssml+xml",
|
|
70
|
+
"X-Microsoft-OutputFormat": "raw-8khz-8bit-mono-mulaw",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def get_tts_endpoint(config: AzureTTSConfig) -> str:
|
|
75
|
+
return f"https://{config.speech_region}.tts.speech.microsoft.com/cognitiveservices/v1"
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def create_request_body(text: str, conf: AzureTTSConfig) -> str:
|
|
79
|
+
return f"""
|
|
80
|
+
<speak version='1.0' xml:lang='{conf.language}'>
|
|
81
|
+
<voice xml:lang='{conf.language}' name='{conf.voice}'>
|
|
82
|
+
{text}
|
|
83
|
+
</voice>
|
|
84
|
+
</speak>"""
|
|
85
|
+
|
|
86
|
+
def engine_bytes_to_rasa_audio_bytes(self, chunk: bytes) -> RasaAudioBytes:
|
|
87
|
+
"""Convert the generated tts audio bytes into rasa audio bytes."""
|
|
88
|
+
return RasaAudioBytes(chunk)
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def get_default_config() -> AzureTTSConfig:
|
|
92
|
+
return AzureTTSConfig(
|
|
93
|
+
language="en-US",
|
|
94
|
+
voice="en-US-JennyNeural",
|
|
95
|
+
speech_region="germanywestcentral",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_config_dict(cls, config: Dict) -> "AzureTTS":
|
|
100
|
+
return cls(AzureTTSConfig.from_dict(config))
|