项目实训(4)——Unity实现语音转文字STT功能_unity 语音转文字
基础设施建设的第四部分来到了语音转文字STT部分,这一部分仍然先使用科大讯飞的模型(主要是讯飞开放平台已经配过了)。
准备部分
UI准备
准备好语音输入模式的UI,配置鼠标事件,在点击语音输入按钮后需要切换文本输入框和语音输入框的状态,中间框起来的是UI的基本结构,右侧基本上操作的也是这些部分。
API准备
参考上一篇,依然是讯飞开放平台控制台-讯飞开放平台,选择语音识别——语音听写(流式版),只要没有切换应用,Websocket服务接口认证信息和上一部分TTS就用的是同一块,然后记下来API地址。
配置部分
对话控制器处理
private void Awake() { m_CommitMsgBtn.onClick.AddListener(delegate { SendData(); }); RegistButtonEvent(); }
在Awake部分增加按钮事件注册
private void RegistButtonEvent() { if (m_VoiceInputBotton == null || m_VoiceInputBotton.GetComponent()) return; EventTrigger _trigger = m_VoiceInputBotton.gameObject.AddComponent(); //添加按钮按下的事件 EventTrigger.Entry _pointDown_entry = new EventTrigger.Entry(); _pointDown_entry.eventID = EventTriggerType.PointerDown; _pointDown_entry.callback = new EventTrigger.TriggerEvent(); //添加按钮松开事件 EventTrigger.Entry _pointUp_entry = new EventTrigger.Entry(); _pointUp_entry.eventID = EventTriggerType.PointerUp; _pointUp_entry.callback = new EventTrigger.TriggerEvent(); //添加委托事件 _pointDown_entry.callback.AddListener(delegate { StartRecord(); }); _pointUp_entry.callback.AddListener(delegate { StopRecord(); }); _trigger.triggers.Add(_pointDown_entry); _trigger.triggers.Add(_pointUp_entry); }
当用户按下按钮时调用 StartRecord() —— 开始语音录制,当用户松开按钮时调用Stop()按钮——结束语音录制。
这里需要特别说明一下Unity的监听和委托机制:
1.首先创建监听器_trigger监听条目,这里EventTrigger.Entry 是一个监听“条目”,代表一个具体事件类型的监听器。
2.通过设置监听器eventID 指定监听哪种事件,比如 PointerDown(按下)、PointerUp(松开)等。
3.定义回调事件,callback 是一个 UnityEvent,即事件响应函数的集合,首先让它完成初始化,再向其中添加委托事件。
4.最后将监听器添加到EventTrigger 的 triggers 列表,完成注册。
StartRecord()与StopRecord()如下:
public void StartRecord() { m_VoiceBottonText.text = \"正在录音中...\"; m_VoiceInputs.StartRecordAudio(); } public void StopRecord() { m_VoiceBottonText.text = \"按住按钮,开始录音\"; m_RecordTips.text = \"录音结束,正在识别...\"; m_VoiceInputs.StopRecordAudio(AcceptClip); }
这里使用的VoiceInputs是子组件录音器,这里负责调用麦克风录音并存储录音文件。
public void StartRecordAudio() { recording = Microphone.Start(null, false, m_RecordingLength, 16000); } /// /// 结束录制,返回audioClip /// /// public void StopRecordAudio(Action _callback) { Microphone.End(null); _callback(recording); }
回到对话控制器,这里使用了回调机制将函数 AcceptClip()作为参数传过去,以便于在完成StopRecordAudio之后直接进行音频片段处理。
private void AcceptClip(AudioClip _audioClip) { if (m_ChatSettings.m_SpeechToText == null) return; m_ChatSettings.m_SpeechToText.SpeechToText(_audioClip, DealingTextCallback); }
这里具体的处理开始调用设置的STT组件,也就是准备阶段科大讯飞部署的部分,将音频转换为相应的文字,而在完成处理之后直接回调DealingTextCallback以继续进行对话部分。
private void DealingTextCallback(string _msg) { m_RecordTips.text = _msg; StartCoroutine(SetTextVisible(m_RecordTips)); //自动发送 if (m_AutoSend) { SendData(_msg); return; } m_InputWord.text = _msg; }
TTS类部分
从对话控制器调用过来,转换完音频数据类型之后启动了Unity协程来异步发送音频数据。
public override void SpeechToText(AudioClip _clip, Action _callback) { byte[] _audioData = ConvertClipToBytes(_clip); StartCoroutine(SendAudioData(_audioData, _callback)); }
public IEnumerator SendAudioData(byte[] _audioData, Action _callback) { yield return null; ConnetHostAndRecognize(_audioData, _callback); }
在协程中通过websocket启动与讯飞服务器的连接,发送音频数据,并解析返回的语音识别结果,这里参考了API的使用文档。
private async void ConnetHostAndRecognize(byte[] _audioData, Action _callback) { try { stopwatch.Restart(); //建立socket连接 m_WebSocket = new ClientWebSocket(); m_CancellationToken = new CancellationToken(); Uri uri = new Uri(GetUrl()); await m_WebSocket.ConnectAsync(uri, m_CancellationToken); //开始识别 SendVoiceData(_audioData, m_WebSocket); StringBuilder stringBuilder = new StringBuilder(); while (m_WebSocket.State == WebSocketState.Open) { var result = new byte[4096]; await m_WebSocket.ReceiveAsync(new ArraySegment(result), m_CancellationToken); //去除空字节 List list = new List(result); while (list[list.Count - 1] == 0x00) list.RemoveAt(list.Count - 1); string str = Encoding.UTF8.GetString(list.ToArray()); //获取返回的json ResponseData _responseData = JsonUtility.FromJson(str); if (_responseData.code == 0) { stringBuilder.Append(GetWords(_responseData)); } else { PrintErrorLog(_responseData.code); } m_WebSocket.Abort(); } string _resultMsg = stringBuilder.ToString(); //识别成功,回调 _callback(_resultMsg); stopwatch.Stop(); Debug.Log(\"讯飞语音识别耗时:\" + stopwatch.Elapsed.TotalSeconds); } catch (Exception ex) { Debug.LogError(\"报错信息: \" + ex.Message); m_WebSocket.Dispose(); } }
private string GetWords(ResponseData _responseData) { StringBuilder stringBuilder = new StringBuilder(); foreach (var item in _responseData.data.result.ws) { foreach (var _cw in item.cw) { stringBuilder.Append(_cw.w); } } return stringBuilder.ToString(); } private void SendVoiceData(byte[] audio, ClientWebSocket socket) { if (socket.State != WebSocketState.Open) { return; } PostData _postData = new PostData() { common = new CommonTag(m_XunfeiSettings.m_AppID), business = new BusinessTag(m_Language, m_Domain, m_Accent), data = new DataTag(2, m_Format, m_Encoding, Convert.ToBase64String(audio)) }; string _jsonData = JsonUtility.ToJson(_postData); //发送数据 socket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(_jsonData)), WebSocketMessageType.Binary, true, new CancellationToken()); }
关于协程
由于我们在Unity中一般不考虑多线程,所以要使用协程。
协程,从字面意义上理解就是协助程序的意思,我们在主任务进行的同时,需要一些分支任务配合工作来达到最终的效果。
稍微形象的解释一下,想象一下,在进行主任务的过程中我们需要一个对资源消耗极大的操作时候,如果在一帧中实现这样的操作,项目就会变得十分卡顿,这个时候,我们就可以通过协程,在一定帧内完成该工作的处理,同时不影响主任务的进行。
协程和线程之间是有异有同,对于协程而言,同一时间只能执行一个协程,而线程则是并发的,可以同时有多个线程在运行。但两者在内存的使用上是相同的,共享堆,不共享栈。