文章目录
讯飞ASR转写API完整指南
0. 引言
在这篇博客中,我们将详细解析如何使用讯飞ASR(自动语音识别)API进行音频转写,包括上传音频、查询转写结果及解析返回数据。本文将涵盖API的参数说明,并提供完整的Python代码,确保代码能够顺利执行。
1. 流程图
2. 讯飞ASR API介绍
讯飞ASR API提供了一整套音频转写的解决方案,主要流程如下:
生成签名 - 认证请求的合法性。
上传音频 - 通过URL方式或本地文件上传音频。
查询结果 - 轮询转写结果,等待识别完成。
解析结果 - 处理返回的JSON数据,提取文本和说话人信息。
3. API参数说明
3.1 认证参数
参数名 | 说明 |
---|---|
appId | 讯飞开发者平台分配的应用ID |
secret_key | 用于生成签名的密钥 |
ts | 时间戳,单位为秒 |
signa | 认证签名,由appId、ts和secret_key计算得出 |
3.2 上传基本参数
根据实际情况调优
参数名 | 说明 |
---|---|
fileName | 音频文件名称 |
fileSize | 文件大小(若使用URL方式可随意填写) |
duration | 音频时长(单位秒,可随机填写) |
language | 语言(cn代表中文) |
audioMode | 上传模式(urlLink 代表通过URL上传) |
audioUrl | 音频文件的URL(需要URL编码) |
3.3 查询结果参数
参数名 | 说明 |
---|---|
orderId | 订单ID,用于查询转写结果 |
resultType | 返回结果类型(transfer 表示最终转写文本) |
成功
{
"code": "000000",
"descInfo": "success",
"content": {
"orderId": "DKHJQ202209021522090215490FAAE7DD0008C",
"taskEstimateTime": 28000
}
}
失败
{
"code": "26600",
"descInfo": "转写业务通用错误"
}
3.4 orderResult 字段
参数名 | 类型 | 说明 |
---|---|---|
lattice | List | 做顺滑功能的识别结果 |
lattice2 | List | 未做顺滑功能的识别结果,当开启顺滑和后语规整后 orderResult 才返回 lattice2 字段(需要开通权限) |
label | Object | 转写结果标签信息,用于补充转写结果相关信息,标记转写结果角色和声道的对应关系 |
3.5 Lattice 字段
参数名 | 类型 | 说明 |
---|---|---|
json_1best | String | 单个 VAD 的结果的 JSON 内容 |
3.6 json_1best 字段
参数名 | 类型 | 说明 |
---|---|---|
st | Object | 单个句子的结果对象 |
3.7 st 字段
参数名 | 类型 | 说明 |
---|---|---|
bg | String | 单个句子的开始时间,单位毫秒 |
ed | String | 单个句子的结束时间,单位毫秒 |
rl | String | 分离的角色编号,取值正整数,需开启角色分离的功能才返回对应的分离角色编号 |
rt | List | 输出词语识别结果集合 |
4. java代码实现
4.1 生成签名
/**
* 加签加密抽象类
*
*/
public abstract class AbstractSignature {
/**
* 签名ID
*/
private String id;
/**
* 加密key
*/
private String key;
/**
* 服务url
*/
private String url;
/**
* 加密算法
*/
private String encryptType;
/**
* 待加密原始字符
*/
private String originSign;
/**
* 最终生成的签名
*/
protected String signa;
/**
* 时间戳timestamp
*/
private String ts;
/**
* 请求类型,默认get
*/
protected String requestMethod = "GET";
/**
* @param id
* @param key
* @param url
*/
public AbstractSignature(String id, String key, String url) {
this.id = id;
this.key = key;
this.url = url;
this.ts = generateTs();
}
/**
* 可设置请求类型
* @param id
* @param key
* @param url
* @param isPost 是否为POST
*/
public AbstractSignature(String id, String key, String url, boolean isPost) {
this.id = id;
this.key = key;
this.url = url;
if (isPost) {
this.requestMethod = "POST";
}else{
this.requestMethod = "GET";
}
this.ts = generateTs();
}
/**
* 生成ts时间
*/
public String generateTs() {
return String.valueOf(System.currentTimeMillis() / 1000L);
}
/**
* 完成签名,返回完整签名
*
* @return
* @throws SignatureException
*/
public abstract String getSigna() throws SignatureException;
public String generateOriginSign() throws SignatureException {
try {
URL url = new URL(this.getUrl());
return "host: " + url.getHost() + "\n" +
"date: " + this.getTs() + "\n" +
"GET " + url.getPath() + " HTTP/1.1";
} catch (MalformedURLException e) {
throw new SignatureException("MalformedURLException:" + e.getMessage());
}
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getOriginSign() {
return originSign;
}
public void setOriginSign(String originSign) {
this.originSign = originSign;
}
public String getTs() {
return ts;
}
public void setTs(String ts) {
this.ts = ts;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getEncryptType() {
return encryptType;
}
public void setEncryptType(String encryptType) {
this.encryptType = encryptType;
}
}
签名
public class LfasrSignature extends AbstractSignature {
/**
*
* @param appId
* @param keySecret
*/
public LfasrSignature(String appId, String keySecret) {
super(appId, keySecret, null);
}
@Override
public String getSigna() throws SignatureException {
if (ObjectUtils.isEmpty(this.signa)) {
this.setOriginSign(generateOriginSign());
this.signa = generateSignature();
}
return this.signa;
}
/**
* 生成最终的签名,需要先生成原始sign
*
* @throws SignatureException
*/
public String generateSignature() throws SignatureException {
return CryptTools.hmacEncrypt(CryptTools.HMAC_SHA1, this.getOriginSign(), this.getKey());
}
/**
* 生成待加密原始字符
*
* @throws NoSuchAlgorithmException
*/
@Override
public String generateOriginSign() throws SignatureException {
return CryptTools.md5Encrypt(this.getId() + this.getTs());
}
}
4.2 上传音频文件
1、文件上传
#概述
首先调用文件上传接口,上传待转写音频文件的基本信息(文件名、大小等)和相关的可配置参数。
调用成功,返回订单ID(orderId
,用于查询结果或者联调排查问题时使用),是后续接口的必传参数。
#请求示例
https://raasr.xfyun.cn/v2/api/upload?duration=200&signa=Je5YsBvPcsbB4qy8Qvzd367fiv0%3D&fileName=%E9%98%B3%E5%85%89%E6%80%BB%E5%9C%A8%E9%A3%8E%E9%9B%A8%E5%90%8E.speex-wb&fileSize=11895&sysDicts=uncivilizedLanguage&appId=3e79d91c&ts=1662101767
#URL
POST https: //raasr.xfyun.cn/v2/api/upload
#请求头
Content-Type: application/json; charset=UTF-8,Chunked: false
#signa生成
public String createAsrUpload(String audioUrl, String language, Long orgId, String pd) {
HashMap<String, Object> params = new HashMap<>(32);
try {
String fileName = new File(new URL(audioUrl).getPath()).getName();
LfasrSignature lfasrSignature = new LfasrSignature(appid, keySecret);
params.put("appId", appid);
params.put("signa", lfasrSignature.getSigna());
params.put("ts", lfasrSignature.getTs());
params.put("fileName", fileName);
params.put("fileSize", "url自定义、file实际值");
params.put("duration", "200");
params.put("audioMode", "urlLink");
params.put("roleType", "1");
params.put("roleNum", "2");
// 远近场模式 1:远场模式 (默认) 2:近场模式
params.put("eng_vad_mdn", "2");
String encodeAudioUrl = URLEncoder.encode(audioUrl, "UTF-8");
params.put("audioUrl", encodeAudioUrl);
if (encodeAudioUrl.length() > 512) {
throw new RuntimeException("audioUrl length must not be greater than 512");
}
if (StringUtils.isNotBlank(language)) {
params.put("language", language);
} else {
params.put("language", "cn");
}
if (StringUtils.isNotBlank(pd)) {
params.put("pd", pd);
}
} catch (Exception e) {
throw new Exception("参数构建失败: " + e.getMessage());
}
//
String paramString = HttpUtil.parseMapToPathParam(params);
String url ="https://raasr.xfyun.cn" + "/v2/api/upload" + "?" + paramString;
String response = HttpUtil.iflyrecUpload(url, null);
return response;
}
注意对参数的key和value编码
URLEncoder.encode(entry.getKey(), UTF8)
4.3 获取转写结果
status == 4
private static final String API_GET_RESULT = "/v2/api/getResult";
/**
* 获取任务结果
*
* @param orderId 任务id
* @return 请求结果
* @throws SignatureException 签名异常
*/
public String getResult(String orderId) throws SignatureException {
HashMap<String, Object> map = new HashMap<>(16);
LfasrSignature lfasrSignature = new LfasrSignature(appid, keySecret);
map.put("orderId", orderId);
map.put("signa", lfasrSignature.getSigna());
map.put("ts", lfasrSignature.getTs());
map.put("appId", appid);
map.put("resultType", "transfer");
String paramString = HttpUtil.parseMapToPathParam(map);
String url = HOST + API_GET_RESULT + "?" + paramString;
String response = HttpUtil.iflyrecGet(url);
return response;
}
查询结构返回数据
{
"code": "000000",
"descInfo": "success",
"content": {
"orderInfo": {
"orderId": "DKHJQ2022090510220905100562536FEF00062",
"failType": 0,
"status": 4,
"originalDuration": 200,
"realDuration": 1878
},
"orderResult": "{\"lattice\":[{\"json_1best\":\"{\\\"st\\\":{\\\"sc\\\":\\\"0.86\\\",\\\"pa\\\":\\\"0\\\",\\\"rt\\\":[{\\\"ws\\\":[{\\\"cw\\\":[{\\\"w\\\":\\\"这\\\",\\\"wp\\\":\\\"n\\\",\\\"wc\\\":\\\"1.0000\\\"}],\\\"wb\\\":1,\\\"we\\\":16},{\\\"cw\\\":[{\\\"w\\\":\\\"是\\\",\\\"wp\\\":\\\"n\\\",\\\"wc\\\":\\\"1.0000\\\"}],\\\"wb\\\":17,\\\"we\\\":36},{\\\"cw\\\":[{\\\"w\\\":\\\"一\\\",\\\"wp\\\":\\\"n\\\",\\\"wc\\\":\\\"1.0000\\\"}],\\\"wb\\\":37,\\\"we\\\":52},{\\\"cw\\\":[{\\\"w\\\":\\\"条\\\",\\\"wp\\\":\\\"n\\\",\\\"wc\\\":\\\"1.0000\\\"}],\\\"wb\\\":53,\\\"we\\\":80},{\\\"cw\\\":[{\\\"w\\\":\\\"测试\\\",\\\"wp\\\":\\\"n\\\",\\\"wc\\\":\\\"1.0000\\\"}],\\\"wb\\\":81,\\\"we\\\":116},{\\\"cw\\\":[{\\\"w\\\":\\\"音频\\\",\\\"wp\\\":\\\"n\\\",\\\"wc\\\":\\\"1.0000\\\"}],\\\"wb\\\":117,\\\"we\\\":172},{\\\"cw\\\":[{\\\"w\\\":\\\"。\\\",\\\"wp\\\":\\\"p\\\",\\\"wc\\\":\\\"0.0000\\\"}],\\\"wb\\\":172,\\\"we\\\":172},{\\\"cw\\\":[{\\\"w\\\":\\\"\\\",\\\"wp\\\":\\\"g\\\",\\\"wc\\\":\\\"0.0000\\\"}],\\\"wb\\\":172,\\\"we\\\":172}]}],\\\"bg\\\":\\\"50\\\",\\\"rl\\\":\\\"0\\\",\\\"ed\\\":\\\"1840\\\"}}\"}],\"lattice2\":[{\"lid\":\"0\",\"end\":\"1840\",\"begin\":\"50\",\"json_1best\":{\"st\":{\"sc\":\"0.86\",\"pa\":\"0\",\"rt\":[{\"nb\":\"1\",\"nc\":\"1.0\",\"ws\":[{\"cw\":[{\"w\":\"这\",\"wp\":\"n\",\"wc\":\"1.0000\"}],\"wb\":1,\"we\":16},{\"cw\":[{\"w\":\"是\",\"wp\":\"n\",\"wc\":\"1.0000\"}],\"wb\":17,\"we\":36},{\"cw\":[{\"w\":\"一\",\"wp\":\"n\",\"wc\":\"1.0000\"}],\"wb\":37,\"we\":52},{\"cw\":[{\"w\":\"条\",\"wp\":\"n\",\"wc\":\"1.0000\"}],\"wb\":53,\"we\":80},{\"cw\":[{\"w\":\"测试\",\"wp\":\"n\",\"wc\":\"1.0000\"}],\"wb\":81,\"we\":116},{\"cw\":[{\"w\":\"音频\",\"wp\":\"n\",\"wc\":\"1.0000\"}],\"wb\":117,\"we\":172},{\"cw\":[{\"w\":\"。\",\"wp\":\"p\",\"wc\":\"0.0000\"}],\"wb\":172,\"we\":172},{\"cw\":[{\"w\":\"\",\"wp\":\"g\",\"wc\":\"0.0000\"}],\"wb\":172,\"we\":172}]}],\"pt\":\"reserved\",\"bg\":\"50\",\"si\":\"0\",\"rl\":\"0\",\"ed\":\"1840\"}},\"spk\":\"段落-0\"}]}",
"taskEstimateTime": 0
}
4.4 循环获取转写结果
LfasrResponse uploadResponse = lfasrClient.uploadUrl(AUDIO_URL);
if (uploadResponse == null) {
logger.error("上传失败,响应为空");
return;
}
if (!StringUtils.equals(uploadResponse.getCode(), "000000")) {
logger.error("上传失败,错误码:{},错误信息:{}", uploadResponse.getCode(), uploadResponse.getDescInfo());
return;
}
String orderId = uploadResponse.getContent().getOrderId();
logger.info("转写任务orderId:{}", orderId);
// 3、查询转写结果
int status = LfasrOrderStatusEnum.CREATED.getKey();
// 循环直到订单完成或失败
while (status != LfasrOrderStatusEnum.COMPLETED.getKey() && status != LfasrOrderStatusEnum.FAILED.getKey()) {
LfasrResponse resultResponse = lfasrClient.getResult(orderId, TASK_TYPE);
if (!StringUtils.equals(resultResponse.getCode(), "000000")) {
logger.error("转写任务失败,错误码:{},错误信息:{}", resultResponse.getCode(), resultResponse.getDescInfo());
return;
}
// 获取订单状态信息
if (resultResponse.getContent() != null && resultResponse.getContent().getOrderInfo() != null) {
status = resultResponse.getContent().getOrderInfo().getStatus();
int failType = resultResponse.getContent().getOrderInfo().getFailType();
// 根据状态输出日志
LfasrOrderStatusEnum statusEnum = LfasrOrderStatusEnum.getEnum(status);
if (statusEnum != null) {
logger.info("订单状态:{}", statusEnum.getValue());
// 如果订单失败,输出失败原因
if (statusEnum == LfasrOrderStatusEnum.FAILED) {
LfasrFailTypeEnum failTypeEnum = LfasrFailTypeEnum.getEnum(failType);
logger.error("订单处理失败,失败原因:{}", failTypeEnum.getValue());
return;
}
// 如果订单已完成,输出结果
if (statusEnum == LfasrOrderStatusEnum.COMPLETED) {
printResult(resultResponse);
return;
}
} else {
logger.error("未知的订单状态:{}", status);
}
} else {
logger.error("返回结果中缺少订单信息");
}
TimeUnit.SECONDS.sleep(20);
}
}
4.5解析转写结果
/**
* 从转写结果的lattice数组中提取文本
*/
private static String getLatticeText(List<LfasrOrderResult.Lattice> latticeList) {
StringBuilder resultText = new StringBuilder();
for (LfasrOrderResult.Lattice lattice : latticeList) {
LfasrOrderResult.Json1Best json1Best = lattice.getJson1Best();
if (json1Best == null || json1Best.getSt() == null || json1Best.getSt().getRt() == null) {
continue;
}
String rl = json1Best.getSt().getRl();
StringBuilder rlText = getRlText(json1Best);
resultText.append("角色-").append(rl).append(":").append(rlText).append("\n");
}
return resultText.toString();
}
/**
* 从Json1Best中提取识别结果文本并拼接
*/
private static StringBuilder getRlText(LfasrOrderResult.Json1Best json1Best) {
StringBuilder rlText = new StringBuilder();
for (LfasrOrderResult.RecognitionResult rt : json1Best.getSt().getRt()) {
if (rt.getWs() == null) {
continue;
}
for (LfasrOrderResult.WordResult ws : rt.getWs()) {
if (ws.getCw() != null && !ws.getCw().isEmpty()) {
// 获取每个词的识别结果
String word = ws.getCw().get(0).getW();
if (word != null && !word.isEmpty()) {
rlText.append(word);
}
}
}
}
return rlText;
}
def parse_result(self, result_json):
"""
解析转写结果,按说话人分组
"""
try:
result = json.loads(result_json)
speakers = {}
if 'lattice2' in result:
for item in result['lattice2']:
speaker = item.get('spk', '未知')
json_1best = json.loads(item['json_1best'])
text = "".join(cw['w'] for rt in json_1best.get('st', {}).get('rt', []) for ws in rt.get('ws', []) for cw in ws.get('cw', []) if 'w' in cw)
speakers.setdefault(speaker, []).append(text)
return speakers
except Exception as e:
return None
5. 请求与返回示例
5.1 成功返回示例
参数回看:3.3 查询结果参数
{
"code": "000000",
"descInfo": "success",
"content": {
"orderId": "DKHJQ202209021522090215490FAAE7DD0008C",
"taskEstimateTime": 28000
}
}
5.2 异步回调
1. 转写结束异步回调状态
当订单转写流程结束时会回调用户(如果录音文件转写接口 upload
传了callbackUrl
),会把订单号和订单状态返回,具体的格式和参数说明如下: 回调地址示例:
GET http://ip:prot/server/xxx?orderId=DKHJQ202004291620042916580FBC96690001F&status=1
5.3注意 QPS 和 API 限流
//{"code":"100012","descInfo":"access too fast, please wait a moment"}
// {"code":“26603","descInfo":"接口访问频率受限”}
else if (code.equals("100012") || code.equals("26603") || code.equals("26605")) {
if (tryCount < MAX_RETRY_COUNT) {
// 队列处理
this.submitToQue(orderId, tryCount + 1, taskId ,type);
} else {
throw new Exception("Max retry count reached for orderId: "+orderId);
}
} else {
throw new Exception("讯飞响应异常:" + code + "resp:" + respResult);
}
CODE: 100012 26603
可进行队列延迟重试处理
6. 文档地址
7. 常见问题
录音文件转写支持哪些音频格式?
答:目前录音文件转写支持的音频格式为:已录制音频(5小时内),wav,flac,opus,m4a,mp3,单声道&多声道,支持语种:中文普通话、英语、开通的小语种以及中文方言,采样率:8KHz,16KHz
#录音文件转写支不支持并发?
答:支持,要保证同一个appid每秒请求接口次数最大值在20次以下。
#录音文件转写可以试用吗?
答:可以领取新用户礼包,根据您认证的程度,提供最多50小时的免费时长,有效期为一年。
#录音文件转写支持什么语言?
答:支持语种:中文普通话、英语,小语种以及中文方言可以到控制台-语音转写-方言/语种处添加试用或购买;设置方式参考上述语言参数切换即可
#录音文件转写的套餐扣费顺序是怎样的?
答:扣量优先级:免费试用>批量购买,即在“批量购买”的套餐额度剩余的情况下,又领取了免费试用的体验包,则领取的免费试用体验包立即生效,并被设定为当前扣量套餐。而之前购买的套餐包的额度和到期日不变。
8. 错误码
错误码 描述 | |
26600 转写业务通用错误,检查下请求参数是否正确 | |
26601 非法应用信息,检查下上传的appi是否正确 | |
26602 任务ID不存在 | |
26603 接口访问频率受限 | |
26604 获取结果次数超过限制 | |
26605 任务正在处理中,请稍后重试 | |
26606 空音频,请检查 | |
26607 转写语种未授权或已过有效期 | |
26610 请求参数错误 | |
26621 预处理文件大小受限(500M) | |
26622 预处理音频时长受限(5小时) | |
26623 预处理音频格式受限 | |
26625 预处理服务时长不足。您剩余的可用服务时长不足,请移步产品页http://www.xfyun.cn/services/lfasr 进行购买或者免费领取 | |
26631 音频文件大小受限(500M) | |
26632 音频时长受限(5小时) | |
26633 音频服务时长不足。您剩余的可用服务时长不足,请移步产品页http://www.xfyun.cn/services/lfasr 进行购买或者免费领取 | |
26634 文件下载失败 | |
26635 文件长度校验失败 | |
26640 文件上传失败 | |
26641 上传分片超过限制 | |
26642 分片合并失败 | |
26643 计算音频时长失败,请检查您的音频是否加密或者损坏 | |
26650 音频格式转换失败,请检查您的音频是否加密或者损坏 | |
26660 计费计量失败 | |
26670 转写结果集解析失败 | |
26671 下载转写结果失败 | |
26680 引擎错误 | |
26681 引擎获取订单异常 | |
26682 引擎订单处理中 | |
26689 引擎网络异常 |
9. 结论
本文详细讲解了讯飞ASR API的使用流程,包括如何生成签名、上传音频、查询结果并解析返回数据。希望这篇文章对你有所帮助!如果对你有帮助,帮忙给个一键三连,求求了,各位吴彦祖,刘亦菲们