本文介绍如何使用智能语音交互流式文本WebSocket协议使用语音合成。如果您不希望引入阿里云智能语音交互产品SDK,或者目前提供的SDK不能满足您的要求,可以基于本文描述自行开发代码访问阿里语音服务。
前提条件
在使用WebSocket协议对接之前,请先阅读API详情中的服务交互流程说明 。
鉴权
服务端通过临时Token进行鉴权,请求时需要在URL中携带Token参数,Token获取方式请参见获取Token概述。获取Token之后通过如下方式访问语音服务端。
访问类型 | 说明 | URL |
外网访问(默认北京地域) | 所有服务器均可使用外网访问URL(SDK中默认设置了外网访问URL)。 | 北京: |
ECS内网访问 | 使用阿里云北京ECS(即ECS地域为华北2(北京)),可使用内网访问URL。 ECS的经典网络不能访问AnyTunnel,即不能在内网访问语音服务;如果希望使用AnyTunnel,需要创建专有网络在其内部访问。 说明 使用内网访问方式,将不产生ECS实例的公网流量费用。 关于ECS的网络类型请参见网络类型。 | 北京: |
请求指令
请求指令用于控制语音识别任务的起止,标识任务边界,以JSON格式的Text Frame方式发送服务端请求,需要在Header中设置请求的基础信息。指令由Header和Payload两部分组成,其中Header部分为统一格式,不同指令的Payload部分格式各不相同。
1. Header格式说明
Header格式如下:
参数 | 类型 | 是否必选 | 说明 |
header | 请求头 | - | - |
header.appkey | String | 是 | 管控台创建的项目Appkey。 |
header.message_id | String | 是 | 当次消息请求ID,随机生成32位唯一ID。 |
header.task_id | String | 是 | 整个实时语音合成的会话ID,整个请求中需要保持一致,32位唯一ID。 |
header.namespace | String | 是 | 访问的产品名称,固定为“FlowingSpeechSynthesizer”。 |
header.name | String | 是 | 指令名称,包含StartSynthesis和StopSynthesis指令。 |
2. StartSynthesis指令
参数 | 类型 | 是否必选 | 说明 |
payload.voice | String | 否 | 发音人,默认是longxiaochun。发音人,默认是longxiaochun。 |
payload.format | String | 否 | 音频编码格式,支持pcm、wav和mp3格式,默认值:pcm。音频编码格式,支持pcm、wav和mp3格式,默认值:pcm。 |
payload.sample_rate | Integer | 否 | 音频采样率,默认值:16000Hz。 |
payload.volume | Integer | 否 | 音量,取值范围:0~100。默认值:50。 |
payload.speech_rate | Integer | 否 | 语速,取值范围:-500~500,默认值:0。 [-500,0,500]对应的语速倍速区间为 [0.5,1.0,2.0]。 |
payload.pitch_rate | Integer | 否 | 语调,取值范围:-500~500,默认值:0。 |
payload.enable_subtitle | Boolean | 否 | 开启字级别时间戳。 |
payload.enable_phoneme_timestamp | Boolean | 否 | 开启音素级别时间戳。 |
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "StartSynthesis",
"appkey": "17d4c634****"
},
"payload": {
"voice": "longxiaochun",
"format": "wav",
"sample_rate": 16000,
"volume": 50,
"speech_rate": 0,
"pitch_rate": 0,
"enable_subtitle": true
}
}
3. RunSynthesis指令
参数 | 类型 | 是否必选 | 说明 |
text | String | 是 | 需要合成的文本 |
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "RunSynthesis",
"appkey": "17d4c634****"
},
"payload": {
"text": "流式输入文本"
}
}
4. StopSynthesis指令
StopSynthesis指令要求服务端停止语音合成,并且合成所有缓存文本。
由于流式文本语音合成服务端会分句合成音频,因此服务端存在未满足分句条件的缓存文本,需要在文本流发送结束后立刻发送此指令,否则有可能丢失文本。
Payload为空。示例代码如下:
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "StopSynthesis",
"appkey": "17d4c634****"
}
}
下行数据
WebSocket 数据帧分为文本帧 (text frame)、二进制帧 (binary frame)、关闭帧 (close frame)、Ping帧 和 Pong帧。我们使用文本帧下发事件,使用二进制帧下发音频数据流。
以python中的websocket-client为例,可以参考下述示例代码解析WebSocket收到的数据:
audio_data = None
# 监听消息的回调函数
def on_message(self, ws, message):
if isinstance(message, str):
# 将文本帧解析为json
try:
json_data = json.loads(message)
# TODO: 解析事件
except json.JSONDecodeError:
print("Failed to parse message as JSON.")
elif isinstance(message, (bytes, bytearray)):
# 将二进制帧作为音频帧保存
# TODO: 保存音频或使用支持流式输入的播放器播放,例如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,
)
关于Websocket详细介绍可以参考链接
事件
事件指的是服务端返回给客户端的处理进度事件,代表不同的处理阶段,客户端可获取不同处理阶段的事件实现不同的业务逻辑。以JSON格式返回,事件由Header和Payload两部分组成,其中Header部分为统一格式,不同事件的Payload部分格式可能不同。
1. SynthesisStarted事件
参数 | 类型 | 说明 |
session_id | String | 客户端请求时传入session_id的话则原样返回,否则由服务端自动生成32位唯一ID。 |
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "SynthesisStarted",
"status": 20000000,
"status_message": "GATEWAY|SUCCESS|Success."
},
"payload": {
"session_id": "1231231dfdf****"
}
2. SentenceBegin事件
SentenceBegin事件表示服务端检测到了一句话的开始。
参数 | 类型 | 说明 |
index | Integer | 句子编号,从1开始递增。 |
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "SentenceBegin",
"status": 20000000,
"status_message": "GATEWAY|SUCCESS|Success."
},
"payload": {
"index": 1
}
}
3. SentenceSynthesis事件
SentenceSynthesis事件表示有新的合成结果返回,包含最新的音频和时间戳,句内全量,句间增量。
参数 | 类型 | 说明 |
subtitles[] | ArrayList | 时间戳信息。 |
subtitles[0].text | String | ⽂本信息。 |
subtitles[0].sentence | String | 句子时间戳控制,True表示当前时间戳为句子。 |
subtitles[0].begin_index | Integer | 该字在整句中的开始位置,从0开始。 |
subtitles[0].end_index | Integer | 该字在整句中的结束位置,从0开始。 |
subtitles[0].begin_time | Integer | ⽂本对应TTS语⾳开始时间戳,单位ms。 |
subtitles[0].end_time | Integer | ⽂本对应TTS语⾳结束时间戳,单位ms。 |
subtitles[0].phoneme_list | ArrayList | 文本的音素时间戳信息。 |
subtitles[0].phoneme_list[0].index | Integer | 该音素的下标,从0开始。 |
subtitles[0].phoneme_list[0].beginTime | Integer | 该音素对应TTS语音开始时间戳,单位ms。 |
subtitles[0].phoneme_list[0].endTime | Integer | 该音素对应TTS语音结束时间戳,单位ms。 |
subtitles[0].phoneme_list[0].phoneme | String | 音素信息。 |
subtitles[0].phoneme_list[0].tone | String | 语气信息。 |
{
"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": "今",
"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事件
SentenceEnd事件表示服务端检测到了一句话的结束,返回该句的全量时间戳。
参数 | 类型 | 说明 |
subtitles[] | ArrayList | 时间戳信息。 |
subtitles[0].text | String | ⽂本信息。 |
subtitles[0].sentence | String | 句子时间戳控制,True表示当前时间戳为句子。 |
subtitles[0].begin_index | Integer | 该字在整句中的开始位置,从0开始。 |
subtitles[0].end_index | Integer | 该字在整句中的结束位置,从0开始。 |
subtitles[0].begin_time | Integer | ⽂本对应TTS语⾳开始时间戳,单位ms。 |
subtitles[0].end_time | Integer | ⽂本对应TTS语⾳结束时间戳,单位ms。 |
subtitles[0].phoneme_list | ArrayList | 文本的音素时间戳信息。 |
subtitles[0].phoneme_list[0].index | Integer | 该音素的下标,从0开始。 |
subtitles[0].phoneme_list[0].beginTime | Integer | 该音素对应TTS语音开始时间戳,单位ms。 |
subtitles[0].phoneme_list[0].endTime | Integer | 该音素对应TTS语音结束时间戳,单位ms。 |
subtitles[0].phoneme_list[0].phoneme | String | 音素信息。 |
subtitles[0].phoneme_list[0].tone | String | 语气信息。 |
{
"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": "今",
"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": "天",
"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事件
SynthesisCompleted事件表示服务端已停止了语音合成并且所有音频数据下发完毕。
{
"header": {
"message_id": "05450bf69c53413f8d88aed1ee60****",
"task_id": "640bc797bb684bd6960185651307****",
"namespace": "FlowingSpeechSynthesizer",
"name": "SynthesisCompleted",
"status": 20000000,
"status_message": "GATEWAY|SUCCESS|Success."
},
"payload": {
"measureType": "TextLengthHD",
"measureLength": 57
}
}
下行音频流
在流式语音合成中,音频会以数据流的方式分帧下发。下发的所有音频为一个完整的音频文件,可以通过支持流式播放的播放器,例如:ffmpeg、pyaudio (Python)、AudioFormat (Java)、MediaSource (Javascript)等,实时播放。
从第一次发起RunSynthesis指令发送文本开始,到收到SynthesisCompleted之间会收到音频流。
在流式语音合成中,是将一个完整的音频文件分多次返回。在播放流式音频时,需要使用支持流式播放的音频播放器,而不是将每一帧当作一个独立的音频播放,这样无法成功解码。
在保存音频时,请使用追加模式写入同一个文件。
在使用wav/mp3格式合成音频时,由于文件按照流式合成,因此只在第一帧中包含当前任务的文件头信息。
JavaScript示例代码
可以参考 流式语音合成JS示例 使用JavaScript实现流式语音合成协议并播放。
请在打开demo.html前首先替换app.js中的appkey和token,并且将voice参数替换为cosyvoice大模型音色。之后可以点击demo.html网页中的按钮发送对应指令。在一次流程中,RunSynthesis可以点击多次。
播放器说明
播放逻辑集中在 app.js中的 connectAndStartSynthesis 函数中,主要步骤如下:
在网页中添加audio标签的player用于播放音频。
创建一个 MediaSource 对象,它用于动态生成音频流。设置播放器的源为MediaSource对象,并且通过sourceopen事件启动player开始播放。
为MediaSource 创建 SourceBuffer,用来解码 mp3 格式的音频数据。
当 WebSocket 接收到音频数据(Blob 类型)时,通过 FileReader 读取其 ArrayBuffer 并添加到 SourceBuffer。
在收到SynthesisCompleted事件后结束MediaSource流。
通过上述步骤,确保可以实时播放收到的音频流,并且正确结束。
测试工具
在根据Websocket协议开发接口过程中,可以下载NlsStreamInputTtsMockServer.py脚本,运行如下命令安装依赖,并在本地模拟公有云流式语音合成服务进行调试:
pip install websocket-client
python NlsStreamInputTtsMockServer.py
在成功执行脚本后,将默认在本地的 ws://127.0.0.1:12345 运行模拟服务。请在可以成功调用本地的模拟服务后再切换到线上服务调试。