声音复刻-音频合成(算法编码:8000200007)
协议类型:WebSocket
一、接入点
| 环境 |
地址 |
| 公网 |
wss://api-maas.teleai.com.cn/aipaas/voice/v1/audio/synthesis |
- API Path:
/aipaas/voice/v1/audio/synthesis
- 请求方法:
WebSocket
- Content-Type:
application/json
二、鉴权
采用请求头携带鉴权信息,在建立 WebSocket 连接时通过 Header 传递:
2.1 标准鉴权
Authorization: <鉴权信息>
| 请求头 |
必填 |
说明 |
Content-Type |
是 |
内容类型,固定 application/json |
X-APP-ID |
是 |
控制台 → 应用管理 → AppID |
Device-Uuid |
否 |
设备uuid |
Authorization |
是 |
鉴权信息 |
2.2 Bearer Token 鉴权
Authorization: Bearer <用户申请的API-KEY>
| 请求头 |
必填 |
说明 |
Content-Type |
是 |
内容类型,固定 application/json |
Device-Uuid |
否 |
设备uuid |
Authorization |
是 |
Bearer |
三、能力简介
流式声音复刻提供对用户音色、说话风格、口音和声学环境音的复刻的功能。
- 输出格式:PCM 编码
- 采样率支持:8000 Hz、16000 Hz、22050 Hz
- 支持一次性合成 500 字符以内的文字(1 个汉字、1 个英文字母、1 个标点或 1 个句子中间空格均算作 1 个字符),超过 500 个字符的内容将会截断
- 仅支持 UTF-8 编码的文本输入
- 调用该模型前需要先调用声音复刻-音色注册
四、请求与响应
4.1 请求参数(Json)
| 参数名 |
说明 |
必填 |
类型 |
示例 |
req_id |
请求ID,记录该值便于排查问题 |
是 |
string |
- |
speaker_id |
全局唯一音色代号 |
是 |
string |
- |
text |
待合成的文本,需要为 UTF-8 编码 |
是 |
string |
- |
format |
音频编码格式,支持 PCM 格式,默认值:PCM |
否 |
string |
- |
sampling_rate |
音频采样率,默认值:22050 Hz |
否 |
int |
- |
4.2 响应体(Json Object)
| 参数名 |
说明 |
必填 |
类型 |
code |
状态码 |
是 |
string |
message |
状态说明 |
是 |
string |
sid |
会话ID,用于记录本次会话,仅建立连接时返回 |
否 |
string |
result |
合成结果,建立连接后返回 |
否 |
object |
合成结果 result
| 参数名 |
说明 |
必填 |
类型 |
result>>audio |
合成音频数据,经过 base64 编码 |
否 |
string |
result>>audio_len |
合成音频长度,单位:ms |
否 |
string |
result>>is_end |
标志位,true:最后一个合成片段,false:中间合成片段 |
否 |
string |
五、状态码说明
5.1 算法状态码
| 返回码 |
解释 |
说明 |
解决方法 |
10301 |
Required parameter miss |
参数错误 |
请检查参数是否正确 |
10304 |
Parse request body fail |
请求格式错误 |
查看请求的 URL body 格式是否正确,参考接口文档 |
10503 |
Server connection time out |
服务连接超时 |
联系技术人员 |
10601 |
Speaker feature illegal |
说话人特征不合法 |
联系技术人员 |
10903 |
Synthesis failed |
合成失败 |
联系技术人员 |
10000 |
Success |
成功 |
执行下一步操作 |
通用状态码请参考【状态码】中的【网关认证】。
5.2 公共状态码
- 接口响应的公共状态码由网关统一定义,一般用于标识鉴权失败、限流或服务异常,具体业务错误码、错误信息及排查建议请参考:开发指南-公共信息及状态码。
六、调用示例
6.1 请求参数示例
{
"req_id": "3a87fe9793c9-4ebd-95d4-4ce2-a80c054b",
"speaker_id": "e0639793-4119-8482-d0ab-01471575c327",
"text": "yourText"
}
6.2 Java 版本
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONObject;
import org.json.JSONException;
import javax.xml.bind.DatatypeConverter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
public class WebSocketTTSClient {
private static final Logger logger = Logger.getLogger(WebSocketTTSClient.class.getName());
private String url;
private String xAppId;
private String authorization;
private long startTime;
private long connectionEstablishedTime;
private FileOutputStream fileOutputStream;
static {
// 配置日志格式
System.setProperty("java.util.logging.SimpleFormatter.format",
"[%1$tF %1$tT] [%4$-7s] %5$s %n");
}
public WebSocketTTSClient(String url, String xAppId, String authorization) {
this.url = url;
this.xAppId = xAppId;
this.authorization = authorization;
}
public void call() throws Exception {
this.startTime = System.currentTimeMillis();
logger.info(String.format("开始建立WebSocket连接,获取当前时间:%d", startTime));
// 准备输出文件
String outputPath = "test.pcm";
try {
fileOutputStream = new FileOutputStream(outputPath);
} catch (IOException e) {
logger.severe("创建输出文件失败: " + e.getMessage());
throw e;
}
// 创建WebSocket连接
Map headers = new HashMap();
headers.put("Content-Type", "application/json");
headers.put("X-APP-ID", xAppId);
headers.put("Authorization", authorization);
CountDownLatch latch = new CountDownLatch(1);
WebSocketClient client = new WebSocketClient(URI.create(url), headers) {
@Override
public void onOpen(ServerHandshake handshakedata) {
logger.info("WebSocket连接已建立");
sendInitialRequest();
}
@Override
public void onMessage(String message) {
try {
handleMessage(message);
} catch (Exception e) {
logger.severe("处理消息时发生错误: " + e.getMessage());
close();
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
logger.info("WebSocket连接已关闭: " + reason);
try {
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (IOException e) {
logger.warning("关闭文件输出流时发生错误: " + e.getMessage());
}
latch.countDown();
}
@Override
public void onError(Exception ex) {
logger.severe("WebSocket错误: " + ex.getMessage());
latch.countDown();
}
};
try {
client.connect();
// 等待连接完成或超时
if (!latch.await(30, TimeUnit.SECONDS)) {
logger.warning("连接超时");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.severe("连接过程被中断: " + e.getMessage());
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
logger.warning("关闭文件输出流时发生错误: " + e.getMessage());
}
}
}
}
private void sendInitialRequest() {
try {
JSONObject startJson = new JSONObject();
startJson.put("req_id", "3a87fe9793c9-4ebd-95d4-4ce2-a80c054b");
startJson.put("speaker_id", "e0639793-4119-8482-d0ab-01471575c327");
startJson.put("text", "yourText");
logger.info("=================================请求参数start_json===============================");
logger.info("请求参数:" + startJson.toString());
send(startJson.toString());
} catch (JSONException e) {
logger.severe("构建请求参数时发生JSON错误: " + e.getMessage());
}
}
private void handleMessage(String message) {
try {
JSONObject respData = new JSONObject(message);
if (connectionEstablishedTime == 0) {
// 第一个响应,记录连接建立时间
if (respData.getInt("status") == 10000) {
connectionEstablishedTime = System.currentTimeMillis();
logger.info("Received initial response: " + message);
logger.info(String.format("WebSocket connection established in %.2f seconds",
(connectionEstablishedTime - startTime) / 1000.0));
}
} else {
// 处理音频数据响应
if (respData.getInt("status") != 10000) {
logger.severe("Received non-success status: " + respData);
close();
return;
}
if (respData.has("result")) {
JSONObject result = respData.getJSONObject("result");
logger.info("Received synthesis info: " + result);
// 解码音频数据并写入文件
String audioBase64 = result.getString("audio");
byte[] audioData = DatatypeConverter.parseBase64Binary(audioBase64);
fileOutputStream.write(audioData);
boolean isEnd = result.getBoolean("is_end");
if (isEnd) {
long totalDuration = System.currentTimeMillis() - startTime;
logger.info(String.format("Total interaction duration: %.2f seconds",
totalDuration / 1000.0));
close();
}
} else {
logger.severe("No result in response: " + respData);
close();
}
}
} catch (JSONException e) {
logger.severe("解析JSON响应时发生错误: " + e.getMessage());
} catch (IOException e) {
logger.severe("写入音频文件时发生错误: " + e.getMessage());
}
}
public static void main(String[] args) {
// 设置参数(实际使用时可以从命令行参数获取)
String url = "算法调用地址";
String xAppId = "yourAppId";
String authorization = "yourAuthorization";
if (args.length >= 3) {
url = args[0];
xAppId = args[1];
authorization = args[2];
}
WebSocketTTSClient client = new WebSocketTTSClient(url, xAppId, authorization);
try {
client.call();
} catch (Exception e) {
logger.severe("执行过程中发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
6.3 Python 版本
import argparse
import asyncio
import websockets
import json
import base64
import logging
import time
# 配置日志,设置日志级别为INFO,并指定日志格式
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger()
async def uws_call(args):
# 获取命令行参数中的URL
url = args.url
# 记录开始时间
start_time = time.time()
logger.info(f"开始建立websocket连接,获取当前时间:{start_time}")
header = {
"Content-Type": "application/json",
'X-APP-ID': args.X_APP_ID,
'Authorization': args.Authorization
}
# 建立WebSocket连接
async with websockets.connect(url, extra_headers=header) as websocket:
logger.info("=================================请求参数start_json===============================")
# 构建请求参数
start_json = {
"req_id": "3a87fe9793c9-4ebd-95d4-4ce2-a80c054b",
"speaker_id": "e0639793-4119-8482-d0ab-01471575c327",
"text": "yourText"
}
logger.info(f"请求参数:{start_json}")
# 发送请求参数
await websocket.send(json.dumps(start_json))
try:
# 接收响应
resp_str = await websocket.recv()
resp_data = json.loads(resp_str)
if resp_data['status'] == 10000:
logger.info('Received initial response: %s', resp_str)
# 记录连接建立(即收到第一个响应)的时间
connection_established_time = time.time()
logger.info('WebSocket connection established in %.2f seconds',
connection_established_time - start_time)
is_end = False
# 打开文件以写入音频数据
with open("test.pcm", "wb") as f:
while not is_end:
# 接收音频数据
resp_audio_str = await websocket.recv()
resp_audio_data = json.loads(resp_audio_str)
if resp_audio_data['status'] != 10000:
logger.error('Received non-success status: %s', resp_audio_data)
break
if 'result' in resp_audio_data:
synth_info = resp_audio_data['result']
logger.info('Received synthesis info: %s', synth_info)
# 解码音频数据并写入文件
audio_wav = base64.b64decode(synth_info['audio'])
f.write(audio_wav)
is_end = synth_info['is_end']
else:
logger.error('No result in response: %s', resp_audio_data)
break
# 计算整个交互的耗时(从尝试连接到接收完所有消息)
total_duration = time.time() - start_time
logger.info('Total interaction duration: %.2f seconds', total_duration)
except websockets.exceptions.ConnectionClosed as e:
logger.error('WebSocket connection closed: %s', e)
# 如果连接关闭了,我们仍然可以记录到目前为止的耗时
logger.info('Duration until connection closed: %.2f seconds', time.time() - start_time)
except Exception as e:
logger.error('An error occurred: %s', e)
# 如果发生错误的话,我们同样可以记录到目前为止的耗时
logger.info('Duration until error occurred: %.2f seconds', time.time() - start_time)
finally:
# 关闭WebSocket连接
await websocket.close()
def main(args):
# 运行异步函数
asyncio.get_event_loop().run_until_complete(uws_call(args))
if __name__ == '__main__':
# 解析命令行参数
parser = argparse.ArgumentParser()
parser.add_argument('--url', type=str, default='算法调用地址')
parser.add_argument('--X_APP_ID', type=str, default='yourAppId')
parser.add_argument('--Authorization', type=str, default='yourAuthorization')
args = parser.parse_args()
# 调用主函数
main(args)