ToB企服应用市场:ToB评测及商务社交产业平台

标题: Unity对接科大讯飞实时语音转写WebAPI(Windows平台)(一) [打印本页]

作者: 欢乐狗    时间: 2024-7-22 09:40
标题: Unity对接科大讯飞实时语音转写WebAPI(Windows平台)(一)
科大讯飞官方文档:实时语音转写 API 文档 | 讯飞开放平台文档中心 (xfyun.cn)
参考文章:unity通过WebAPI连接Websocket实现讯飞语音辨认与合成。_unity websocket audio-CSDN博客
        要实现语音转文字。起首我们须要从麦克风获取到语音数据,这里用到了Microphone类,Unity自带;其次,须要将语音数据发送给讯飞,这里用到的是WebSocketSharp.WebSocket,用习惯了。然后就是按照文档一步步踩坑了。
        直接贴代码了。代码重要实现握手阶段参数签名,实时通讯阶段的数据传输以及结果剖析。
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using System;
  5. using WebSocketSharp;
  6. using System.Text;
  7. using System.Security.Cryptography;
  8. using LitJson;
  9. using Newtonsoft.Json;
  10. public class SpeechHelper : MonoBehaviour
  11. {
  12.     public event Action<string> 语音识别完成事件;   //语音识别回调事件
  13.     public AudioClip RecordedClip;
  14.     private string micphoneName = string.Empty;
  15.     WebSocket speechWebSocket;
  16.     private System.Action<string> resultCallback;
  17.     public void InitSpeechHelper(System.Action<string> textCallback)
  18.     {
  19.         resultCallback = textCallback;
  20.     }
  21.     public void StartSpeech()
  22.     {
  23.         if (speechWebSocket != null && speechWebSocket.ReadyState == WebSocketState.Open)
  24.         {
  25.             Debug.LogWarning("开始语音识别失败!,等待上次识别连接结束");
  26.             return;
  27.         }
  28.         if(Microphone.devices.Length <= 0)
  29.         {
  30.             Debug.LogWarning("找不到麦克风");
  31.             return;
  32.         }
  33.         messageQueue.Clear();
  34.         micphoneName = Microphone.devices[0];
  35.         Debug.Log("micphoneName:" + micphoneName);
  36.         try
  37.         {
  38.             RecordedClip = Microphone.Start(micphoneName, false, 60, 16000);
  39.             ConnectSpeechWebSocket();
  40.         }
  41.         catch(Exception ex)
  42.         {
  43.             Debug.LogError(ex.Message);
  44.         }
  45.     }
  46.     public void StopSpeech()
  47.     {
  48.         Microphone.End(micphoneName);
  49.         Debug.Log("识别结束,停止录音");
  50.     }
  51.     void ConnectSpeechWebSocket()
  52.     {
  53.         try
  54.         {
  55.             speechWebSocket = new WebSocket(GetWebSocketUrl());
  56.         }
  57.         catch (Exception ex)
  58.         {
  59.             UnityEngine.Debug.LogError(ex.Message);
  60.             return;
  61.         }
  62.         speechWebSocket.OnOpen += (sender, e) =>
  63.         {
  64.             Debug.Log("OnOpen");
  65.             speechWebSocket.OnClose += OnWebSocketClose;
  66.         };
  67.         speechWebSocket.OnMessage += OnInitMessage;
  68.         speechWebSocket.OnError += OnError;
  69.         speechWebSocket.ConnectAsync();
  70.         StartCoroutine(SendVoiceData());
  71.     }
  72.     void OnWebSocketClose(object sender, CloseEventArgs e)
  73.     {
  74.         Debug.Log("OnWebSocketClose");
  75.     }
  76.     private static Queue<string> messageQueue = new Queue<string>();
  77.     void OnInitMessage(object sender, MessageEventArgs e)
  78.     {
  79.         UnityEngine.Debug.Log("qqqqqqqqqqqqqWebSocket数据返回:" + e.Data);
  80.         messageQueue.Enqueue(e.Data);
  81.     }
  82.     private void MainThreadOnMessage(string message)
  83.     {
  84.         try
  85.         {
  86.             XFResponse response = JsonConvert.DeserializeObject<XFResponse>(message);
  87.             if (0 != response.code)
  88.             {
  89.                
  90.                 return;
  91.             }
  92.             if (response.action.Equals("result"))
  93.             {
  94.                 var result = ParseXunfeiRecognitionResult(response.data);
  95.                 if(result.IsFinal)
  96.                 {
  97.                     Debug.Log("Text最终:" + result.Text);
  98.                     resultCallback?.Invoke(result.Text);
  99.                 }else
  100.                 {
  101.                     Debug.Log("Text中间:" + result.Text);
  102.                 }
  103.             }
  104.         }
  105.         catch (Exception ex)
  106.         {
  107.             Debug.LogError(ex.Message);
  108.         }
  109.     }
  110.     void OnError(object sender, ErrorEventArgs e)
  111.     {
  112.         UnityEngine.Debug.Log("WebSoclet:发生错误:" + e.Message);
  113.     }
  114.     public SpeechRecognitionResult ParseXunfeiRecognitionResult(string dataJson)
  115.     {
  116.         StringBuilder builder = new StringBuilder();
  117.         SpeechRecognitionResult res = new SpeechRecognitionResult();
  118.         try
  119.         {
  120.             JsonData data = JsonMapper.ToObject(dataJson);
  121.             JsonData cn = data["cn"];
  122.             JsonData st = cn["st"];
  123.             if (st["ed"].ToString().Equals("0"))
  124.             {
  125.                 res.IsFinal = false;
  126.             }
  127.             else
  128.             {
  129.                 res.IsFinal = true;
  130.             }
  131.             JsonData rtArry = st["rt"];
  132.             foreach (JsonData rtObject in rtArry)
  133.             {
  134.                 JsonData wsArr = rtObject["ws"];
  135.                 foreach (JsonData wsObject in wsArr)
  136.                 {
  137.                     JsonData cwArr = wsObject["cw"];
  138.                     foreach (JsonData cwObject in cwArr)
  139.                     {
  140.                         builder.Append(cwObject["w"].ToString());
  141.                     }
  142.                 }
  143.             }
  144.         }catch(Exception ex)
  145.         {
  146.             Debug.LogError(ex.Message);
  147.         }
  148.         res.Text = builder.ToString();
  149.         return res;
  150.     }
  151.     void SendData(byte[] voiceData)
  152.     {
  153.         Debug.Log("SendData:" + voiceData.Length + ",time:" + Time.realtimeSinceStartup);
  154.         if (speechWebSocket.ReadyState != WebSocketState.Open)
  155.         {
  156.             return;
  157.         }
  158.         try
  159.         {
  160.             if (speechWebSocket != null && speechWebSocket.IsAlive)
  161.             {
  162.                 speechWebSocket.SendAsync(voiceData, success =>
  163.                 {
  164.                     if (success)
  165.                     {
  166.                         UnityEngine.Debug.Log("WebSoclet:发送成功:" + voiceData.Length);
  167.                     }
  168.                     else
  169.                     {
  170.                         UnityEngine.Debug.Log("WebSoclet:发送失败:");
  171.                     }
  172.                 });
  173.             }
  174.         }
  175.         catch
  176.         {
  177.         }
  178.     }
  179.     void SendEndMsg(System.Action callback)
  180.     {
  181.         string endMsg = "{"end": true}";
  182.         byte[] data = Encoding.UTF8.GetBytes(endMsg);
  183.         try
  184.         {
  185.             if (speechWebSocket != null && speechWebSocket.IsAlive)
  186.             {
  187.                 speechWebSocket.SendAsync(data, success =>
  188.                 {
  189.                     if (success)
  190.                     {
  191.                         UnityEngine.Debug.Log("WebSoclet:发送END成功:" + data.Length);
  192.                     }
  193.                     else
  194.                     {
  195.                         UnityEngine.Debug.Log("WebSoclet:发送END失败:");
  196.                     }
  197.                     callback?.Invoke();
  198.                 });
  199.             }
  200.         }
  201.         catch
  202.         {
  203.         }
  204.     }
  205.     IEnumerator SendVoiceData()
  206.     {
  207.         yield return new WaitUntil(()=> (speechWebSocket.ReadyState == WebSocketState.Open));
  208.         yield return new WaitWhile(() => Microphone.GetPosition(micphoneName) <= 0);
  209.         float t = 0;
  210.         int position = Microphone.GetPosition(micphoneName);
  211.         const float waitTime = 0.04f;//每隔40ms发送音频
  212.         int lastPosition = 0;
  213.         const int Maxlength = 640;//最大发送长度
  214.         //Debug.Log("position:" + position + ",samples:" + RecordedClip.samples);
  215.         while (position < RecordedClip.samples && speechWebSocket.ReadyState == WebSocketState.Open)
  216.         {
  217.             t += waitTime;
  218.             yield return new WaitForSecondsRealtime(waitTime);
  219.             if (Microphone.IsRecording(micphoneName)) position = Microphone.GetPosition(micphoneName);
  220.             //Debug.Log("录音时长:" + t + "position=" + position + ",lastPosition=" + lastPosition);
  221.             if (position <= lastPosition)
  222.             {
  223.                 Debug.LogWarning("字节流发送完毕!强制结束!");
  224.                 break;
  225.             }
  226.             int length = position - lastPosition > Maxlength ? Maxlength : position - lastPosition;
  227.             byte[] date = GetClipData(lastPosition, length, RecordedClip);
  228.             SendData(date);
  229.             lastPosition = lastPosition + length;
  230.         }
  231.         yield return new WaitForSecondsRealtime(waitTime);
  232.         SendEndMsg(null);
  233.         Microphone.End(micphoneName);
  234.     }
  235.     public byte[] GetClipData(int star, int length, AudioClip recordedClip)
  236.     {
  237.         float[] soundata = new float[length];
  238.         recordedClip.GetData(soundata, star);
  239.         int rescaleFactor = 32767;
  240.         byte[] outData = new byte[soundata.Length * 2];
  241.         for (int i = 0; i < soundata.Length; i++)
  242.         {
  243.             short temshort = (short)(soundata[i] * rescaleFactor);
  244.             byte[] temdata = BitConverter.GetBytes(temshort);
  245.             outData[i * 2] = temdata[0];
  246.             outData[i * 2 + 1] = temdata[1];
  247.         }
  248.         return outData;
  249.     }
  250.     private string GetWebSocketUrl()
  251.     {
  252.         string appid = "appid";
  253.         string ts = GetCurrentUnixTimestampMillis().ToString();
  254.         string baseString = appid + ts;
  255.         string md5 = GetMD5Hash(baseString);
  256.         UnityEngine.Debug.Log("baseString:" + baseString + ",md5:" + md5);
  257.         string sha1 = CalculateHmacSha1(md5, "appkey");
  258.         string signa = sha1;
  259.         string url = string.Format("ws://rtasr.xfyun.cn/v1/ws?appid={0}&ts={1}&signa={2}", appid, ts, signa);
  260.         UnityEngine.Debug.Log(url);
  261.         return url;
  262.     }
  263.     private long GetCurrentUnixTimestampMillis()
  264.     {
  265.         DateTime unixStartTime = new DateTime(1970, 1, 1).ToLocalTime();
  266.         DateTime now = DateTime.Now;// DateTime.UtcNow;
  267.         TimeSpan timeSpan = now - unixStartTime;
  268.         long timestamp = (long)timeSpan.TotalSeconds;
  269.         return timestamp;
  270.     }
  271.     public string GetMD5Hash(string input)
  272.     {
  273.         MD5 md5Hasher = MD5.Create();
  274.         byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(input));
  275.         StringBuilder sBuilder = new StringBuilder();
  276.         for (int i = 0; i < data.Length; i++)
  277.         {
  278.             sBuilder.Append(data[i].ToString("x2"));
  279.         }
  280.         return sBuilder.ToString();
  281.     }
  282.     public string CalculateHmacSha1(string data, string key)
  283.     {
  284.         HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key));
  285.         byte[] hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
  286.         return Convert.ToBase64String(hashBytes);
  287.     }
  288.     private void Update()
  289.     {
  290.         if(messageQueue.Count > 0)
  291.         {
  292.             MainThreadOnMessage(messageQueue.Dequeue());
  293.         }
  294.     }
  295. }
复制代码
Json剖析类。
  1. [Serializable]
  2. public struct XFResponse
  3. {
  4.     public string action;
  5.     public int code;
  6.     public string data;
  7.     public string desc;
  8.     public string sid;
  9. }
  10. [Serializable]
  11. public struct SpeechRecognitionResult
  12. {
  13.     public string Text;        
  14.     public bool IsFinal;        
  15. }
复制代码
值得注意的问题。
1、Microphone使用时传默认设备名比传null好使
2、握手阶段时间戳用的是秒(不是毫秒)
3、上传竣事标志时,也要间隔40ms,否则讯飞像是充公到一样
4、假如Microphone.devices的长度为0,电脑确实又有麦克风设备,那么大概是麦克风的名字是中文导致的
遗留问题:
yield return new WaitForSecondsRealtime(0.04f)现实间隔时间0.1s左右,导致消息发送得很慢,语音辨认慢。
2024.5.24更新第二篇,有效解决消息发送慢,辨认慢的问题
2024.6.19更新:取消协程中发送数据,直接在Update中发送。解决消息发送很慢问题
  1.     private void Update()
  2.     {
  3.         if (isRunning)
  4.         {
  5.             byte[] voiceData = GetVoiveData();
  6.             if (voiceData != null)
  7.             {
  8.                 SendData(voiceData);
  9.             }
  10.         }
  11.         if (messageQueue.Count > 0)
  12.         {
  13.             MainThreadOnMessage(messageQueue.Dequeue());
  14.         }
  15.     }
  16. private int last_length = -1;
  17.     private float[] volumeData = new float[9999];
  18.     private short[] intData = new short[9999];
  19.     bool isRunning;
  20.     private byte[] GetVoiveData()
  21.     {
  22.         if (RecordedClip == null)
  23.         {
  24.             return null;
  25.         }
  26.         int new_length = Microphone.GetPosition(null);
  27.         if (new_length == last_length)
  28.         {
  29.             if (Microphone.devices.Length == 0)
  30.             {
  31.                 isRunning = false;
  32.             }
  33.             return null;
  34.         }
  35.         int length = new_length - last_length;
  36.         int offset = last_length + 1;
  37.         last_length = new_length;
  38.         if (offset < 0)
  39.         {
  40.             return null;
  41.         }
  42.         if (length < 0)
  43.         {
  44.             float[] temp = new float[RecordedClip.samples];
  45.             RecordedClip.GetData(temp, 0);
  46.             int lengthTail = RecordedClip.samples - offset;
  47.             int lengthHead = new_length + 1;
  48.             try
  49.             {
  50.                 Array.Copy(temp, offset, volumeData, 0, lengthTail);
  51.                 Array.Copy(temp, 0, volumeData, lengthTail + 1, lengthHead);
  52.                 length = lengthTail + lengthHead;
  53.             }
  54.             catch (Exception)
  55.             {
  56.                 return null;
  57.             }
  58.         }
  59.         else
  60.         {
  61.             if (length > volumeData.Length)
  62.             {
  63.                 volumeData = new float[length];
  64.                 intData = new short[length];
  65.             }
  66.             RecordedClip.GetData(volumeData, offset);
  67.         }
  68.         byte[] bytesData = new byte[length * 2];
  69.         int rescaleFactor = 32767; //to convert float to Int16
  70.         for (int i = 0; i < length; i++)
  71.         {
  72.             intData[i] = (short)(volumeData[i] * rescaleFactor);
  73.             byte[] byteArr = BitConverter.GetBytes(intData[i]);
  74.             byteArr.CopyTo(bytesData, i * 2);
  75.         }
  76.         return bytesData;
  77.     }
复制代码


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4