1. 申请账号及获得AppID等

控制台填写资料

控制台-讯飞开放平台

语音听写的文档及接口说明

语音听写(流式版)WebAPI 文档 | 讯飞开放平台文档中心

关键信息:

设置参数

业务数据流参数

data

参数名类型必传描述
statusint音频的状态
0 :第一帧音频
1 :中间的音频
2 :最后一帧音频,最后一帧必须要发送
formatstring音频的采样率支持16k和8k
16k音频:audio/L16;rate=16000
8k音频:audio/L16;rate=8000
encodingstring音频数据格式
raw:原生音频(支持单声道的pcm)
speex:speex压缩后的音频(8k)
speex-wb:speex压缩后的音频(16k)
请注意压缩前也必须是采样率16k或8k单声道的pcm。
lame:mp3格式(仅中文普通话和英文支持,方言及小语种暂不支持)
样例音频请参照音频样例
audiostring音频内容,采用base64编码

返回数据解析

返回参数

参数类型描述
sidstring本次会话的id,只在握手成功后第一帧请求时返回
codeint返回码,0表示成功,其它表示异常,详情请参考错误码
messagestring错误描述
dataobject听写结果信息
data.statusint识别结果是否结束标识:
0:识别的第一块结果
1:识别中间结果
2:识别最后一块结果
data.resultobject听写识别结果
data.result.snint返回结果的序号
data.result.lsbool是否是最后一片结果
data.result.bgint保留字段,无需关心
data.result.edint保留字段,无需关心
data.result.wsarray听写结果
data.result.ws.bgint起始的端点帧偏移值,单位:帧(1帧=10ms)
注:以下两种情况下bg=0,无参考意义:
1)返回结果为标点符号或者为空;2)本次返回结果过长。
data.result.ws.cwarray中文分词
data.result.ws.cw.wstring字词
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. 待研究

需要解决唤醒,断句等问题,因为需要实时流处理,所以还要考虑处理速度能不能满足识别后控制设备的要求。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐