网上关于讯飞接入的博客都很少,按说讯飞都是业界翘楚,不知为何,很少搜索到精品,一搜就是一个要求开会员的博客,我也是醉了。讯飞提供的文档也是不清晰,我是摸着石头过河,过来了,我就给大家总结一个方法,给大家免费看,只要是我的粉丝,我永远给大家免费的阅读支持。
之前接入过web端的语音评测,那叫个麻烦建议大家不要轻易尝试,如果大家有必要的需求,可以尝试,但是建议不要玩,容易入坑。文章结尾有对各平台使用讯飞的一些看法和建议,需要的可以到文章末尾去看。
首先,我们从官网上下载demo,此时如果你还没有注册讯飞账号,那你先去注册一个开发者账号,不得不说,作为大公司,讯飞还是有些气量的,每天给账号500个免费的评测额度,(数量不知道我理解的是否正确,至少我已经免费用了一年了),然后创建自己的应用,如下图创建好以后就有了appid,这些不细说了,讯飞的文档主要就是介绍这些了,至于技术上的事儿基本没咋提
然后我们开始打包下载讯飞的sdk,我们点击上图的应用名称,然后进入到如下页面,点击下载sdk,这里的sdk会把我们生成的应用秘钥打包进去,我们只需要在app中配置一个appid就可以了。
然后我们点亮我们需要的功能,点击下载就可以,我选择了语音合成和语音测评功能,如下图,不建议选择新版,就用普通版本就可以,功能接口都一样,新版盲猜有坑:
下载完以后,打开文件夹,会看到一堆文件夹,我们需要的就是libs,其他的不需要,也不需要运行他说的sample,因为运行这个项目可能会很耗费时间,没必要。但是我们还是可以使用AS打开这个sample项目,以便于我们查找一些功能的使用方法,当然这是我的操作,你们也可以只看我的教程,不过我的教程只讲评测的功能,其实有了评测的经验,再做别的也手到擒来。
首先我们新建一个安卓项目,添加阿里云的镜像依赖,然后重新下载依赖,然后我们把上图的libs中的所有内容拷贝至Android工程的libs目录下。如下图所示:
然后,我们鼠标右键Msc.jar文件,选择一个Add As Library,将jar包添加到库里,我已经添加过了,弹不出来这个选项所以不截图了。
同时,我们在main包下创建如下图所示的文件夹,并且将上述sdk中的文件复制过来,如下图:
然后我们开始开发:
打开清单文件,然后添加文档中提到的安卓权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA" />
然后,需要混淆的话,添加一下这个,不添加会将讯飞sdk同步混淆导致闪退,如果不配置混淆就可以忽略。
-keep class com.iflytek.**{*;}
-keepattributes Signature
然后进行初始化:将如下代码行添加到应用的自定义Application或者调用评测的Activity的oncreate方法中,context用this就行:
// 将“12345678”替换成您申请的APPID,申请地址:http://www.xfyun.cn
// 请勿在“=”与appid之间添加任何空字符或者转义符
SpeechUtility.createUtility(context, SpeechConstant.APPID +"=12345678");
然后就开始调用关键方法:
我把封装的类贴出来吧,可以拿来即用,我是通过h5调用的,使用activity也是可以直接调用的:
package com.lz.english.activity.webview
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.webkit.JavascriptInterface
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.gson.Gson
import com.iflytek.cloud.EvaluatorListener
import com.iflytek.cloud.EvaluatorResult
import com.iflytek.cloud.SpeechConstant
import com.iflytek.cloud.SpeechError
import com.iflytek.cloud.SpeechEvaluator
import com.lz.english.activity.WebViewActivity
import com.lz.english.databinding.ActivityWebviewBinding
import com.lz.english.result.xml.XmlResultParser
import com.lz.english.utils.ToastUtils
import org.greenrobot.eventbus.EventBus
private const val s1 = "ise_unite"
private const val s = "ise_unite"
class JsBridge(
context: WebViewActivity,
binding: ActivityWebviewBinding,
mIse: SpeechEvaluator
) {
private val TAG = "JsBride"
// 创建语音评测对象,使用lateinit关键字标记为非空
// private lateinit var mIse:SpeechEvaluator ;
companion object {
private const val REQUEST_RECORD_AUDIO_PERMISSION = 1
}
private lateinit var context: WebViewActivity
private lateinit var binding: ActivityWebviewBinding
private lateinit var mIse: SpeechEvaluator
init {
this.context = context
this.binding = binding
this.mIse = mIse
setParams()
}
// 评测监听接口
private val mEvaluatorListener: EvaluatorListener = object : EvaluatorListener {
override fun onResult(result: EvaluatorResult, isLast: Boolean) {
Log.d(TAG, "evaluator result :$isLast")
if (isLast) {
val builder = StringBuilder()
builder.append(result.resultString)
Log.d(TAG, "evaluator result :${builder.toString()}")
mLastResult = builder.toString()
val resultParser = XmlResultParser()
val result = resultParser.parse(mLastResult)
val gsonStr = Gson().toJson(result)
Log.e(TAG,gsonStr)
if (null != result) {
binding.webView.loadUrl("javascript:recordFinishCallback('${gsonStr}')")
} else {
ToastUtils.showToast("语音解析失败")
}
}
}
override fun onError(error: SpeechError) {
Log.d(TAG, "evaluator over")
}
override fun onBeginOfSpeech() {
// 此回调表示:sdk内部录音机已经准备好了,用户可以开始语音输入
Log.d(TAG, "evaluator begin")
binding.webView.loadUrl("javascript:androidStartRecord()")
}
override fun onEndOfSpeech() {
// 此回调表示:检测到了语音的尾端点,已经进入识别过程,不再接受语音输入
Log.d("WebView", "evaluator stoped")
}
override fun onVolumeChanged(volume: Int, data: ByteArray) {
Log.d("WebView","当前音量:$volume")
Log.d(TAG, "返回音频数据:" + data.size)
}
override fun onEvent(eventType: Int, arg1: Int, arg2: Int, obj: Bundle) {
// 以下代码用于获取与云端的会话id,当业务出错时将会话id提供给技术支持人员,可用于查询会话日志,定位出错原因
// if (SpeechEvent.EVENT_SESSION_ID == eventType) {
// String sid = obj.getString(SpeechEvent.KEY_EVENT_SESSION_ID);
// Log.d(TAG, "session id =" + sid);
// }
}
}
@JavascriptInterface
fun goBack() {
EventBus.getDefault().post("view_goBack")
}
// 启动录音
@JavascriptInterface
fun startRecord(content:String,timeout:String,vad_bos:String,vad_eos:String) {
Log.e(TAG,"timeout::$timeout")
Log.e(TAG,"vad_bos::$vad_bos")
Log.e(TAG,"vad_eos::$vad_eos")
if (ContextCompat.checkSelfPermission(this.context, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED
) {
// 如果没有录音权限,请求权限
ActivityCompat.requestPermissions(
this.context,
arrayOf(Manifest.permission.RECORD_AUDIO),
REQUEST_RECORD_AUDIO_PERMISSION
)
} else {
// 已经有录音权限,执行录音操作
mIse?.setParameter(SpeechConstant.KEY_SPEECH_TIMEOUT, timeout)
mIse?.setParameter(SpeechConstant.VAD_BOS, vad_bos)// 设置语音前端点:静音超时时间,即用户多长时间不说话则当做超时处理
mIse?.setParameter(SpeechConstant.VAD_EOS, vad_eos)// 设置语音后端点:后端点静音检测时间,即用户停止说话多长时间内即认为不再输入, 自动停止录音
val int = mIse?.startEvaluating(content, null, mEvaluatorListener)
if (mIse == null){
Log.e(TAG,"mIse is null")
}
Log.e(TAG,"startRecord")
Log.e(TAG,"ErrorCode:$int")
}
}
// 停止录音
@JavascriptInterface
fun stopRecord() {
Log.e(TAG,"stoping...")
mIse.stopEvaluating()
}
// 评测语种
private var language: String? = null
// 评测题型
private var category: String? = null
// 结果等级
private var result_level: String? = null
private var mLastResult: String? = null
private fun setParams() {
// mIse = SpeechEvaluator.createEvaluator(context, null);
// 设置评测语言
language = "en_us"
// 设置需要评测的类型
category = "read_sentence"
// 设置结果等级(中文仅支持complete)
result_level = "complete"
// 设置语音前端点:静音超时时间,即用户多长时间不说话则当做超时处理
val vad_bos = "5000"
// 设置语音后端点:后端点静音检测时间,即用户停止说话多长时间内即认为不再输入, 自动停止录音
val vad_eos = "1800"
// 语音输入超时时间,即用户最多可以连续说多长时间;
val speech_timeout = "5000"
// 设置流式版本所需参数 : ent sub plev
if ("zh_cn" == language) {
mIse?.setParameter("ent", "cn_vip")
}
if ("en_us" == language) {
mIse?.setParameter("ent", "en_vip")
}
mIse?.setParameter(SpeechConstant.SUBJECT, "ise")
mIse?.setParameter("plev", "0")
// 设置评分百分制 使用 ise_unite rst extra_ability 参数, 其中ise_unite==0是5分制,==1是百分制
mIse?.setParameter("ise_unite", "1")
mIse?.setParameter("rst", "entirety")
mIse?.setParameter("extra_ability", "syll_phone_err_msg;pitch;multi_dimension")
mIse?.setParameter(SpeechConstant.LANGUAGE, language)
mIse?.setParameter(SpeechConstant.ISE_CATEGORY, category)
mIse?.setParameter(SpeechConstant.TEXT_ENCODING, "utf-8")
// mIse?.setParameter(SpeechConstant.VAD_BOS, vad_bos)
// mIse?.setParameter(SpeechConstant.VAD_EOS, vad_eos)
// mIse?.setParameter(SpeechConstant.KEY_SPEECH_TIMEOUT, speech_timeout)
mIse?.setParameter(SpeechConstant.RESULT_LEVEL, result_level)
mIse?.setParameter(SpeechConstant.AUDIO_FORMAT_AUE, "opus")
// 设置音频保存路径,保存音频格式支持pcm、wav,
mIse?.setParameter(SpeechConstant.AUDIO_FORMAT, "wav")
mIse?.setParameter(
SpeechConstant.ISE_AUDIO_PATH,
context.getExternalFilesDir("msc")?.absolutePath + "/ise.wav"
)
//通过writeaudio方式直接写入音频时才需要此设置
//mIse?.setParameter(SpeechConstant.AUDIO_SOURCE,"-1");
}
}
我来解释一下上面的工具类里的内容,可以看到方法上有一个注解@JavascriptInterface,这个注解是表示是给h5的js代码调用的,如果不涉及和前端js交互的话,这个注解可以删。其中评测最关键的方法,就是setParams方法,这个方法配置了一些打分参数等,注释已经很详细了,然后就是startRecord方法,这个方法就是开始录音的方法,这个方法还动态传入了几个参数,这几个参数是用来配置几个时长的,其中
timeout:String是用来设置最长录制几秒就自动停止录制并开始打分,
vad_bos:String是表示用户多长时间不说话则当做超时处理,
vad_eos:String是表示用户停止说话多长时间内即认为不再输入, 自动停止录音
还有一些配置在setParams方法中的参数,特别说一个分制的参数,有满分百分制和5分制,其中ise_unite==0是5分制,==1是百分制:
// 设置评分百分制 使用 ise_unite rst extra_ability 参数, 其中ise_unite==0是5分制,==1是百分制
mIse?.setParameter("ise_unite", "1")
然后就是关键的结果处理监听回调mEvaluatorListener了,这个是一个监听对象,这个对象复写了回调方法,来处理讯飞返回的结果,这里用到了一个类库,XmlResultParser,这个类库我们需要从讯飞的sdk中的sample中去copy过来,如图,我们把result文件夹完整复制
然后粘贴到自己项目包下的位置,如图,此时我们需要把这里面所有文件中引用的错误的包名路径修改成我们自己的正确的包名,然后再去工具类中引用XmlResultParser工具,这个工具,顾名思义,就是讯飞的程序员手撸了一个xml解析工具
sample自带的这个XmlResultParser工具,里面的实体类的数据不是很全,比如有一些流畅分,准确度分这些都没有,我们可以手动加一下,如图,打开result文件,我加了三个分值数据,在这里加的属性,必须得是人家返回结果包含的,不能随便加:
result这里加了以后,还需要在这里加一下解析:
然后怎么调用这个工具类呢?很简单,先在activity中创建一个mIse全局对象,在oncreate中赋值,我这里只写了赋值,声明的代码你自己写吧,然后传入这个对象,至于我用的这个工具对象jsBridge,你完全可以不按照我这个来,你可以直接吧工具类定义成object类型,怎么调用都可以。我是将上下文this传了过去,以防有ui操作,我还将binding传了进去,你们不需要可以不传,然后把mIse评测对象传了进去。
mIse = SpeechEvaluator.createEvaluator(this, null)
jsBridge = JsBridge(this, binding,mIse!!)
如果你觉得麻烦,甚至可以直接将工具类的那部分关键代码复制到你的activity里,省的麻烦,我之所以分离,是因为我的activity里逻辑太多了,看着有些头大。
到此,就算大功告成了,写博客太累了,为了帮助大家理解,我仔细梳理了流程,花了有三个小时吧,要是我个人做记录,可能复制几个代码就够了,所以如果帮到阁下,还望给个关注,虽然关注也不挣钱,但是看到有人跟我有互动,我就知道这个世界,不止我一个人。
以下说一些个人观点,不想看或者不认同的就可以离开了:
关于科大讯飞:讯飞给了每天500的免费评测数量,这个气量,就比某些公司要强很多了,再加上讯飞的技术确实很好,所以基本在同行业市场上没有对手,比如云知声(个人免费半年,企业免费一年,有安卓sdk但是文档说明比葛优的头发还要稀疏),还有一些小众的比如有个叫什么驰声的这种完全没有免费额度的(而且sdk还是你在他们官网留下电话然后他们的销售给你打电话联系你,然后加你微信跟你拉个群让技术给申请一个appkey来使用),当然,讯飞的翻译功能也强于专业做翻译的同行不少,比如有道,有道的翻译比较死板,讯飞没有专攻翻译领域,否则应该就没有有道翻译的市场了。用过讯飞输入法的应该都知道,讯飞输入法自带语音转文字,甚至可以你直接说中文,他转英文文字,而且是多年前就很准确了,甚至他还能识别方言,其实要说国内的AI大模型,我唯一比较期待的就是讯飞的大模型了,关于文心一言和千义通问,我是不怎么看好的,因为这两为了竞争市场匆忙上线,上下文关联性以及对于中文理解能力跟ChatGPT比起来差得不是一点半点,甚至听说文心一言还推出了付费版本,这一点就挺搞笑的,有点搞不清楚自己的斤两,也有可圈可点的地方,那就是免费版支持图片生成,虽然生成的图不尽如人意吧,但是也算一个得分项。
关于浏览器端:因为浏览器对于录音功能的限制,也就只能局限于PC端用一用了,移动端用h5来访问录音功能,体验很不好。要开发移动端使用的,建议还是开发一个app,如果实在想做网页,也可以使用原生壳子加webview嵌套h5来实现讯飞语音的功能,这样可以使用原生的录音功能和讯飞sdk,这样实现起来用户使用很舒服,开发也很舒服。
关于uniapp:还有就是uniapp的客户端版不要接入科大讯飞的所有内容,这是因为科大讯飞已经封禁了新客户的http的请求方式,只能通过websocket的方式进行流式数据传输,而uniapp的录音分片功能不支持app端,只支持部分小程序,而uniapp的h5端就更不考虑了,uni开发是挺舒服的,但是遇到问题了,就知道uniapp还不如原生vue项目好做。其实如果非小程序需求,不建议使用uniapp做开发,除非是特别简单的页面需求,否则涉及一点点特殊定制的硬件功能,可能就歇菜了,举个简单例子,uniapp不支持使用formdata进行数据传输,而他提供了一个叫upload的方法,说是能代替进行formdata 传输,但是格式不能自定义,相当于白搞,再就是从uniapp的文档上就可以看到各种的不支持,有的功能不支持app端,有的不支持h5端,有的不支持小程序端,有的干脆只支持h5,有的只支持微信小程序,有的只支持抖音小程序,
说句唱衰的话,就uni这样的开发平台,你说该咋用,说他是跨平台解决方案弃之可惜吧,但是用着又如此食之无味,这大概也就是他这么多年没火起来的原因吧,给你们截个图你们就大概明白了,这个截图只是冰山一角,
所以想搞跨平台方案的,建议还是考虑清楚,毕竟貌似uni的团队并不打算维护这个uniapp了,那些社区里陈年的bug,貌似也打算让他继续陈年了,千万别相信uni市场上那些收费的插件,可能各平台版本更新一次插件就歇菜了,听说最近uni出了一个appx,貌似很吊的样子,说是性能和原生一样,众所周知,uniapp是一个webview的性能,性能很差,要是这个appx能做好,确实会很有市场,那将是要超越flutter的存在了,毕竟uni还能兼容小程序,但是要我相信dcloud团队能做好他,诸葛亮来当说客,我也得犹豫一下要不要信,我预估这个appx能用的周期应该是10年左右,但是技术更新换代这么快,10年也许写代码都不用太多的人力了,所以本人暂时先不考虑用了。