目录

Unity 讯飞实时语音转写(一)—— 使用WebSocket连接讯飞语音服务器
Unity 讯飞实时语音转写(二)—— 接收转写结果
Unity 讯飞实时语音转写(三)—— 分析转写结果

正文

一、官网示例
在官方文档中,有一段说了转写结果的json字符串大概长什么样子

{"action":"result","code":"0","data":"{\"cn\":{\"st\":{\"bg\":\"820\",\"ed\":\"0\",\"rt\":[{\"ws\":[{\"cw\":[{\"w\":\"啊\",\"wp\":\"n\"}],\"wb\":0,\"we\":0},{\"cw\":[{\"w\":\"喂\",\"wp\":\"n\"}],\"wb\":0,\"we\":0},{\"cw\":[{\"w\":\"!\",\"wp\":\"p\"}],\"wb\":0,\"we\":0},{\"cw\":[{\"w\":\"你好\",\"wp\":\"n\"}],\"wb\":0,\"we\":0},{\"cw\":[{\"w\":\"!\",\"wp\":\"p\"}],\"wb\":0,\"we\":0},{\"cw\":[{\"w\":\"我\",\"wp\":\"n\"}],\"wb\":0,\"we\":0},{\"cw\":[{\"w\":\"是\",\"wp\":\"n\"}],\"wb\":0,\"we\":0},{\"cw\":[{\"w\":\"上\",\"wp\":\"n\"}],\"wb\":0,\"we\":0}]}],\"type\":\"1\"}},\"seg_id\":5}\n","desc":"success","sid":"rta0000000e@ch312c0e3f6bcc9f0900"}

经过测试,我自己转写的结果

接收消息:{"action":"result",
"code":"0",
"data":"{\"seg_id\":7,\"cn\":{\"st\":{\"rt\":[{\"ws\":[{\"cw\":[{\"w\":\"我们\",\"wp\":\"n\"}],\"wb\":23,\"we\":70},{\"cw\":[{\"w\":\"生活\",\"wp\":\"n\"}],\"wb\":71,\"we\":118},{\"cw\":[{\"w\":\"的\",\"wp\":\"n\"}],\"wb\":119,\"we\":130},{\"cw\":[{\"w\":\"世界\",\"wp\":\"n\"}],\"wb\":131,\"we\":172},{\"cw\":[{\"w\":\"里\",\"wp\":\"n\"}],\"wb\":173,\"we\":201},{\"cw\":[{\"w\":\"有\",\"wp\":\"n\"}],\"wb\":202,\"we\":226},{\"cw\":[{\"w\":\"两\",\"wp\":\"n\"}],\"wb\":227,\"we\":249},{\"cw\":[{\"w\":\"个\",\"wp\":\"n\"}],\"wb\":250,\"we\":263},{\"cw\":[{\"w\":\"世界\",\"wp\":\"n\"}],\"wb\":264,\"we\":320}]}],\"bg\":\"5120\",\"type\":\"0\",\"ed\":\"8520\"}},\"ls\":false
}",
"desc":"success",
"sid":"xxxxxxxxxxxxxxxxxxxxxxxxxx"
}

发现实际转写结果有一些细微差别,例如:增加了一个字段“ls”,尚不知道是干嘛的。不过,既然接收到了该字段,就说明以后可能会用到,先在代码中添加上就好了。

二、关键数据结构

/// <summary>
/// 返回值结果格式为json
/// </summary>
[Serializable]
public struct JsonData
{/// <summary>/// 结果标识,started:握手,result:结果,error:异常/// </summary>public string action;/// <summary>/// 结果码(具体见错误码)/// </summary>public string code;/// <summary>/// 转写结果数据/// </summary>public Data data;/// <summary>/// 描述/// </summary>public string desc;/// <summary>/// 会话ID/// 主要用于DEBUG追查问题,如果出现问题,可以提供sid帮助确认问题。/// </summary>public string sid;
}

结果格式为json,字段说明如下:

参数 类型 说明
action string 结果标识,started:握手,result:结果,error:异常
code string 结果码(具体见错误码)
data string 转写结果
desc string 描述
sid string 会话ID
/// <summary>
/// 语音识别的结果
/// </summary>
[Serializable]
public struct Data
{/// <summary>/// 转写结果序号  从0开始/// </summary>public string seg_id;[Serializable]public struct CN{[Serializable]public struct ST{[Serializable]public struct RT{[Serializable]public class WS{[Serializable]public class CW{/// <summary>/// 词识别结果/// </summary>public string w;/// <summary>/// 词标识  n-普通词;s-顺滑词(语气词);p-标点/// </summary>public string wp;}public CW[] cw;/// <summary>/// 词在本句中的开始时间,单位是帧,1帧=10ms  即词在整段语音中的开始时间为(bg+wb*10)ms/// 中间结果的 wb 为 0/// </summary>public string wb;/// <summary>/// 词在本句中的结束时间,单位是帧,1帧=10ms  即词在整段语音中的结束时间为(bg+we*10)ms/// 中间结果的 we 为 0/// </summary>public string we;}public WS[] ws;}public RT rt;/// <summary>/// 句子在整段语音中的开始时间,单位毫秒(ms)/// 中间结果的bg为准确值/// </summary>public string bg;/// <summary>/// 结果类型标识   0-最终结果;1-中间结果/// </summary>public string type;/// <summary>/// 句子在整段语音中的结束时间,单位毫秒(ms)/// 中间结果的ed为0/// </summary>public string ed;}public ST st;}public CN cn;/// <summary>/// /// </summary>public string ls;
}

转写结果data字段说明如下:

字段 含义 描述
seg_id 转写结果序号 从0开始
w 词识别结果
wp 词标识 n-普通词;s-顺滑词(语气词);p-标点
wb 词在本句中的开始时间,单位是帧,1帧=10ms 即词在整段语音中的开始时间为(bg+wb*10)ms 中间结果的 wb 为 0
we 词在本句中的结束时间,单位是帧,1帧=10ms 即词在整段语音中的结束时间为(bg+we*10)ms 中间结果的 we 为 0
bg 句子在整段语音中的开始时间,单位毫秒(ms) 中间结果的bg为准确值
type 结果类型标识 0-最终结果;1-中间结果
ed 句子在整段语音中的结束时间,单位毫秒(ms) 中间结果的ed为0
ls 官网未说明 官网未说明

三、该篇全部代码

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Newtonsoft.Json;/// <summary>
/// 返回值结果格式为json
/// </summary>
[Serializable]
public struct JsonData
{/// <summary>/// 结果标识,started:握手,result:结果,error:异常/// </summary>public string action;/// <summary>/// 结果码(具体见错误码)/// </summary>public string code;/// <summary>/// 转写结果数据/// </summary>public Data data;/// <summary>/// 描述/// </summary>public string desc;/// <summary>/// 会话ID/// 主要用于DEBUG追查问题,如果出现问题,可以提供sid帮助确认问题。/// </summary>public string sid;
}/// <summary>
/// 语音识别的结果
/// </summary>
[Serializable]
public struct Data
{/// <summary>/// 转写结果序号  从0开始/// </summary>public string seg_id;[Serializable]public struct CN{[Serializable]public struct ST{[Serializable]public struct RT{[Serializable]public class WS{[Serializable]public class CW{/// <summary>/// 词识别结果/// </summary>public string w;/// <summary>/// 词标识  n-普通词;s-顺滑词(语气词);p-标点/// </summary>public string wp;}public CW[] cw;/// <summary>/// 词在本句中的开始时间,单位是帧,1帧=10ms  即词在整段语音中的开始时间为(bg+wb*10)ms/// 中间结果的 wb 为 0/// </summary>public string wb;/// <summary>/// 词在本句中的结束时间,单位是帧,1帧=10ms  即词在整段语音中的结束时间为(bg+we*10)ms/// 中间结果的 we 为 0/// </summary>public string we;}public WS[] ws;}public RT rt;/// <summary>/// 句子在整段语音中的开始时间,单位毫秒(ms)/// 中间结果的bg为准确值/// </summary>public string bg;/// <summary>/// 结果类型标识   0-最终结果;1-中间结果/// </summary>public string type;/// <summary>/// 句子在整段语音中的结束时间,单位毫秒(ms)/// 中间结果的ed为0/// </summary>public string ed;}public ST st;}public CN cn;/// <summary>/// /// </summary>public string ls;
}public class VisualCommunication : MonoBehaviour
{private string appid = "xxxxx";private string appkey = "xxxxxxxxxxxxxxxxxxxxxxxxx";private string timeStamp;private string baseString;private string toMd5;private string signa;public AudioClip RecordedClip;ClientWebSocket ws;CancellationToken ct;private int MAX_RECORD_LENGTH = 3599;/// <summary>/// 语音识别回调事件/// </summary>public event Action<string> asrCallback;void Start(){asrCallback += Output;//Debug.Log("\nappid: " + appid);//Debug.Log("\nappkey: " + appkey);//Debug.Log("\ntimeStamp: " + timeStamp);//Debug.Log("\nbaseString: " + baseString);//Debug.Log("\nToMD5: " + toMd5);//Debug.Log("\nsigna: " + signa);}void Output(string str){Debug.Log("语音识别结果:" + str);}public void StartASR(){if (ws != null && ws.State == WebSocketState.Open){Debug.LogWarning("开始语音识别失败!,等待上次识别连接结束");return;}if (Microphone.devices.Length == 0){Debug.LogError("未检测到可用的麦克风");return;}ConnectASR_Aysnc();RecordedClip = Microphone.Start(null, false, MAX_RECORD_LENGTH, 16000);}public void StopASR(){if (ws != null){//关掉发送音频的协程StopCoroutine(SendAudioClip());//Debug.Log("发送结束标识" + ws.State);//音频数据上传完成后,客户端需发送一个 {"end": true} 到服务端作为结束标识ws.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes("{\"end\": true}")), WebSocketMessageType.Binary,true, new CancellationToken());//ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "关闭WebSocket连接", new CancellationToken());Microphone.End(null);StartCoroutine(StopRecord());}}private IEnumerator StopRecord(){yield return new WaitUntil(() => ws.State != WebSocketState.Open);Debug.Log("识别结束,停止录音");}async void ConnectASR_Aysnc(){ws = new ClientWebSocket();ct = new CancellationToken();Uri url = GetUri();await ws.ConnectAsync(url, ct);StartCoroutine(SendAudioClip());StringBuilder stringBuilder = new StringBuilder();while (ws.State == WebSocketState.Open){var result = new byte[4096];await ws.ReceiveAsync(new ArraySegment<byte>(result), ct); //接受数据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());Debug.Log("接收消息:" + str);if (string.IsNullOrEmpty(str)){return;}JsonData jsonData = JsonUtility.FromJson<JsonData>(str);if (jsonData.action.Equals("started")){Debug.Log("握手成功!");}else if (jsonData.action.Equals("result")){stringBuilder.Append(AnalysisResult(jsonData));}else if (jsonData.action.Equals("error")){Debug.Log("Error: " + jsonData.desc);ws.Abort();}}Debug.LogWarning("断开连接");string s = stringBuilder.ToString();if (!string.IsNullOrEmpty(s)){asrCallback?.Invoke(s);Debug.LogWarning("识别到声音:" + s);}}/// <summary>/// 发送音频片段/// </summary>/// <param name="socket"></param>/// <returns></returns>IEnumerator SendAudioClip(){yield return new WaitWhile(() => Microphone.GetPosition(null) <= 0);float t = 0;int position = Microphone.GetPosition(null);const float waitTime = 0.04f; //每隔40ms发送音频const int Maxlength = 1280; //最多发送1280字节int status = 0;int lastPosition = 0;while (position < RecordedClip.samples && ws.State == WebSocketState.Open){t += waitTime;if (t >= MAX_RECORD_LENGTH){Debug.Log("录音时长已用尽,结束语音识别!");break;}yield return new WaitForSecondsRealtime(waitTime);if (Microphone.IsRecording(null)){position = Microphone.GetPosition(null);//Debug.Log("录音时长:" + t + "position=" + position + ",lastPosition=" + lastPosition);}if (position <= lastPosition){// 防止出现当前采样位置和上一帧采样位置一样,导致length为0// 那么在调用AudioClip.GetData(float[] data, int offsetSamples);时,将报错continue;}int length = position - lastPosition > Maxlength ? Maxlength : position - lastPosition;byte[] data = GetAudioClip(lastPosition, length, RecordedClip);ws.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Binary, true,new CancellationToken()); //发送数据lastPosition = lastPosition + length;status = 1;}}private void OnApplicationQuit(){StopASR();}/// <summary>/// 获取识别并返回字符串/// </summary>/// <param name="data">所获取的识别的Json字符串</param>/// <returns>所识别的连贯的一句话</returns>string AnalysisResult(JsonData jsonData){StringBuilder stringBuilder = new StringBuilder();var ws = jsonData.data.cn.st.rt.ws;if (ws != null){foreach (var item in ws){var cw = item.cw;foreach (var w in cw){stringBuilder.Append(w.w);}}return stringBuilder.ToString();}return "";}/// <summary>/// 获取音频流片段/// </summary>/// <param name="start">起始采样点</param>/// <param name="length">采样长度</param>/// <param name="recordedClip">音频</param>/// <returns></returns>public static byte[] GetAudioClip(int start, int length, AudioClip recordedClip){float[] soundata = new float[length];recordedClip.GetData(soundata, start);int rescaleFactor = 32767;byte[] outData = new byte[soundata.Length * 2];for (int i = 0; i < soundata.Length; i++){short temshort = (short) (soundata[i] * rescaleFactor);byte[] temdata = BitConverter.GetBytes(temshort);outData[i * 2] = temdata[0];outData[i * 2 + 1] = temdata[1];}return outData;}/// <summary>/// 获得请求URI/// </summary>/// <returns>请求的URI</returns>private Uri GetUri(){//精确到秒timeStamp = GetTimeStamp();//baseString由appid和当前时间戳ts拼接而成baseString = appid + timeStamp;//对baseString进行MD5toMd5 = ToMD5(baseString);//以apiKey为key对MD5之后的baseString进行HmacSHA1加密//然后再对加密后的字符串进行base64编码signa = ToHmacSHA1(toMd5, appkey);string requestUrl = string.Format("wss://rtasr.xfyun.cn/v1/ws?appid={0}&ts={1}&signa={2}&pd=tech", appid,timeStamp, UrlEncode(signa));Debug.Log("requestUrl: " + requestUrl);return new Uri(requestUrl);}/// <summary>/// 对字符串进行UrlEncode转码/// </summary>/// <param name="str">需要转码的字符串</param>/// <returns>经过UrlEncode转码的字符串</returns>public static string UrlEncode(string str){StringBuilder sb = new StringBuilder();byte[] byStr = System.Text.Encoding.UTF8.GetBytes(str); //默认是System.Text.Encoding.Default.GetBytes(str)for (int i = 0; i < byStr.Length; i++){sb.Append(@"%" + Convert.ToString(byStr[i], 16));}return (sb.ToString());}/// <summary>/// 获取时间戳/// </summary>/// <returns>时间戳,精确到秒</returns>public static string GetTimeStamp(){TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);return Convert.ToInt64(ts.TotalSeconds).ToString();}/// <summary>/// MD5字符串加密/// </summary>/// <param name="txt">需要加密的字符串</param>/// <returns>加密后字符串</returns>public static string ToMD5(string txt){using (MD5 mi = MD5.Create()){byte[] buffer = Encoding.Default.GetBytes(txt);//开始加密byte[] newBuffer = mi.ComputeHash(buffer);StringBuilder sb = new StringBuilder();for (int i = 0; i < newBuffer.Length; i++){sb.Append(newBuffer[i].ToString("x2"));}return sb.ToString();}}/// <summary>/// HMACSHA1算法加密并返回ToBase64String/// </summary>/// <param name="text">要加密的原串</param>///<param name="key">私钥</param>/// <returns>返回一个签名值(即哈希值)</returns>public static string ToHmacSHA1(string text, string key){//HMACSHA1加密HMACSHA1 hmacsha1 = new HMACSHA1();hmacsha1.Key = System.Text.Encoding.UTF8.GetBytes(key);byte[] dataBuffer = System.Text.Encoding.UTF8.GetBytes(text);byte[] hashBytes = hmacsha1.ComputeHash(dataBuffer);return Convert.ToBase64String(hashBytes);}
}

四、如何测试

  1. 正确填写讯飞控制台上自己创建的应用所对应的appid、appkey
  2. 将该脚本VisualCommunication.cs挂在场景任意对象上,
  3. 确保你的麦克风可用

五、说明
在调用StopASR()方法后,223行会报一个错误“WebSocketException: The remote party closed the WebSocket connection without completing the close handshake.”,尚未解决

六、该篇参考资料
官方文档:https://www.xfyun.cn/doc/asr/rtasr/API.html
代码实现:https://blog.csdn.net/chunyu90225/article/details/106172895

转载注明出处!

Unity 讯飞实时语音转写(二)—— 接收转写结果相关推荐

  1. 讯飞实时语音转写 python3.6.1 可完美运行 解析返回的json字符串 输出所获语音文字

    百度语音识别对录音要求较高(可能是我的问题,sdk和在线api都试过了(滑稽保命)),失败后选择讯飞语音,官方提供的文档是python2版本的 ,经过修改后可在python3中运行 ,解析返回的jso ...

  2. 讯飞智能语音鼠标G50:AI语音、转写翻译、记录截图一键搞定!

    随着互联网的发展,智能鼠标已经成为我们生活和工作中不可或缺的组成部分.然而,鼠标滚轮异响.按键失灵.驱动难用.手感不合适等一系列问题仍时有发生,所以选择一款智能鼠标尤为重要,它不仅可以提高我们的工作效 ...

  3. 讯飞离线语音命令词识别

    讯飞离线语音命令词识别 强烈推荐 分享一个大神的人工智能教程.零基础!通俗易懂!风趣幽默!希望你也加入到人工智能的队伍中来! 网址:http://www.captainbed.net/yancyang ...

  4. Android 讯飞离线语音听写/离线语音识别SDK

    平台 Android + 讯飞离线语音SDK SDK包 下载路径及方法见讯飞官方SDK文档: 离线语音听写 Android SDK 文档 # 在开发者控制台, 可以直接下载SDK. SDK包中的文件结 ...

  5. Unity接讯飞在线语音API

    本文意在讲解如何利用讯飞官方提供的API通过讯飞服务实时的进行文字转语音. 先决条件:需要在讯飞官网注册自己的账号,拿到讯飞给的APPID.APISecret.APIKey,这三个字段是访问讯飞服务器 ...

  6. asr语音转写_python 腾讯/百度/讯飞 ASR 语音转文字

    因为项目中有需要把微信里的语音转成文本处理, 本次只说语音转文本. 需要注意的是平台对语音的格式有要求, 所以我们需要对语音进行转换格式. 语音转换 使用的工具是ffmpeg, ffmpeg的安装和配 ...

  7. 讯飞智能语音先锋者:等到人机交互与人类交流一样自然时,真正的智能时代就来了...

    作者 | 夕颜 出品 | CSDN(ID:CSDNnews) 「AI 技术生态论」 人物访谈栏目是 CSDN 发起的百万人学 AI 倡议下的重要组成部分.通过对 AI 生态顶级大咖.创业者.行业 KO ...

  8. 讯飞智能语音先锋者:等到人机交互与人类交流一样自然时,真正的智能时代就来了!...

    受访者 | 刘聪 记者 | 夕颜 出品 | CSDN(ID:CSDNnews) 「AI 技术生态论」 人物访谈栏目是 CSDN 发起的百万人学 AI 倡议下的重要组成部分.通过对 AI 生态顶级大咖. ...

  9. springboot 讯飞语音_讯飞智能语音鼠标实际体验感受

    这几天入手一个科大讯飞智能语音鼠标,我的实际体验感受就是"用嘴打字的体会,你体会不了!简称"嘴强王者"" 我拿到手的是lite版本,讯飞一共出了三款智能鼠标,分 ...

最新文章

  1. 一些有用的webservice
  2. Java中集合类型线程安全性
  3. 未找到要求的 from 关键字_性能优化|这恐怕是解释Explain关键字最全的一篇文章
  4. Android开发之Android studio4.1查看app布局的方法 | 使用布局检查器和布局验证工具调试布局
  5. .NET CoreCLR开发人员指南(上)
  6. SQL Server 开发指南(经典教程)
  7. matlab 平滑曲线连接_【仪光学习】技能分享 | 前方高能:如何用Matlab轻松实现数学建模...
  8. 组策略里更改更新和设置客户端首页
  9. jquery获取radio值
  10. 李宏毅机器学习——逻辑回归
  11. Cannot complete this action,please try again. Correlation ID :bd640a9d-4c19-doff-2fe0-6ce1104b59ae
  12. oracle在线视频教程,Oracle性能优化视频教程 - Oracle - 数据库 - 私塾在线 - 只做精品视频课程服务...
  13. python程序设计 清华大学出版社 pdf下载-清华大学出版社-图书详情-《Python程序设计教程》...
  14. 夏普PC_1500计算机使用,夏普PC-1500袖珍计算机的检修(续)
  15. Android压缩Apk
  16. C#实现等差与等比数列求和
  17. OLED屏幕花屏的原因(I2C+DMA)
  18. 矩阵相乘(Python)
  19. Greenplum 6安装指南(CentOS 7.X)
  20. 教师工资管理系统之随机产生教师详细信息

热门文章

  1. cad直线和圆弧倒角不相切_CAD圆角、倒角分不清?详细讲解CAD圆角与倒角对象的区别和技巧...
  2. c语言最近点对问题(4个点)-分治法递归
  3. springboot中的各个模块及其功能
  4. 关于sungard和博彦的笔试
  5. 相亲APP开发功能及解决方案
  6. 实时风控引擎项目部署
  7. NeuroImage:磁共振3D梯度回波磁化转移序列同时对铁和神经黑色素进行成像—用于帕金森病生物标志物研究
  8. 骨传导加动圈,这款Dacom耳机有何出众之处?开箱验证
  9. C++ 获取个位数十位数等
  10. 安创安全OA——小程序