This topic explains how to use the WebSocket protocol for real-time text-to-speech (TTS) with Intelligent Speech Interaction. If you prefer not to use the Alibaba Cloud Intelligent Speech Interaction software development kit (SDK), or if the available SDKs do not meet your requirements, you can write your own code to access the Alibaba Cloud Voice Service.
Prerequisites
Before you connect using the WebSocket protocol, see the API reference.
Authentication
The service authenticates requests using a temporary token. You must include the token as a parameter in the request URL. For more information about how to obtain a token, see Obtain a token. After you obtain the token, you can access the Voice Service using one of the following URLs.
Access method |
Description |
URL |
Public network access |
All servers can use the public network access URL. The SDK uses the public network access URL by default. |
Beijing: |
ECS internal network access |
If you use an Alibaba Cloud ECS instance in the China (Beijing) region, you can use the internal network access URL. ECS instances in a classic network cannot access AnyTunnel, which means they cannot access the Voice Service over the internal network. If you want to use AnyTunnel, create a VPC and access the service from within the VPC.
Note
Using the internal network access method does not incur data transfer costs for the ECS instance. For more information about ECS network types, see Network types. |
Beijing: |
Request instructions
Request instructions control the start and end of a speech synthesis task and define its boundaries. You can send instructions to the service as a JSON-formatted text frame. The basic request information is set in the header. An instruction consists of a Header and a Payload. The Header has a standard format, but the Payload format varies depending on the instruction.
1. Header format
The header has the following format:
Parameter |
Type |
Required |
Description |
header |
Request header |
- |
- |
header.appkey |
String |
Yes |
The AppKey of the project created in the console. |
header.message_id |
String |
Yes |
The ID of the message request. Generate a random 32-bit unique ID. |
header.task_id |
String |
Yes |
The session ID for the entire real-time speech synthesis task. This ID must be a unique 32-bit ID and remain consistent throughout the request. |
header.namespace |
String |
Yes |
The name of the product to access. Set this to `FlowingSpeechSynthesizer`. |
header.name |
String |
Yes |
The name of the instruction, such as `StartSynthesis` or `StopSynthesis`. |
2. StartSynthesis instruction
Parameter |
Type |
Required |
Description |
payload.voice |
String |
No |
The voice. The default value is `xiaoyun`. |
payload.format |
String |
No |
The audio coding format. Supported formats are PCM, WAV, and MP3. The default value is `pcm`. |
payload.sample_rate |
Integer |
No |
The audio sampling rate. The default value is 16000 Hz. |
payload.volume |
Integer |
No |
The volume. The value ranges from 0 to 100. The default value is 50. |
payload.speech_rate |
Integer |
No |
The speech rate. The value ranges from -500 to 500. The default value is 0. The values [-500, 0, 500] correspond to playback speeds of [0.5, 1.0, 2.0]. |
payload.pitch_rate |
Integer |
No |
The pitch. The value ranges from -500 to 500. The default value is 0. |
payload.enable_subtitle |
Boolean |
No |
Enables character-level timestamps. |
payload.enable_phoneme_timestamp |
Boolean |
No |
Enables phoneme-level timestamps. |
payload.enable_aigc_tag |
Boolean |
No |
Specifies whether to add an invisible AIGC identifier to the generated audio. If you set this to true, the invisible identifier is embedded into the audio in a supported format (WAV, MP3, or Opus). Default value: false. |
payload.aigc_propagator |
String |
No |
Sets the Default value: Your Alibaba Cloud UID. |
payload.aigc_propagate_id |
String |
No |
Sets the Default value: The Task ID of the current speech synthesis request. |
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "StartSynthesis",
"appkey": "17d4c634****"
},
"payload": {
"voice": "xiaoyun",
"format": "wav",
"sample_rate": 16000,
"volume": 50,
"speech_rate": 0,
"pitch_rate": 0,
"enable_subtitle": true
}
}
3. RunSynthesis instruction
Parameter |
Type |
Required |
Description |
text |
String |
Yes |
The text to synthesize. |
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "RunSynthesis",
"appkey": "17d4c634****"
},
"payload": {
"text": "Streaming input text"
}
}
4. StopSynthesis instruction
The `StopSynthesis` instruction tells the service to stop speech synthesis and synthesize any cached text.
The real-time TTS service synthesizes audio on a sentence-by-sentence basis. The service may cache text that does not form a complete sentence. You must send this instruction immediately after you finish sending the text stream to prevent text loss.
The Payload is empty. The following code provides an example:
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "StopSynthesis",
"appkey": "17d4c634****"
}
}
Downstream data
WebSocket data frames include text frames, binary frames, close frames, Ping frames, and Pong frames. The service uses text frames to send events and binary frames to send the audio data stream.
The following example code for `websocket-client` in Python shows how to parse data received from the WebSocket:
audio_data = None
# Callback function for the message listener
def on_message(self, ws, message):
if isinstance(message, str):
# Parse the text frame as JSON
try:
json_data = json.loads(message)
# TODO: Parse the event
except json.JSONDecodeError:
print("Failed to parse message as JSON.")
elif isinstance(message, (bytes, bytearray)):
# Save the binary frame as an audio frame
# TODO: Save the audio or play it using a player that supports streaming input, such as pyaudio
if audio_data is None:
audio_data = bytes(message)
else:
audio_data = self._audio_data + bytes(message)
ws = websocket.WebSocketApp(
url,
header={
"X-NLS-Token": token,
},
on_message=on_message,
on_error=None,
on_close=None,
)
For more information about WebSocket, see the documentation at this link.
Events
Events are progress notifications sent from the service to the client. They represent different processing stages. The client can use these events to implement various types of business logic. Events are returned in JSON format and consist of a Header and a Payload. The Header has a standard format, but the Payload format may vary depending on the event.
1. SynthesisStarted event
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "SynthesisStarted",
"status": 20000000,
"status_message": "GATEWAY|SUCCESS|Success."
}
}
2. SentenceBegin event
The `SentenceBegin` event indicates that the service has detected the beginning of a sentence.
Parameter |
Type |
Description |
index |
Integer |
The sentence number. The value starts from 1 and increments. |
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "SentenceBegin",
"status": 20000000,
"status_message": "GATEWAY|SUCCESS|Success."
},
"payload": {
"index": 1
}
}
3. SentenceSynthesis event
The `SentenceSynthesis` event indicates that a new synthesis result is available. This event contains the latest audio and timestamps. The data is complete for the current sentence and is provided incrementally across multiple sentences.
Parameter |
Type |
Description |
subtitles[] |
ArrayList |
Timestamp information. |
subtitles[0].text |
String |
Text information. |
subtitles[0].sentence |
String |
Sentence timestamp control. `True` indicates that the current timestamp is for a sentence. |
subtitles[0].begin_index |
Integer |
The start position of the character in the sentence. The value starts from 0. |
subtitles[0].end_index |
Integer |
The end position of the character in the sentence. The value starts from 0. |
subtitles[0].begin_time |
Integer |
The start timestamp of the TTS audio corresponding to the text, in milliseconds. |
subtitles[0].end_time |
Integer |
The end timestamp of the TTS audio corresponding to the text, in milliseconds. |
subtitles[0].phoneme_list |
ArrayList |
The phoneme timestamp information for the text. |
subtitles[0].phoneme_list[0].index |
Integer |
The index of the phoneme. The value starts from 0. |
subtitles[0].phoneme_list[0].beginTime |
Integer |
The start timestamp of the TTS audio corresponding to the phoneme, in milliseconds. |
subtitles[0].phoneme_list[0].endTime |
Integer |
The end timestamp of the TTS audio corresponding to the phoneme, in milliseconds. |
subtitles[0].phoneme_list[0].phoneme |
String |
Phoneme information. |
subtitles[0].phoneme_list[0].tone |
String |
Tone information. |
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "SentenceSynthesis",
"status": 20000000,
"status_message": "GATEWAY|SUCCESS|Success."
},
"payload": {
"subtitles": [
{
"text": "",
"begin_time": 0,
"end_time": 0,
"begin_index": 0,
"end_index": 1,
"sentence": true,
"phoneme_list": []
},
{
"text": "jīn",
"begin_time": 0,
"end_time": 175,
"begin_index": 0,
"end_index": 1,
"sentence": false,
"phoneme_list": [
{
"begin_time": 0,
"end_time": 120,
"text": "j_c",
"tone": "1"
},
{
"begin_time": 120,
"end_time": 170,
"text": "in_c",
"tone": "1"
}
]
}
]
}
}
4. SentenceEnd event
The `SentenceEnd` event indicates that the service has detected the end of a sentence. It returns the complete timestamp information for that sentence.
Parameter |
Type |
Description |
subtitles[] |
ArrayList |
Timestamp information. |
subtitles[0].text |
String |
Text information. |
subtitles[0].sentence |
String |
Sentence timestamp control. `True` indicates that the current timestamp is for a sentence. |
subtitles[0].begin_index |
Integer |
The start position of the character in the sentence. The value starts from 0. |
subtitles[0].end_index |
Integer |
The end position of the character in the sentence. The value starts from 0. |
subtitles[0].begin_time |
Integer |
The start timestamp of the TTS audio corresponding to the text, in milliseconds. |
subtitles[0].end_time |
Integer |
The end timestamp of the TTS audio corresponding to the text, in milliseconds. |
subtitles[0].phoneme_list |
ArrayList |
The phoneme timestamp information for the text. |
subtitles[0].phoneme_list[0].index |
Integer |
The index of the phoneme. The value starts from 0. |
subtitles[0].phoneme_list[0].beginTime |
Integer |
The start timestamp of the TTS audio corresponding to the phoneme, in milliseconds. |
subtitles[0].phoneme_list[0].endTime |
Integer |
The end timestamp of the TTS audio corresponding to the phoneme, in milliseconds. |
subtitles[0].phoneme_list[0].phoneme |
String |
Phoneme information. |
subtitles[0].phoneme_list[0].tone |
String |
Tone information. |
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "SentenceEnd",
"status": 20000000,
"status_message": "GATEWAY|SUCCESS|Success."
},
"payload": {
"subtitles": [
{
"text": "",
"begin_time": 0,
"end_time": 0,
"begin_index": 0,
"end_index": 1,
"sentence": true,
"phoneme_list": []
},
{
"text": "jin",
"begin_time": 0,
"end_time": 175,
"begin_index": 0,
"end_index": 1,
"sentence": false,
"phoneme_list": [
{
"begin_time": 0,
"end_time": 120,
"text": "j_c",
"tone": "1"
},
{
"begin_time": 120,
"end_time": 170,
"text": "in_c",
"tone": "1"
}
]
},
{
"text": "tian",
"begin_time": 175,
"end_time": 320,
"begin_index": 1,
"end_index": 2,
"sentence": false,
"phoneme_list": [
{
"begin_time": 0,
"end_time": 120,
"text": "t_c",
"tone": "1"
},
{
"begin_time": 120,
"end_time": 170,
"text": "ian_c",
"tone": "1"
}
]
}
]
}
}
5. SynthesisCompleted event
The `SynthesisCompleted` event indicates that the service has stopped speech synthesis and all audio data has been sent.
This event is returned only after you send a `StopSynthesis` instruction.
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "SynthesisCompleted",
"status": 20000000,
"status_message": "GATEWAY|SUCCESS|Success."
}
}
Downstream audio stream
In real-time speech synthesis, the audio is sent in frames as a data stream. Together, all the sent audio frames constitute a single, complete audio file. You can play the stream in real time using a player that supports streaming playback, such as FFmpeg, PyAudio (Python), AudioFormat (Java), or MediaSource (JavaScript).
The audio stream is sent from the time you first send text with a `RunSynthesis` instruction until you receive the `SynthesisCompleted` event.
In real-time speech synthesis, a complete audio file is returned in multiple parts. To play the streaming audio, you must use a player that supports streaming playback. Do not treat each frame as a separate audio file, because this will cause decoding to fail.
When you save the audio, you must append the data to the same file.
When you synthesize audio in WAV or MP3 format, the audio is synthesized as a stream. Therefore, only the first frame contains the file header for the current task.
JavaScript code example
For an example of how to implement the real-time speech synthesis protocol and playback in JavaScript, see JS Playback Example for Real-time Speech Synthesis.
Before you open `index.html`, you must replace the `appkey` and `token` in `app.js`. Then, you can run the following Python command to start a simple HTTP server in the current directory. Open http://localhost:8000 in your browser to view the webpage.
python -m http.server 8000
On the webpage, you can click buttons to send the corresponding instructions. Clicking `RunSynthesis` sends the text from the text box to the server. You can click `RunSynthesis` multiple times in a single session.
Streaming player details
In audio_player.js, the Web Audio API is used to develop the PCMAudioPlayer player, which plays streaming audio in PCM format. This player converts 16-bit sampling points to float values, writes them to an audio buffer for playback, and immediately plays the next audio segment in the `onended` callback after the previous segment finishes playing.
Using `MediaSource` is a simpler way to play streaming audio. However, `MediaSource` is not supported in some browsers, such as Safari, iOS WebView based on Safari, or WeChat mini programs. For more information about compatibility, see MediaSource.
When you use the `wavtools` integrated in openai-realtime-console to play audio on mobile devices or in the Safari browser, you may hear noise.
Testing tool
While you develop your interface based on the WebSocket protocol, you can download the stream_input_tts_mock_server.py script. Run the following commands to install dependencies and run a local mock server for testing. The mock server simulates the real-time speech synthesis service of the public cloud.
pip install websocket-client
python NlsStreamInputTtsMockServer.py
After the script runs successfully, a mock service runs by default at `ws://127.0.0.1:12345`. You can test your implementation against the local mock service before you switch to the online service.