声音转换Websocket API
功能介绍
声音转换Websocket API 满足用户实时声音转换的需求,允许用户以音频流的形式输入原始声音,同时转换后的声音也以流的形式实时推送回去,可以做到边说话边转换的实时效果。声音转换可以支持不同年龄、性别的说话人声音转换换成目标音色,目标音色会携带说话人的语气、情感、口音等特征。
音频要求
- 支持音频文件的编码格式及文件名的后缀: pcm。
- 支持音频文件的采样率/位深: 16000Hz/16bit。
- 支持的语言:中文普通话。
- 音频有效时长:不超过180分钟。
- 声道:单声道
使用方法
服务地址
访问类型 | 说明 | URL | Host |
---|---|---|---|
外网访问 | websocket协议 | wss://openapi.data-baker.com/ws/voice_conversion | openapi.data-baker.com |
交互流程
请求参数
参数名 | 参数格式 | 是否必填 | 说明 |
---|---|---|---|
access_token | string | 是 | 通过client_id,client_secret调用授权服务获得见 获取访问令牌 |
enable_vad | bool | 否 | true代表启动服务端vad功能,默认false。如果启动系统会根据输入音频进行检测,过滤环境噪音。否则直接将原始输入音频进行转换。 |
align_input | bool | 否 | true代表输出音频与输入音频进行对齐,默认false。即开启vad时会保留静音部分,false丢弃静音部分 |
voice_name | string | 是 | 发音人参数即转换的目标声音 |
lastpkg | bool | 是 | 是否为最后一包,当时发送最后一包数据时设置为true 告诉系统输入完成 |
ws数据包格式
格式说明
-
第一部分 HEAD
四字节的二进制,长度为一个大端的32位int转换而来,表示第二部分JSON数据的长度,转换关系如下:
//length转byte[] int32_t length = 123; unsigned char B[4]; B[0] = length >> 24 & 0xFF; B[1] = length >> 16 & 0xFF; B[2] = length >> 8 & 0xFF; B[3] = length & 0xFF; //byte[]转length length = (B[0] << 24) + (B[1] << 16) + (B[2] << 8) + B[3];
- 第二部分是一个JSON的字符串
- 第三部分是音频的二进制数据,若数据包中不包含音频,则第三部分可以为空
Python示例代码
import argparse import json import wave import requests import websocket from threading import Thread #websocket客户端 class Client: def __init__(self, data, uri, save_path): self.data = data self.uri = uri self.converted_data = b"" self.save_path = save_path #建立连接 def connect(self): ws_app = websocket.WebSocketApp(uri, on_open=self.on_open, on_message=self.on_message, on_error=self.on_error, on_close=self.on_close) ws_app.run_forever() # 建立连接后发送消息 def on_open(self, ws): print("sending..") def run(*args): for message in self.data: ws.send(message, websocket.ABNF.OPCODE_BINARY) Thread(target=run).start() # 接收消息 def on_message(self, ws, message): length = int.from_bytes(message[:4], byteorder='big', signed=False) json_data = json.loads((message[4: length + 4]).decode()) self.converted_data += message[4 + length:] if json_data['lastpkg']: with wave.open(self.save_path, 'wb') as wavfile: wavfile.setparams((1, 2, 16000, 0, 'NONE', 'NONE')) wavfile.writeframes(self.converted_data) ws.close() print("task finished successfully") code = json.loads(message).get("errcode") print(str(json.loads(message))) if code != 0: # 打印接口错误 print(message) # 打印错误 def on_error(slef, ws, error): print("error: ", str(error)) # 关闭连接 def on_close(ws): print("client closed.") # 准备数据 def prepare_data(args, access_token): # 填写Header信息 voice_name = args.voice_name with open(args.file_path, 'rb') as f: file = f.read() data = [] for i in range(0, len(file), 32000): if i + 32000 > len(file): tts_params = {"access_token": access_token, "voice_name": voice_name, 'enable_vad': True, 'align_input': True, "lastpkg": True} else: tts_params = {"access_token": access_token, "voice_name": voice_name, 'enable_vad': True, 'align_input': True, "lastpkg": False} json_data = json.dumps(tts_params) json_data_bi = json_data.encode() length = len(json_data) head_data = length.to_bytes(4, byteorder='big') if i + 32000 > len(file): data.append(head_data + json_data_bi + file[i:]) else: data.append(head_data + json_data_bi + file[i: i + 32000]) return data # 获取命令行输入参数 def get_args(): text = "今天天气不错哦!" parser = argparse.ArgumentParser(description='ASR') parser.add_argument('-client_secret', type=str, required=True) parser.add_argument('-client_id', type=str, required=True) parser.add_argument('-file_path', type=str, required=True) parser.add_argument('-file_save_path', type=str, required=True) parser.add_argument('--voice_name', type=str, default='Vc_baklong') args = parser.parse_args() return args # 获取access_token用于鉴权 def get_access_token(client_secret, client_id): grant_type = "client_credentials" url = "https://openapi.data-baker.com/oauth/2.0/token?grant_type={}&client_secret={}&client_id={}" \ .format(grant_type, client_secret, client_id) try: response = requests.post(url) response.raise_for_status() except Exception as e: print(response.text) raise Exception else: access_token = json.loads(response.text).get('access_token') return access_token if __name__ == '__main__': try: args = get_args() # 获取access_token client_secret = args.client_secret client_id = args.client_id access_token = access_token = get_access_token(client_secret, client_id) # 准备数据 data = prepare_data(args, access_token) uri = "wss://openapi.data-baker.com/ws/voice_conversion" # 建立Websocket连接 client = Client(data, uri, args.file_save_path) client.connect() except Exception as e: print(e)
命令行执行
如有需要可自行修改参数
voice_conversion.py -client_secret=您的client_secret -client_id=您的client_id -file_path=test.pcm -file_save_path=output.wav
JAVA示例代码
package com.databaker.web.vc; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import okhttp3.*; import okio.ByteString; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ArrayUtils; import java.io.*; import java.text.SimpleDateFormat; import java.util.Date; /** * 声音转换WebSocket API接口调用示例 * 附:声音转换Websocket API文档 【https://www.data-baker.com/specs/file/vc_api_websocket】 * <p> * 注意:仅作为demo示例,失败重试、token过期重新获取、日志打印等优化工作需要开发者自行完成 * * @author data-baker */ public class VcWebSocketDemo extends WebSocketListener { /** * 授权:需要在开放平台获取【https://ai.data-baker.com/】 */ private static final String clientId = "YOUR_CLIENT_ID"; private static final String clientSecret = "YOUR_CLIENT_SECRET"; /** * 获取token的地址信息 */ public static String tokenUrl = "https://openapi.data-baker.com/oauth/2.0/token?grant_type=client_credentials&client_secret=%s&client_id=%s"; private static final String hostUrl = "wss://openapi.data-baker.com/ws/voice_conversion"; /** * 文件路径【开发者需要根据实际路径调整。支持的音频编码格式:PCM,采样率16K,位深16bit,中文普通话,时长不超过180分钟】 */ private static final String pcmFile = "/home/asr/16k_16bit.pcm"; /** * 文件夹路径【声音转换后文件存放地址,开发者需要根据实际路径调整。】 */ private static final String dic = "/home/asr"; private static final SimpleDateFormat sdf = new SimpleDateFormat("yyy-MM-dd HH:mm:ss.SSS"); // 开始时间 private static ThreadLocal<Date> timeBegin = ThreadLocal.withInitial(() -> new Date()); // 结束时间 private static ThreadLocal<Date> timeEnd = ThreadLocal.withInitial(() -> new Date()); // 发音人 private String voiceName = "Vc_baklong"; private Date startTime; private String accessToken = getAccessToken(); @Override public void onOpen(WebSocket webSocket, Response response) { super.onOpen(webSocket, response); this.startTime = timeBegin.get(); // 该demo直接从文件中读取音频流【实际场景可能是实时从麦克风获取音频流,开发者自行修改获取音频流的逻辑即可】 new Thread(() -> { File file = new File(pcmFile); // 连接成功,开始发送数据 // 第二部分是一个JSON的字符串 JSONObject jsonObject = new JSONObject(); jsonObject.put("access_token", accessToken); jsonObject.put("voice_name", voiceName); jsonObject.put("enable_vad", true); jsonObject.put("align_input", true); jsonObject.put("lastpkg", false); int length = jsonObject.toJSONString().getBytes().length; // 第一部分 byte[] b = new byte[4]; b[0] = (byte) (length >> 24 & 0xFF); b[1] = (byte) (length >> 16 & 0xFF); b[2] = (byte) (length >> 8 & 0xFF); b[3] = (byte) (length & 0xFF); FileInputStream fileInputStream = null; ByteArrayOutputStream byteOut = null; try { fileInputStream = new FileInputStream(file); byteOut = new ByteArrayOutputStream(); int size = 32000; byte[] byteArray = new byte[size]; int totalLength = 0; int read = 0; // 发送音频 while ((read = fileInputStream.read(byteArray, 0, size)) > 0) { totalLength += read; System.out.println(); if (read == size && totalLength < file.length()) { byte[] bytes = ArrayUtils.addAll(ArrayUtils.addAll(b, jsonObject.toJSONString().getBytes()), byteArray); webSocket.send(new ByteString(bytes)); Thread.sleep(40); } else { // 最后一包 jsonObject.put("lastpkg", true); length = jsonObject.toJSONString().getBytes().length; b[0] = (byte) (length >> 24 & 0xFF); b[1] = (byte) (length >> 16 & 0xFF); b[2] = (byte) (length >> 8 & 0xFF); b[3] = (byte) (length & 0xFF); byte[] subarray = ArrayUtils.subarray(byteArray, 0, read); byte[] bytes = ArrayUtils.addAll(ArrayUtils.addAll(b, jsonObject.toJSONString().getBytes()), subarray); webSocket.send(new ByteString(bytes)); } } System.out.println("all data is send"); } catch (Exception e) { e.printStackTrace(); } finally { if (fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (byteOut != null) { try { byteOut.close(); } catch (IOException e) { e.printStackTrace(); } } } }).start(); } @Override public void onMessage(WebSocket webSocket, String text) { super.onMessage(webSocket, text); } @Override public void onMessage(WebSocket webSocket, ByteString bytes) { super.onMessage(webSocket, bytes); byte[] byteArray = bytes.toByteArray(); // byte[]转length int length = (byteArray[0] << 24) + (byteArray[1] << 16) + (byteArray[2] << 8) + byteArray[3]; byte[] jsonArray = ArrayUtils.subarray(byteArray, 4, 4 + length); String jsonStr = new String(jsonArray); JSONObject jsonObject = JSONObject.parseObject(jsonStr); byte[] subarray = ArrayUtils.subarray(byteArray, 4 + length, byteArray.length); File resultPcmFile = new File(dic, new File(pcmFile).getName().replace(".pcm", "_result.pcm")); if (!resultPcmFile.exists()) { try { resultPcmFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } try { // 写入文件 FileOutputStream outputStream = new FileOutputStream(resultPcmFile, true); IOUtils.write(subarray, outputStream); IOUtils.closeQuietly(outputStream); if ((int) jsonObject.get("errcode") == 0) { if (jsonObject.getBoolean("lastpkg")) { //说明数据全部返回完毕,可以关闭连接,释放资源 System.out.println("session end "); System.out.println(sdf.format(startTime) + "开始"); System.out.println(sdf.format(timeEnd.get()) + "结束"); System.out.println("耗时:" + (timeEnd.get().getTime() - startTime.getTime()) + "ms"); System.out.println("本次声音转换traceId ==》" + jsonObject.getString("traceid")); webSocket.close(1000, ""); } } else { System.out.println("errCode=>" + jsonObject.getInteger("errcode") + " errMsg=>" + jsonObject.getString("errmsg") + " traceId=" + jsonObject.getString("traceid")); // 关闭连接 webSocket.close(1000, ""); System.out.println("发生错误,关闭连接"); return; } } catch (Exception e) { e.printStackTrace(); } } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { super.onFailure(webSocket, t, response); try { if (null != response) { int code = response.code(); System.out.println("onFailure code:" + code); System.out.println("onFailure body:" + response.body().string()); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public static void main(String[] args) { OkHttpClient client = new OkHttpClient.Builder().build(); Request request = new Request.Builder().url(hostUrl).build(); client.newWebSocket(request, new VcWebSocketDemo()); } public static String getAccessToken() { String accessToken = ""; OkHttpClient client = new OkHttpClient(); // request 默认是get请求 String url = String.format(tokenUrl, clientSecret, clientId); Request request = new Request.Builder().url(url).build(); JSONObject jsonObject; try { Response response = client.newCall(request).execute(); if (response.isSuccessful()) { // 解析 String resultJson = response.body().string(); jsonObject = JSON.parseObject(resultJson); accessToken = jsonObject.getString("access_token"); } } catch (Exception e) { e.printStackTrace(); } return accessToken; } }
响应结果
参数名 | 参数格式 | 是否必填 | 说明 |
---|---|---|---|
errcode | int | 是 | 错误码,0 代表成功其他代表识别,参考错误码说明 |
errmsg | string | 是 | 错误信息描述 |
traceid | string | 是 | 会话唯一id,用于追踪定位问题 |
lastpkg | bool | 是 | 是否为最后一包,当时发送最后一包数据时设置为true 告诉调用方输出完成 |
成功时:
'\x00\x00\x00Q{"errcode":0,"errmsg":"success","lastpkg":false,"traceid":"7018124205863151147"}\n\xfc\xff\xfc\xff\x03\x00\x02\x00\x02\x00\x02\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x06\x00\x04\x00\x04\x00\x04\x00\xfd\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\xfa\xff\xfc\xff\xfc\xff\xf7\xff\xf9\xff\xf9\xff\xfa\xff\xfb\xff\xfb\xff\xfc\xff\xfd\xff\xfd\xff\xfe\xff\xfe\xff\xfe\xff\xff\xff\xff\xff\xf9\xff\xfb\xff\xfb\xff\xfc\xff\xfc\xff\x02\x00\x01\x00\x01\x00\x01\x00\x01\x00\x02\x00\x01\x00\x01\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfa\xff\xfc\xff\xfd\xff\xfd\xff\xf8\xff\xf9\xff\xfa\xff\xfb\xff\xfc\xff\xfc\xff\x02\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfb\xff\x02\x00\x01\x00\x01\x00\x01\x00\x00\x00\x06\x00\x05\x00\x04\x00\t\x00\x07\x00\x07\x00\x06\x00\x05\x00\x05\x00\x04\x00\x03\x00\x03\x00\x02\x00\x07\x00\x06\x00\x05\x00\x05\x00\x04\x00\x04\x00\x04\x00\x03\x00\x02\x00\x02\x00\x02\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x06\x00\xff\xff\x00\x00\x00\x00\x06\x00\x05\x00\t\x00\x07\x00\x06\x00\x06\x00\x05\x00\x05\x00\x04\x00\x03\x00\x03\x00\x02\x00\x02\x00\x02\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\xfb\xff\xfd\xff\xfd\xff\xfd\xff\xfe\xff\xfe\xff\xfe\xff\xf9\xff'
失败时:
'\x00\x00\x00s{"errcode":10000,"errmsg":"Param error cause parse payload failed","lastpkg":true,"traceid":"7017724786985292155"}\n'
错误码
错误码 | 含义 | 类别 |
---|---|---|
0 | 成功 | 请求参数错误 |
10000 | ws数据包解析错误 | |
10001 | ws数据包json解析错误 | |
10002 | 请求参数缺少access token | |
10003 | access token过期或不合法 | |
10004 | 请求参数缺少 voice name | |
10005 | voice name 不存在 | |
20000 | 引擎资源不足,拒绝新建会话 | 服务端错误 |
20001 | 声音转换出错 | |
20002 | 动态内存申请失败 | |
20003 | 向客户端推送响应失败 | |
20004 | 其他未定义的内部错误 | |
30000 | 会话进行时客户端主动关闭 | 客户端错误 |
30001 | 会话空闲超时,超过一定时间没有数据传输导致会话空闲超时,主动断开连接 |