1. 申请账号及获得AppID等
控制台填写资料
控制台-讯飞开放平台
语音听写的文档及接口说明
语音听写(流式版)WebAPI 文档 | 讯飞开放平台文档中心
关键信息:
设置参数
业务数据流参数
data
参数名 | 类型 | 必传 | 描述 |
---|---|---|---|
status | int | 是 | 音频的状态 0 :第一帧音频 1 :中间的音频 2 :最后一帧音频,最后一帧必须要发送 |
format | string | 是 | 音频的采样率支持16k和8k 16k音频:audio/L16;rate=16000 8k音频:audio/L16;rate=8000 |
encoding | string | 是 | 音频数据格式 raw:原生音频(支持单声道的pcm) speex:speex压缩后的音频(8k) speex-wb:speex压缩后的音频(16k) 请注意压缩前也必须是采样率16k或8k单声道的pcm。 lame:mp3格式(仅中文普通话和英文支持,方言及小语种暂不支持) 样例音频请参照音频样例 |
audio | string | 是 | 音频内容,采用base64编码 |
返回数据解析
返回参数
参数 | 类型 | 描述 |
---|---|---|
sid | string | 本次会话的id,只在握手成功后第一帧请求时返回 |
code | int | 返回码,0表示成功,其它表示异常,详情请参考错误码 |
message | string | 错误描述 |
data | object | 听写结果信息 |
data.status | int | 识别结果是否结束标识: 0:识别的第一块结果 1:识别中间结果 2:识别最后一块结果 |
data.result | object | 听写识别结果 |
data.result.sn | int | 返回结果的序号 |
data.result.ls | bool | 是否是最后一片结果 |
data.result.bg | int | 保留字段,无需关心 |
data.result.ed | int | 保留字段,无需关心 |
data.result.ws | array | 听写结果 |
data.result.ws.bg | int | 起始的端点帧偏移值,单位:帧(1帧=10ms) 注:以下两种情况下bg=0,无参考意义: 1)返回结果为标点符号或者为空;2)本次返回结果过长。 |
data.result.ws.cw | array | 中文分词 |
data.result.ws.cw.w | string | 字词 |
data.result.ws.cw.其他字段 sc/wb/wc/we/wp |
int/string | 均为保留字段,无需关心。如果解析sc字段,建议float与int数据类型都做兼容 |
2. 研究demo,发现问题
下载python demo和node js demo
原理都是调用websocket连接方式
nodejs 的demo可以直接运行,需要安装两个库
npm install crypto-js
npm install ws
/**
*
* 运行前:请先填写Appid、APIKey、APISecret
*
* 语音听写流式 WebAPI 接口调用示例 接口文档(必看):https://doc.xfyun.cn/rest_api/语音听写(流式版).html
* webapi 听写服务参考帖子(必看):http://bbs.xfyun.cn/forum.php?mod=viewthread&tid=38947&extra=
* 语音听写流式WebAPI 服务,热词使用方式:登陆开放平台https://www.xfyun.cn/后,找到控制台--我的应用---语音听写---服务管理--上传热词
* 注意:热词只能在识别的时候会增加热词的识别权重,需要注意的是增加相应词条的识别率,但并不是绝对的,具体效果以您测试为准。
* 错误码链接:https://www.xfyun.cn/document/error-code (code返回错误码时必看)
* @author iflytek
*/
const CryptoJS = require('crypto-js')
const WebSocket = require('ws')
var fs = require('fs')
// 系统配置
const config = {
// 请求地址
hostUrl: "wss://iat-api.xfyun.cn/v2/iat",
host: "iat-api.xfyun.cn",
//在控制台-我的应用-语音听写(流式版)获取
appid: "d9f99f36",
//在控制台-我的应用-语音听写(流式版)获取
apiSecret: "YTBkNzkyYWYwMGQ2ZDNiZTYzMDc4NDY1",
//在控制台-我的应用-语音听写(流式版)获取
apiKey: "d2519c5bab022ec11c27dc5752263604",
file: "./16k_10.pcm", //请填写您的音频文件路径
uri: "/v2/iat",
highWaterMark: 1280
}
// 帧定义
const FRAME = {
STATUS_FIRST_FRAME: 0,
STATUS_CONTINUE_FRAME: 1,
STATUS_LAST_FRAME: 2
}
// 获取当前时间 RFC1123格式
let date = (new Date().toUTCString())
// 设置当前临时状态为初始化
let status = FRAME.STATUS_FIRST_FRAME
// 记录本次识别用sid
let currentSid = ""
// 识别结果
let iatResult = []
let wssUrl = config.hostUrl + "?authorization=" + getAuthStr(date) + "&date=" + date + "&host=" + config.host
let ws = new WebSocket(wssUrl)
// 连接建立完毕,读取数据进行识别
ws.on('open', (event) => {
console.log("websocket connect!")
var readerStream = fs.createReadStream(config.file, {
highWaterMark: config.highWaterMark
});
readerStream.on('data', function(chunk) {
send(chunk)
});
// 最终帧发送结束
readerStream.on('end', function() {
status = FRAME.STATUS_LAST_FRAME
send("")
});
})
// 得到识别结果后进行处理,仅供参考,具体业务具体对待
ws.on('message', (data, err) => {
if (err) {
console.log(`err:${err}`)
return
}
res = JSON.parse(data)
if (res.code != 0) {
console.log(`error code ${res.code}, reason ${res.message}`)
return
}
let str = ""
if (res.data.status == 2) {
// res.data.status ==2 说明数据全部返回完毕,可以关闭连接,释放资源
str += "最终识别结果"
currentSid = res.sid
ws.close()
} else {
str += "中间识别结果"
}
iatResult[res.data.result.sn] = res.data.result
if (res.data.result.pgs == 'rpl') {
res.data.result.rg.forEach(i => {
iatResult[i] = null
})
str += "【动态修正】"
}
str += ":"
iatResult.forEach(i => {
if (i != null) {
i.ws.forEach(j => {
j.cw.forEach(k => {
str += k.w
})
})
}
})
console.log(str)
// ... do something
})
// 资源释放
ws.on('close', () => {
console.log(`本次识别sid:${currentSid}`)
console.log('connect close!')
})
// 建连错误
ws.on('error', (err) => {
console.log("websocket connect err: " + err)
})
// 鉴权签名
function getAuthStr(date) {
let signatureOrigin = `host: ${config.host}\ndate: ${date}\nGET ${config.uri} HTTP/1.1`
let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, config.apiSecret)
let signature = CryptoJS.enc.Base64.stringify(signatureSha)
let authorizationOrigin = `api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`
let authStr = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(authorizationOrigin))
return authStr
}
// 传输数据
function send(data) {
let frame = "";
let frameDataSection = {
"status": status,
"format": "audio/L16;rate=16000",
"audio": data.toString('base64'),
"encoding": "raw"
}
switch (status) {
case FRAME.STATUS_FIRST_FRAME:
frame = {
// 填充common
common: {
app_id: config.appid
},
//填充business
business: {
language: "zh_cn",
domain: "iat",
accent: "mandarin",
dwa: "wpgs" // 可选参数,动态修正
},
//填充data
data: frameDataSection
}
status = FRAME.STATUS_CONTINUE_FRAME;
break;
case FRAME.STATUS_CONTINUE_FRAME:
case FRAME.STATUS_LAST_FRAME:
//填充frame
frame = {
data: frameDataSection
}
break;
}
ws.send(JSON.stringify(frame))
}
播放的目录下的样例pcm文件,可以正常返回
C:\Users\pocea\Desktop\KXWELL\dev\语音识别\iat_ws_nodejs_demo>node iat-ws-node.js
websocket connect!
中间识别结果:四月
中间识别结果【动态修正】:四月十四日
中间识别结果【动态修正】:四月十三日
中间识别结果【动态修正】:四月十三日中国
中间识别结果【动态修正】:四月十三日中国台北
中间识别结果【动态修正】:四月十三日中国台北选手
中间识别结果【动态修正】:四月十三日中国台北选手代之
中间识别结果【动态修正】:四月十三日中国台北选手戴资颖在比赛
中间识别结果【动态修正】:4月13日,中国台北选手戴资颖在比赛中发球
中间识别结果:4月13日,中国台北选手戴资颖在比赛中发球,当日
中间识别结果:4月13日,中国台北选手戴资颖在比赛中发球,当日,在新加坡室内体育场举行的新加坡羽毛球公开赛女子单打半决赛中
中间识别结果:4月13日,中国台北选手戴资颖在比赛中发球,当日,在新加坡室内体育场举行的新加坡羽毛球公开赛女子单打半决赛中 ,中国台北选手戴资颖以2:1战胜日本选手山口茜
中间识别结果:4月13日,中国台北选手戴资颖在比赛中发球,当日,在新加坡室内体育场举行的新加坡羽毛球公开赛女子单打半决赛中 ,中国台北选手戴资颖以2:1战胜日本选手山口茜,晋级决赛
最终识别结果:4月13日,中国台北选手戴资颖在比赛中发球,当日,在新加坡室内体育场举行的新加坡羽毛球公开赛女子单打半决赛中 ,中国台北选手戴资颖以2:1战胜日本选手山口茜,晋级决赛。
本次识别sid:iat000dff5a@dx17a8f6bd0b67a1c802
connect close!
这里的动态修正怎么用还没搞明白。
# -*- coding:utf-8 -*-
#
# author: iflytek
#
# 本demo测试时运行的环境为:Windows + Python3.7
# 本demo测试成功运行时所安装的第三方库及其版本如下,您可自行逐一或者复制到一个新的txt文件利用pip一次性安装:
# cffi==1.12.3
# gevent==1.4.0
# greenlet==0.4.15
# pycparser==2.19
# six==1.12.0
# websocket==0.2.1
# websocket-client==0.56.0
#
# 语音听写流式 WebAPI 接口调用示例 接口文档(必看):https://doc.xfyun.cn/rest_api/语音听写(流式版).html
# webapi 听写服务参考帖子(必看):http://bbs.xfyun.cn/forum.php?mod=viewthread&tid=38947&extra=
# 语音听写流式WebAPI 服务,热词使用方式:登陆开放平台https://www.xfyun.cn/后,找到控制台--我的应用---语音听写(流式)---服务管理--个性化热词,
# 设置热词
# 注意:热词只能在识别的时候会增加热词的识别权重,需要注意的是增加相应词条的识别率,但并不是绝对的,具体效果以您测试为准。
# 语音听写流式WebAPI 服务,方言试用方法:登陆开放平台https://www.xfyun.cn/后,找到控制台--我的应用---语音听写(流式)---服务管理--识别语种列表
# 可添加语种或方言,添加后会显示该方言的参数值
# 错误码链接:https://www.xfyun.cn/document/error-code (code返回错误码时必看)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
import websocket
import datetime
import hashlib
import base64
import hmac
import json
from urllib.parse import urlencode
import time
import ssl
from wsgiref.handlers import format_date_time
from datetime import datetime
from time import mktime
import _thread as thread
STATUS_FIRST_FRAME = 0 # 第一帧的标识
STATUS_CONTINUE_FRAME = 1 # 中间帧标识
STATUS_LAST_FRAME = 2 # 最后一帧的标识
'''
正常运行返回结果:
sid:iat000d6ced@dx17a8fa05ca4a493802 call success!,data is:[{"bg": 64, "cw": [{"sc": 0, "w": "这"}]}, {"bg": 84, "cw": [{"sc": 0, "w": "是"}]}, {"bg": 104, "cw": [{"sc": 0, "w": "一"}]}, {"bg": 124, "cw": [{"sc": 0, "w": "段"}]}, {"bg": 144, "cw": [{"sc": 0, "w": "测试"}]}, {"bg": 184, "cw": [{"sc": 0, "w": "音频"}]}]
sid:iat000d6ced@dx17a8fa05ca4a493802 call success!,data is:[{"bg": 0, "cw": [{"w": "。", "sc": 0}]}]
0:00:02.382220
'''
class Ws_Param(object):
# 初始化
def __init__(self, APPID, APIKey, APISecret, AudioFile):
self.APPID = APPID
self.APIKey = APIKey
self.APISecret = APISecret
self.AudioFile = AudioFile
# 公共参数(common)
self.CommonArgs = {"app_id": self.APPID}
# 业务参数(business),更多个性化参数可在官网查看
self.BusinessArgs = {"domain": "iat", "language": "zh_cn", "accent": "mandarin", "vinfo":1,"vad_eos":2000}
# 生成url
def create_url(self):
url = 'wss://ws-api.xfyun.cn/v2/iat'
# 生成RFC1123格式的时间戳
now = datetime.now()
date = format_date_time(mktime(now.timetuple()))
# 拼接字符串
signature_origin = "host: " + "ws-api.xfyun.cn" + "\n"
signature_origin += "date: " + date + "\n"
signature_origin += "GET " + "/v2/iat " + "HTTP/1.1"
# 进行hmac-sha256进行加密
signature_sha = hmac.new(self.APISecret.encode('utf-8'), signature_origin.encode('utf-8'),
digestmod=hashlib.sha256).digest()
signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8')
authorization_origin = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % (
self.APIKey, "hmac-sha256", "host date request-line", signature_sha)
authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8')
# 将请求的鉴权参数组合为字典
v = {
"authorization": authorization,
"date": date,
"host": "ws-api.xfyun.cn"
}
# 拼接鉴权参数,生成url
url = url + '?' + urlencode(v)
# print("date: ",date)
# print("v: ",v)
# 此处打印出建立连接时候的url,参考本demo的时候可取消上方打印的注释,比对相同参数时生成的url与自己代码生成的url是否一致
# print('websocket url :', url)
return url
# 收到websocket消息的处理
def on_message(ws, message):
try:
code = json.loads(message)["code"]
sid = json.loads(message)["sid"]
if code != 0:
errMsg = json.loads(message)["message"]
print("sid:%s call error:%s code is:%s" % (sid, errMsg, code))
else:
data = json.loads(message)["data"]["result"]["ws"]
# print(json.loads(message))
result = ""
for i in data:
for w in i["cw"]:
result += w["w"]
print("sid:%s call success!,data is:%s" % (sid, json.dumps(data, ensure_ascii=False)))
except Exception as e:
print("receive msg,but parse exception:", e)
# 收到websocket错误的处理
def on_error(ws, error):
print("### error:", error)
# 收到websocket关闭的处理
def on_close(ws):
print("### closed ###")
# 收到websocket连接建立的处理
def on_open(ws):
def run(*args):
frameSize = 8000 # 每一帧的音频大小
intervel = 0.4 # 发送音频间隔(单位:s)
status = STATUS_FIRST_FRAME # 音频的状态信息,标识音频是第一帧,还是中间帧、最后一帧
with open(wsParam.AudioFile, "rb") as fp:
while True:
buf = fp.read(frameSize)
# 文件结束
if not buf:
status = STATUS_LAST_FRAME
# 第一帧处理
# 发送第一帧音频,带business 参数
# appid 必须带上,只需第一帧发送
if status == STATUS_FIRST_FRAME:
d = {"common": wsParam.CommonArgs,
"business": wsParam.BusinessArgs,
"data": {"status": 0, "format": "audio/L16;rate=16000",
"audio": str(base64.b64encode(buf), 'utf-8'),
"encoding": "lame"}} #mp3格式定义lame, pcm格式为raw
d = json.dumps(d)
ws.send(d)
status = STATUS_CONTINUE_FRAME
# 中间帧处理
elif status == STATUS_CONTINUE_FRAME:
d = {"data": {"status": 1, "format": "audio/L16;rate=16000",
"audio": str(base64.b64encode(buf), 'utf-8'),
"encoding": "lame"}}
ws.send(json.dumps(d))
# 最后一帧处理
elif status == STATUS_LAST_FRAME:
d = {"data": {"status": 2, "format": "audio/L16;rate=16000",
"audio": str(base64.b64encode(buf), 'utf-8'),
"encoding": "lame"}}
ws.send(json.dumps(d))
time.sleep(1)
break
# 模拟音频采样间隔
time.sleep(intervel)
ws.close()
thread.start_new_thread(run, ())
if __name__ == "__main__":
# 测试时候在此处正确填写相关信息即可运行
time1 = datetime.now()
wsParam = Ws_Param(APPID='d9f99f36', APISecret='YTBkNzkyYWYwMGQ2ZDNiZTYzMDc4NDY1',
APIKey='d2519c5bab022ec11c27dc5752263604',
AudioFile=r'./iat_mp3_16k.mp3')
websocket.enableTrace(False)
wsUrl = wsParam.create_url()
ws = websocket.WebSocketApp(wsUrl, on_message=on_message, on_error=on_error, on_close=on_close)
ws.on_open = on_open
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
time2 = datetime.now()
print(time2-time1)
3. 对齐文件格式
python的例子一直有个问题,开始用wav文件来测试,实际是不行的,看来科大讯飞的API还不支持,改为用PCM文件来测试,但问题又来了,怎么来生成测试文件?又怎么播放测试文件,确认结果呢?所有有了上一篇文章,怎么写一个PCM PLAYER. PCM recorder还要晚一点,要不就用QT把两者打包成一个工具也不错,呵呵。
这里的关键就是文件格式和设置参数要对应
就是连接上传参数这里
d = {"common": wsParam.CommonArgs,
"business": wsParam.BusinessArgs,
"data": {"status": 0, "format": "audio/L16;rate=16000",
"audio": str(base64.b64encode(buf), 'utf-8'),
"encoding": "lame"}} #mp3格式定义lame, pcm格式为raw
实测MP3和PCM都可以正常识别
4. 待研究
需要解决唤醒,断句等问题,因为需要实时流处理,所以还要考虑处理速度能不能满足识别后控制设备的要求。