文章目录
- 前言
- [1. 简介](#1. 简介)
-
- [1.1 能力体验](#1.1 能力体验)
- [1.2 功能特性](#1.2 功能特性)
- [1.3 音色列表](#1.3 音色列表)
- [1.4 收费情况](#1.4 收费情况)
- [2. 开启服务](#2. 开启服务)
-
- [2.1 创建应用](#2.1 创建应用)
- [2.3 使用服务介绍](#2.3 使用服务介绍)
- 3.Websocket接入演示
-
- [3.1 编写demo](#3.1 编写demo)
- [3.2 代码解释](#3.2 代码解释)
- 3.4运行demo
- [4. 参考链接](#4. 参考链接)
前言
语音合成TTS(text to Speech)是我觉得后续开发产品所不可或缺的一个功能,因为相比较于过去的GUI 图形+文字展示,动态形象+语音会更利用人与设备之间的交互。
另外语音沟通更加灵活,因为过去的GUI图形界面都是预先设计好的,就像APP内的界面。这种更加标准但是不够灵活,不能满足所有人的需求和爱好。所以我觉得现在了解下TTS也是非常有必要的。
之前看到小智AI中有提到支持了cosyVoice(阿里的TTS模型)与火山引擎的TTS
。然后搜索了两者的区别发现火山引擎的TTS在高拟真克隆这块做的比较好。这样的话就能够使用该TTS生成各种符合产品形象的声音。所以暂时先选择了火山引擎的TTS去研究一下。
1. 简介
火山引擎的TTS也叫做豆包语音合成大模型,它是依托新一代大模型能力,豆包语音合成模型能够根据上下文智能预测文本的情绪、语调等信息,并生成超自然、高保真、个性化的语音,以满足不同用户的个性化需求。
简介中很多的内容都是来自于火山引擎的文档中心,我这里就简单的介绍下,大家需要详细了解的可以到以下地址去看看:
1.1 能力体验
在官方的网站中有个能力体验的页面,这个体验很简单 输入想要描述的文字,然后选择配音和一些声音相关的配置就能生成语音了。
我们最终要实现的功能也是类似的,只是这个是人家已经实现好的功能比较固化,我们想要更灵活一些,所以需要通过代码去使用这个模型来做一些定制化的开发。
1.2 功能特性
下面这个表里面说了很多,说实话有一些对于我们这些刚接触的人来说并没有什么概念,例如这里我也只是对部署方案比较感兴趣。这里后续有需要的时候大家可以去官方介绍文档中去查看。

1.3 音色列表
使用克隆语音需要用到另外一个模型,不是我们本次所使用的"语音合成大模型",所以我们并不是说想用什么声音就用什么声音,而是要使用官方给出的声音列表。不过好在可选择性还是很多的。
这里截图不全,更详细的内容可以查看官方文档
1.4 收费情况
我最初以为TTS里面包含了语音复制,什么短文本语音合成啥的呢,结果一看乖乖嘞被分成了4个而且是收费的。
但是我们第一次用的话是免费的,会赠送一定的使用额度,所以大家不要太过于担心。
2. 开启服务
我们需要申请appid、token、secret_key
等用来开启和使用TTS的服务。
这个就类似于从豆包那里申请个账号,这个账号里面包含了我们的身份信息,以及能够使用哪些模型还有我们的剩余额度,有了这些信息后我们才能真正的去使用大模型语音合成功能。
2.1 创建应用
先根据下方的快速入门创建账号:
点击"创建应用"来新增应用,填入应用名称、简介和所需接入的能力服务
我们第一次使用会有个免费额度,所以大家不用太担心。
创建成功后,能够在应用管理界面看到我们所创建的应用
获取token和Secret_key信息
在控制台界面,我们能够找到属于我们的Access Token 和 Secret Key,有了这些信息我们才能去使用该服务。
2.3 使用服务介绍
使用的话有两种方式,分别是API和SDK接入。
SDK的话目前它只能运行在安卓和IOS操作系统上,应该是集成到APP中,这不方便我们去进行体验。而且像小智AI这种也是采用的API方式接入的,所以这里我们也使用API的方式。
API接入又分为WebSocket
还有Http
,基本上工作原理大差不差,都是发送请求,然后接收响应处理。这里我们就以WebSocket
为主。
3.Websocket接入演示
Websocket接入演示的功能,需要使用账号申请部分申请到的 appid和access_token进行调用文本一次性送入,后端边合成边返回音频数据。所以大家一定要先按照上面的步骤获取对应的token和appid等信息。
接口说明地址为下方的链接,详细的使用方法大概可以进入该链接查看:
wss://openspeech.bytedance.com/api/v1/tts/ws_binary
3.1 编写demo
文档中心有个demo,我们拿下来直接运行即可,本次演示的代码来源是tts_websocket_demo.py
源码如下:
python
#coding=utf-8
'''
requires Python 3.6 or later
pip install asyncio
pip install websockets
'''
import asyncio
import websockets
import uuid
import json
import gzip
import copy
MESSAGE_TYPES = {11: "audio-only server response", 12: "frontend server response", 15: "error message from server"}
MESSAGE_TYPE_SPECIFIC_FLAGS = {0: "no sequence number", 1: "sequence number > 0",
2: "last message from server (seq < 0)", 3: "sequence number < 0"}
MESSAGE_SERIALIZATION_METHODS = {0: "no serialization", 1: "JSON", 15: "custom type"}
MESSAGE_COMPRESSIONS = {0: "no compression", 1: "gzip", 15: "custom compression method"}
appid = "xxx"
token = "xxx"
cluster = "xxx"
voice_type = "xxx"
host = "openspeech.bytedance.com"
api_url = f"wss://{host}/api/v1/tts/ws_binary"
# version: b0001 (4 bits)
# header size: b0001 (4 bits)
# message type: b0001 (Full client request) (4bits)
# message type specific flags: b0000 (none) (4bits)
# message serialization method: b0001 (JSON) (4 bits)
# message compression: b0001 (gzip) (4bits)
# reserved data: 0x00 (1 byte)
default_header = bytearray(b'\x11\x10\x11\x00')
request_json = {
"app": {
"appid": appid,
"token": "access_token",
"cluster": cluster
},
"user": {
"uid": "388808087185088"
},
"audio": {
"voice_type": "xxx",
"encoding": "mp3",
"speed_ratio": 1.0,
"volume_ratio": 1.0,
"pitch_ratio": 1.0,
},
"request": {
"reqid": "xxx",
"text": "字节跳动语音合成。",
"text_type": "plain",
"operation": "xxx"
}
}
async def test_submit():
submit_request_json = copy.deepcopy(request_json)
submit_request_json["audio"]["voice_type"] = voice_type
submit_request_json["request"]["reqid"] = str(uuid.uuid4())
submit_request_json["request"]["operation"] = "submit"
payload_bytes = str.encode(json.dumps(submit_request_json))
payload_bytes = gzip.compress(payload_bytes) # if no compression, comment this line
full_client_request = bytearray(default_header)
full_client_request.extend((len(payload_bytes)).to_bytes(4, 'big')) # payload size(4 bytes)
full_client_request.extend(payload_bytes) # payload
print("\n------------------------ test 'submit' -------------------------")
print("request json: ", submit_request_json)
print("\nrequest bytes: ", full_client_request)
file_to_save = open("test_submit.mp3", "wb")
header = {"Authorization": f"Bearer; {token}"}
async with websockets.connect(api_url, extra_headers=header, ping_interval=None) as ws:
await ws.send(full_client_request)
while True:
res = await ws.recv()
done = parse_response(res, file_to_save)
if done:
file_to_save.close()
break
print("\nclosing the connection...")
async def test_query():
query_request_json = copy.deepcopy(request_json)
query_request_json["audio"]["voice_type"] = voice_type
query_request_json["request"]["reqid"] = str(uuid.uuid4())
query_request_json["request"]["operation"] = "query"
payload_bytes = str.encode(json.dumps(query_request_json))
payload_bytes = gzip.compress(payload_bytes) # if no compression, comment this line
full_client_request = bytearray(default_header)
full_client_request.extend((len(payload_bytes)).to_bytes(4, 'big')) # payload size(4 bytes)
full_client_request.extend(payload_bytes) # payload
print("\n------------------------ test 'query' -------------------------")
print("request json: ", query_request_json)
print("\nrequest bytes: ", full_client_request)
file_to_save = open("test_query.mp3", "wb")
header = {"Authorization": f"Bearer; {token}"}
async with websockets.connect(api_url, extra_headers=header, ping_interval=None) as ws:
await ws.send(full_client_request)
res = await ws.recv()
parse_response(res, file_to_save)
file_to_save.close()
print("\nclosing the connection...")
def parse_response(res, file):
print("--------------------------- response ---------------------------")
# print(f"response raw bytes: {res}")
protocol_version = res[0] >> 4
header_size = res[0] & 0x0f
message_type = res[1] >> 4
message_type_specific_flags = res[1] & 0x0f
serialization_method = res[2] >> 4
message_compression = res[2] & 0x0f
reserved = res[3]
header_extensions = res[4:header_size*4]
payload = res[header_size*4:]
print(f" Protocol version: {protocol_version:#x} - version {protocol_version}")
print(f" Header size: {header_size:#x} - {header_size * 4} bytes ")
print(f" Message type: {message_type:#x} - {MESSAGE_TYPES[message_type]}")
print(f" Message type specific flags: {message_type_specific_flags:#x} - {MESSAGE_TYPE_SPECIFIC_FLAGS[message_type_specific_flags]}")
print(f"Message serialization method: {serialization_method:#x} - {MESSAGE_SERIALIZATION_METHODS[serialization_method]}")
print(f" Message compression: {message_compression:#x} - {MESSAGE_COMPRESSIONS[message_compression]}")
print(f" Reserved: {reserved:#04x}")
if header_size != 1:
print(f" Header extensions: {header_extensions}")
if message_type == 0xb: # audio-only server response
if message_type_specific_flags == 0: # no sequence number as ACK
print(" Payload size: 0")
return False
else:
sequence_number = int.from_bytes(payload[:4], "big", signed=True)
payload_size = int.from_bytes(payload[4:8], "big", signed=False)
payload = payload[8:]
print(f" Sequence number: {sequence_number}")
print(f" Payload size: {payload_size} bytes")
file.write(payload)
if sequence_number < 0:
return True
else:
return False
elif message_type == 0xf:
code = int.from_bytes(payload[:4], "big", signed=False)
msg_size = int.from_bytes(payload[4:8], "big", signed=False)
error_msg = payload[8:]
if message_compression == 1:
error_msg = gzip.decompress(error_msg)
error_msg = str(error_msg, "utf-8")
print(f" Error message code: {code}")
print(f" Error message size: {msg_size} bytes")
print(f" Error message: {error_msg}")
return True
elif message_type == 0xc:
msg_size = int.from_bytes(payload[:4], "big", signed=False)
payload = payload[4:]
if message_compression == 1:
payload = gzip.decompress(payload)
print(f" Frontend message: {payload}")
else:
print("undefined message type!")
return True
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(test_submit())
loop.run_until_complete(test_query())
填写token等信息
在运行demo之前,我们需要在下方的这里填写上之前在火山引擎处申请到的信息,这里大家就理解为自己的账号密码就行了
appid还有token还有cluster在我们的应用空间那里就能看到,然后有个比较特殊的voice_type需要找到文档中的音色列表去那里找自己想要合成的声音类型。
3.2 代码解释
导入各基本模块
python
import asyncio
import websockets
import uuid
import json
import gzip
import copy
这里的asyncio 是python中的用于实现并发的模块,里面提供了例如协程、协程锁等各种用于异步通信的功能。其它的就不用说了看名字就知道干啥的了。
基础配置填写
python
appid = "xxx"
token = "xxx"
cluster = "xxx"
voice_type = "xxx"
host = "openspeech.bytedance.com"
api_url = f"wss://{host}/api/v1/tts/ws_binary"
这个就是前面我们提到的把应用空间了申请到的信息填写上去
请求体
python
# version: b0001 (4 bits)
# header size: b0001 (4 bits)
# message type: b0001 (Full client request) (4bits)
# message type specific flags: b0000 (none) (4bits)
# message serialization method: b0001 (JSON) (4 bits)
# message compression: b0001 (gzip) (4bits)
# reserved data: 0x00 (1 byte)
default_header = bytearray(b'\x11\x10\x11\x00')
request_json = {
"app": {
"appid": appid,
"token": "access_token",
"cluster": cluster
},
"user": {
"uid": "388808087185088"
},
"audio": {
"voice_type": "xxx",
"encoding": "mp3",
"speed_ratio": 1.0,
"volume_ratio": 1.0,
"pitch_ratio": 1.0,
},
"request": {
"reqid": "xxx",
"text": "字节跳动语音合成。",
"text_type": "plain",
"operation": "xxx"
}
}
header
是发送请求时的消息头,tts
的通讯协议要求二进制的方式进行传输,所以头这里也是采用的二进制。上面的注释代表的是其二进制代表的内容。
request_json
是我们的请求体,里面需要填充我们要发送的具体信息,后续发送时也会将其转换为二进制发送,请求体中的参数主要就是这几个
可以通过下方的链接去查询
提交转换请求
python
请求
async def test_submit():
功能
- 向字节跳动的语音合成WebSocket API提交一个文本合成请求
主要操作:
- 准备提交请求的JSON数据,包括appid、token、cluster等认证信息
- 设置操作类型为"submit"(提交)
- 生成唯一的请求ID
- 使用gzip压缩请求数据
- 建立WebSocket连接并发送请求
- 持续接收服务器返回的音频数据流,保存到test_submit.mp3文件
- 处理完所有音频数据后关闭连接
python
查询
async def test_query():
功能
- 向字节跳动的语音合成WebSocket API发送查询请求
主要操作:
- 准备查询请求的JSON数据,结构与submit类似
- 设置操作类型为"query"(查询)
- 生成唯一的请求ID
- 使用gzip压缩请求数据
- 建立WebSocket连接并发送请求
- 接收服务器响应(通常是一次性返回)
- 将响应数据保存到test_query.mp3文件
- 关闭连接
查询与请求的主要区别:
- test_submit()用于提交语音合成任务并持续接收音频流
- test_query()用于查询状态或结果,通常只接收一次响应
- test_submit()会处理多个响应消息直到完成
- test_query()通常只处理单个响应消息
处理接收到的响应
python
处理响应
def parse_response(res, file):
- 解析响应头部信息
协议版本(protocol_version)
头部大小(header_size)
消息类型(message_type)
消息特定标志(message_type_specific_flags)
序列化方法(serialization_method)
压缩方法(message_compression)
保留字段(reserved)
- 处理不同类型的服务器响应:
错误消息响应(message_type=0xf):
- 解析错误代码(code)
- 解析错误消息大小(msg_size)
- 解压缩并显示错误内容
前端消息响应(message_type=0xc):
- 解析消息大小(msg_size)
- 解压缩并显示前端消息
3.4运行demo
注意下载下来的demo好像名称中有个空格,大家注意修改下名称。
执行指令
python
python tts_websocket_demo.py
执行结果
通过打印日志能够,模型服务返回了对应的响应数据(音频的原始数据)
然后我们就能看到我们的文件夹多了两个mp3的文件,分别是通过请求得到的和通过查询得到的。
听了下是熊二说的"字节跳动语音合成",这里我设置的语音类型也是熊二的。
此时再去查看我们的模型使用情况,会发现少了一定的额度。