Unity3D 交互式AI桌面寵物開發系列【三】ASR 語音識別
該系列主要介紹怎么制作AI桌面寵物的流程,我會從項目開始創建初期到最終可以和AI寵物進行交互為止,項目已經開發完成,我會仔細梳理一下流程,分步講解。 這篇文章主要講有關于語音識別 ASR 方面的一些方法。
提示:內容純個人編寫,歡迎評論點贊,來指正我。
文章目錄
- Unity3D 交互式AI桌面寵物開發系列【三】ASR 語音識別
- 前言
- 一、語音識別 (ASR) 概述
- 二、Unity開發準備階段
- 1.Unity平臺
- 2.示例:訊飛平臺
- 三、科大訊飛 ASR 短語音識別API
- 1. 創建應用
- 2. 查看免費測試服務量
- 3. 查看 WebSocket 接口信息
- 4. API 開發文檔查看
- 四、功能開發階段
- 1. Unity界面開發
- 2. 代碼分析
- 3. 代碼片段
- 4. 數據配置
- 5. 效果展示
- 6. 問題反饋
- 然后就,大功告成了!!!
- 總結
前言
本篇內容主要講Unity開發桌面寵物的語音識別功能,大家感興趣也可以了解一下這個開發方向,目前還是挺有前景的,AI智能科技發展這么迅猛,緊跟步伐哈~
下面讓我們出發吧 ------------>----------------->
一、語音識別 (ASR) 概述
語音識別(Automatic Speech Recognition,ASR)是一種技術,指的是通過計算機程序和算法,將人類所說的語音信號轉化為文字或其他形式的電子文本的過程。ASR系統通過分析語音信號中包含的聲音波形、頻譜和語音特征等信息,識別并轉錄出語音中所包含的文字內容。這項技術在語音識別軟件、智能語音助手、語音搜索、電話客服系統等領域有著廣泛的應用。
二、Unity開發準備階段
1.Unity平臺
- 該系列全部使用Unity2021.3.44開發;
- 該系列前后文章存在關聯,不懂的可以看前面文章;
- 該系列完成之后我會上傳源碼工程,著急的小伙伴可以自己寫框架,我就先編寫各模塊的獨立功能。
2.示例:訊飛平臺
- 注冊訊飛賬戶,已注冊的直接登錄;
- 創建語音識別應用,然后 領取 語音識別 (短語音識別) 免費測試服務量綁定該應用,或者付費購買服務量;(個人認證可以領取免費服務量)
- 前兩步我就不貼流程圖了,該系列在上節的語音喚醒文章中提到過注冊和領取相關功能免費服務量的流程;
- 上述操作很簡單
- 重點來了,科大訊飛平臺的ASR接口怎么接入,下面來看一下吧
三、科大訊飛 ASR 短語音識別API
1. 創建應用
- 點擊用戶下面的 “我的應用” 然后創建應用,命名語音識別的應用,創建成功后會生成一個APPKey或者APPID 保存好。(名字隨便起,記住APPID,后續要用到)
2. 查看免費測試服務量
- 個人測試的話領取免費開發測試服務量就可以了,商用的話就購買相應的服務量。
3. 查看 WebSocket 接口信息
- 記錄保存該各項數據,后續在WebSocket連接的時候要用到這些。
4. API 開發文檔查看
- 這里有開發文檔可以自行查看并學習,當然我是看過的,那么接下來就到了開發階段了!
- ps: 記住這個接口地址,這是WebSocket的API 地址 (調用的網址),接下來會用到。
四、功能開發階段
1. Unity界面開發
- ① 用于顯示識別內容的 Text 文本
- ② 用于按住進行錄音操作的 Button 按鈕
- ③ 用于顯示按鈕的提示詞的 Text 文本
- ④ 用于掛載ASR語音識別 腳本 的空物體
2. 代碼分析
- 參數:用于定義變量;
- 語音輸入:用于錄制音頻片段;
- 獲取鑒權Url:用于鑒權加密的API地址;
- 語音識別:用于實現語音識別的API接口調用功能;
- 工具方法:用于處理Unity中的音頻片段,轉換成訊飛標準格式;
- 數據定義:用于定義發送數據和接收數據的數據結構,根據第三方平臺的參數數據。
3. 代碼片段
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;public class XunfeiSpeechToText : MonoBehaviour
{#region 參數/// <summary>/// host地址/// </summary>[SerializeField] private string m_HostUrl = "iat-api.xfyun.cn";/// <summary>/// 語言/// </summary>[SerializeField] private string m_Language = "zh_cn";/// <summary>/// 應用領域/// </summary>[SerializeField] private string m_Domain = "iat";/// <summary>/// 方言mandarin:中文普通話、其他語種/// </summary>[SerializeField] private string m_Accent = "mandarin";/// <summary>/// 音頻的采樣率/// </summary>[SerializeField] private string m_Format = "audio/L16;rate=16000";/// <summary>/// 音頻數據格式/// </summary>[SerializeField] private string m_Encoding = "raw";/// <summary>/// websocket/// </summary>private ClientWebSocket m_WebSocket;/// <summary>/// 傳輸中斷標記點/// </summary>private CancellationToken m_CancellationToken;/// <summary>/// 語音識別API地址/// </summary>[SerializeField][Header("語音識別API地址")]private string m_SpeechRecognizeURL;/// <summary>/// 訊飛的AppID/// </summary>[Header("填寫APP ID")][SerializeField] private string m_AppID = "訊飛的AppID";/// <summary>/// 訊飛的APIKey/// </summary>[Header("填寫Api Key")][SerializeField] private string m_APIKey = "訊飛的APIKey";/// <summary>/// 訊飛的APISecret/// </summary>[Header("填寫Secret Key")][SerializeField] private string m_APISecret = "訊飛的APISecret";/// <summary>/// 計算方法調用的時間/// </summary>[SerializeField] protected System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();#endregionprivate void Awake(){//注冊按鈕事件RegistButtonEvent();//綁定地址 地址就是訊飛平臺上的 WebSocket API地址m_SpeechRecognizeURL = "wss://iat-api.xfyun.cn/v2/iat";}#region 語音輸入/// <summary>/// 語音輸入的按鈕/// </summary>[SerializeField] private Button m_VoiceInputBotton;/// <summary>/// 錄音按鈕的文本/// </summary>[SerializeField] private Text m_VoiceBottonText;/// <summary>/// 錄音的提示信息/// </summary>[SerializeField] private Text m_RecordTips;/// <summary>/// 錄制的音頻長度/// </summary>public int m_RecordingLength = 5;/// <summary>/// 臨時接收音頻的片段/// </summary>private AudioClip recording;/// <summary>/// 注冊按鈕事件/// </summary>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);}/// <summary>/// 開始錄制/// </summary>public void StartRecord(){m_VoiceBottonText.text = "正在錄音中...";StartRecordAudio();}/// <summary>/// 結束錄制/// </summary>public void StopRecord(){m_VoiceBottonText.text = "按住按鈕,開始錄音";m_RecordTips.text = "錄音結束,正在識別...";StopRecordAudio(AcceptClip);}/// <summary>/// 開始錄制聲音/// </summary>public void StartRecordAudio(){recording = Microphone.Start(null, true, m_RecordingLength, 16000);}/// <summary>/// 結束錄制,返回audioClip/// </summary>/// <param name="_callback"></param>public void StopRecordAudio(Action<AudioClip> _callback){Microphone.End(null);_callback(recording);}/// <summary>/// 處理錄制的音頻數據/// </summary>/// <param name="_data"></param>public void AcceptClip(AudioClip _audioClip){m_RecordTips.text = "正在進行語音識別...";SpeechToText(_audioClip, DealingTextCallback);}/// <summary>/// 處理識別到的文本/// </summary>/// <param name="_msg"></param>private void DealingTextCallback(string _msg){//在此處處理接收到的數據,可以選擇發送給大模型,或者打印測試,會在后續補充功能m_RecordTips.text = _msg;Debug.Log(_msg);}#endregion#region 獲取鑒權Url/// <summary>/// 獲取鑒權url/// </summary>/// <returns></returns>private string GetUrl(){//獲取時間戳string date = DateTime.Now.ToString("r");//拼接原始的signaturestring signature_origin = string.Format("host: " + m_HostUrl + "\ndate: " + date + "\nGET /v2/iat HTTP/1.1");//hmac-sha256算法-簽名,并轉換為base64編碼string signature = Convert.ToBase64String(new HMACSHA256(Encoding.UTF8.GetBytes(m_APISecret)).ComputeHash(Encoding.UTF8.GetBytes(signature_origin)));//拼接原始的authorizationstring authorization_origin = string.Format("api_key=\"{0}\",algorithm=\"hmac-sha256\",headers=\"host date request-line\",signature=\"{1}\"", m_APIKey, signature);//轉換為base64編碼string authorization = Convert.ToBase64String(Encoding.UTF8.GetBytes(authorization_origin));//拼接鑒權的urlstring url = string.Format("{0}?authorization={1}&date={2}&host={3}", m_SpeechRecognizeURL, authorization, date, m_HostUrl);return url;}#endregion#region 語音識別/// <summary>/// 語音識別/// </summary>/// <param name="_clip"></param>/// <param name="_callback"></param>public void SpeechToText(AudioClip _clip, Action<string> _callback){byte[] _audioData = ConvertClipToBytes(_clip);StartCoroutine(SendAudioData(_audioData, _callback));}/// <summary>/// 識別短文本/// </summary>/// <param name="_audioData"></param>/// <param name="_callback"></param>/// <returns></returns>public IEnumerator SendAudioData(byte[] _audioData, Action<string> _callback){yield return null;ConnetHostAndRecognize(_audioData, _callback);}/// <summary>/// 連接服務,開始識別/// </summary>/// <param name="_audioData"></param>/// <param name="_callback"></param>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());//獲取返回的jsonResponseData _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();if (_resultMsg.Equals(null) || _resultMsg.Equals("")){Debug.Log("語音識別為空字符串");}else{//識別的數據不為空 在此處做功能處理}Debug.Log("訊飛語音識別耗時:" + stopwatch.Elapsed.TotalSeconds);}catch (Exception ex){Debug.LogError("報錯信息: " + ex.Message);m_WebSocket.Dispose();}}/// <summary>/// 獲取識別到的文本/// </summary>/// <param name="_responseData"></param>/// <returns></returns>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_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());}#endregion#region 工具方法/// <summary>/// audioclip轉為byte[]/// </summary>/// <param name="audioClip"></param>/// <returns></returns>public byte[] ConvertClipToBytes(AudioClip audioClip){float[] samples = new float[audioClip.samples];audioClip.GetData(samples, 0);short[] intData = new short[samples.Length];byte[] bytesData = new byte[samples.Length * 2];int rescaleFactor = 32767;for (int i = 0; i < samples.Length; i++){intData[i] = (short)(samples[i] * rescaleFactor);byte[] byteArr = new byte[2];byteArr = BitConverter.GetBytes(intData[i]);byteArr.CopyTo(bytesData, i * 2);}return bytesData;}/// <summary>/// 打印錯誤日志/// </summary>/// <param name="status"></param>private void PrintErrorLog(int status){if (status == 10005){Debug.LogError("appid授權失敗");return;}if (status == 10006){Debug.LogError("請求缺失必要參數");return;}if (status == 10007){Debug.LogError("請求的參數值無效");return;}if (status == 10010){Debug.LogError("引擎授權不足");return;}if (status == 10019){Debug.LogError("session超時");return;}if (status == 10043){Debug.LogError("音頻解碼失敗");return;}if (status == 10101){Debug.LogError("引擎會話已結束");return;}if (status == 10313){Debug.LogError("appid不能為空");return;}if (status == 10317){Debug.LogError("版本非法");return;}if (status == 11200){Debug.LogError("沒有權限");return;}if (status == 11201){Debug.LogError("日流控超限");return;}if (status == 10160){Debug.LogError("請求數據格式非法");return;}if (status == 10161){Debug.LogError("base64解碼失敗");return;}if (status == 10163){Debug.LogError("缺少必傳參數,或者參數不合法,具體原因見詳細的描述");return;}if (status == 10200){Debug.LogError("讀取數據超時");return;}if (status == 10222){Debug.LogError("網絡異常");return;}}#endregion#region 數據定義/// <summary>/// 發送的數據/// </summary>[Serializable]public class PostData{[SerializeField] public CommonTag common;[SerializeField] public BusinessTag business;[SerializeField] public DataTag data;}[Serializable]public class CommonTag{[SerializeField] public string app_id = string.Empty;public CommonTag(string app_id){this.app_id = app_id;}}[Serializable]public class BusinessTag{[SerializeField] public string language = "zh_cn";[SerializeField] public string domain = "iat";[SerializeField] public string accent = "mandarin";public BusinessTag(string language, string domain, string accent){this.language = language;this.domain = domain;this.accent = accent;}}[Serializable]public class DataTag{[SerializeField] public int status = 2;[SerializeField] public string format = "audio/L16;rate=16000";[SerializeField] public string encoding = "raw";[SerializeField] public string audio = string.Empty;public DataTag(int status, string format, string encoding, string audio){this.status = status;this.format = format;this.encoding = encoding;this.audio = audio;}}[Serializable]public class ResponseData{[SerializeField] public int code = 0;[SerializeField] public string message = string.Empty;[SerializeField] public string sid = string.Empty;[SerializeField] public ResponcsedataTag data;}[Serializable]public class ResponcsedataTag{[SerializeField] public Results result;[SerializeField] public int status = 2;}[Serializable]public class Results{[SerializeField] public List<WsTag> ws;}[Serializable]public class WsTag{[SerializeField] public List<CwTag> cw;}[Serializable]public class CwTag{[SerializeField] public int sc = 0;[SerializeField] public string w = string.Empty;}#endregion}
4. 數據配置
- 將APPid、ApiKey、SecretKey替換成自己訊飛平臺上的 WebSocket 信息數據
- 變量對應UI進行配置
5. 效果展示
- 按住按鈕進行錄音,松開后進行識別,返回的結果會顯示在UI界面上,就代表成功了。
- 控制臺會有打印結果的,也一樣代表 API 接入成功。
- 注意使用時,打開電腦麥克風權限
6. 問題反饋
- 運行后有任何問題都可以在評論區進行討論~
- 代碼寫的不是很工整,多多指點,后續會進行整個系列的框架搭建
- 下一期更新暫定:
① 其他平臺ASR接入功能,例如:百度等
② 大模型LLM的接入,例如:訊飛、百度、GPT等
二選一哦!- 評論告訴我,下一期更新什么
然后就,大功告成了!!!
比心啦 ?(^_-)
總結
- 提示: 大家根據需求來做功能,后續繼續其他功能啦,不懂的快喊我。
- 大家可以在評論區討論其他系列下一期出什么內容,這個系列會繼續更新的
- 點贊收藏加關注哦~ 蟹蟹