火山引擎TTS使用体验

文章目录

  • 前言
  • [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也叫做豆包语音合成大模型,它是依托新一代大模型能力,豆包语音合成模型能够根据上下文智能预测文本的情绪、语调等信息,并生成超自然、高保真、个性化的语音,以满足不同用户的个性化需求。

简介中很多的内容都是来自于火山引擎的文档中心,我这里就简单的介绍下,大家需要详细了解的可以到以下地址去看看:

https://www.volcengine.com/docs/6561/1257544

1.1 能力体验

在官方的网站中有个能力体验的页面,这个体验很简单 输入想要描述的文字,然后选择配音和一些声音相关的配置就能生成语音了。

我们最终要实现的功能也是类似的,只是这个是人家已经实现好的功能比较固化,我们想要更灵活一些,所以需要通过代码去使用这个模型来做一些定制化的开发。

1.2 功能特性

下面这个表里面说了很多,说实话有一些对于我们这些刚接触的人来说并没有什么概念,例如这里我也只是对部署方案比较感兴趣。这里后续有需要的时候大家可以去官方介绍文档中去查看。

1.3 音色列表

使用克隆语音需要用到另外一个模型,不是我们本次所使用的"语音合成大模型",所以我们并不是说想用什么声音就用什么声音,而是要使用官方给出的声音列表。不过好在可选择性还是很多的。

这里截图不全,更详细的内容可以查看官方文档

1.4 收费情况

我最初以为TTS里面包含了语音复制,什么短文本语音合成啥的呢,结果一看乖乖嘞被分成了4个而且是收费的。

但是我们第一次用的话是免费的,会赠送一定的使用额度,所以大家不要太过于担心。

2. 开启服务

我们需要申请appid、token、secret_key等用来开启和使用TTS的服务。

这个就类似于从豆包那里申请个账号,这个账号里面包含了我们的身份信息,以及能够使用哪些模型还有我们的剩余额度,有了这些信息后我们才能真正的去使用大模型语音合成功能。

2.1 创建应用

先根据下方的快速入门创建账号:

https://www.volcengine.com/docs/6561/163043

点击"创建应用"来新增应用,填入应用名称、简介和所需接入的能力服务

我们第一次使用会有个免费额度,所以大家不用太担心。

创建成功后,能够在应用管理界面看到我们所创建的应用

获取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是我们的请求体,里面需要填充我们要发送的具体信息,后续发送时也会将其转换为二进制发送,请求体中的参数主要就是这几个

可以通过下方的链接去查询

https://www.volcengine.com/docs/6561/1257584

提交转换请求

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):
  1. 解析响应头部信息
  • 协议版本(protocol_version)

  • 头部大小(header_size)

  • 消息类型(message_type)

  • 消息特定标志(message_type_specific_flags)

  • 序列化方法(serialization_method)

  • 压缩方法(message_compression)

  • 保留字段(reserved)

  1. 处理不同类型的服务器响应:

错误消息响应(message_type=0xf):

  • 解析错误代码(code)
  • 解析错误消息大小(msg_size)
  • 解压缩并显示错误内容

前端消息响应(message_type=0xc):

  • 解析消息大小(msg_size)
  • 解压缩并显示前端消息

3.4运行demo

注意下载下来的demo好像名称中有个空格,大家注意修改下名称。

执行指令

python 复制代码
python tts_websocket_demo.py

执行结果

通过打印日志能够,模型服务返回了对应的响应数据(音频的原始数据)

然后我们就能看到我们的文件夹多了两个mp3的文件,分别是通过请求得到的和通过查询得到的。

听了下是熊二说的"字节跳动语音合成",这里我设置的语音类型也是熊二的。

此时再去查看我们的模型使用情况,会发现少了一定的额度。

4. 参考链接

豆包语音合成大模型官网

语音技术开发参考 - 豆包官方的

相关推荐
会写代码的柯基犬1 天前
DeepSeek vs Kimi vs Qwen —— AI 生成俄罗斯方块代码效果横评
人工智能·llm
Mintopia1 天前
OpenClaw 是什么?为什么节后热度如此之高?
人工智能
爱可生开源社区1 天前
DBA 的未来?八位行业先锋的年度圆桌讨论
人工智能·dba
叁两1 天前
用opencode打造全自动公众号写作流水线,AI 代笔太香了!
前端·人工智能·agent
前端付豪1 天前
LangChain记忆:通过Memory记住上次的对话细节
人工智能·python·langchain
strayCat232551 天前
Clawdbot 源码解读 7: 扩展机制
人工智能·开源
王鑫星1 天前
SWE-bench 首次突破 80%:Claude Opus 4.5 发布,Anthropic 的野心不止于写代码
人工智能
lnix1 天前
当“大龙虾”养在本地:我们离“反SaaS”的AI未来还有多远?
人工智能·aigc
泉城老铁1 天前
Dify知识库如何实现多关键词AND检索?
人工智能