项目实训(4)——Unity实现语音转文字STT功能

基础设施建设的第四部分来到了语音转文字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<EventTrigger>())
            return;

        EventTrigger _trigger = m_VoiceInputBotton.gameObject.AddComponent<EventTrigger>();

        //添加按钮按下的事件
        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);
        }

        /// <summary>
        /// 结束录制,返回audioClip
        /// </summary>
        /// <param name="_callback"></param>

 public void StopRecordAudio(Action<AudioClip> _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<string> _callback)
    {
        byte[] _audioData = ConvertClipToBytes(_clip);
        StartCoroutine(SendAudioData(_audioData, _callback));
    }
public IEnumerator SendAudioData(byte[] _audioData, Action<string> _callback)
    {
        yield return null;
        ConnetHostAndRecognize(_audioData, _callback);
    }

 在协程中通过websocket启动与讯飞服务器的连接,发送音频数据,并解析返回的语音识别结果,这里参考了API的使用文档。


    private async void ConnetHostAndRecognize(byte[] _audioData, Action<string> _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<byte>(result), m_CancellationToken);
                //去除空字节
                List<byte> list = new List<byte>(result); while (list[list.Count - 1] == 0x00) list.RemoveAt(list.Count - 1);
                string str = Encoding.UTF8.GetString(list.ToArray());
                //获取返回的json
                ResponseData _responseData = JsonUtility.FromJson<ResponseData>(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<byte>(Encoding.UTF8.GetBytes(_jsonData)), WebSocketMessageType.Binary, true, new CancellationToken());
    }

关于协程

由于我们在Unity中一般不考虑多线程,所以要使用协程。
协程,从字面意义上理解就是协助程序的意思,我们在主任务进行的同时,需要一些分支任务配合工作来达到最终的效果。
稍微形象的解释一下,想象一下,在进行主任务的过程中我们需要一个对资源消耗极大的操作时候,如果在一帧中实现这样的操作,项目就会变得十分卡顿,这个时候,我们就可以通过协程,在一定帧内完成该工作的处理,同时不影响主任务的进行。
协程和线程之间是有异有同,对于协程而言,同一时间只能执行一个协程,而线程则是并发的,可以同时有多个线程在运行。但两者在内存的使用上是相同的,共享堆,不共享栈。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值