Unity+iFlytek Voice+iFlytek Spark+Motionverse는 지능형 디지털 휴먼을 만듭니다.
더 이상 고민하지 말고 효과 영상부터 시작하겠습니다
유니티+아이플라이텍 음성인식+아이플라이텍 스파크 대형모델+모션버
그럼 원리를 얘기해봐
이를 달성하려면 주로 다음 세 가지 모듈의 액세스를 구현해야 합니다.
- 음성 인식. 마이크의 음성을 gpt 대형 모델이 인식할 수 있는 텍스트로 변환하는 기능입니다. 그런 다음 gpt로 보내십시오.
- GPT 대형 모델. 물론 1단계에서 생성된 질문 텍스트를 답변 텍스트로 변환하는 기능입니다.
- 디지털 사람들이 주도합니다. 이 기능은 텍스트 콘텐츠를 사용하여 디지털 휴먼의 움직임을 구동하고 방송용 음성을 생성합니다.
구체적인 접근 단계, 직면하게 되는 함정, 해결책에 대해 이야기해 보겠습니다.
1. iFlytek 보이스에 접속하세요
이는 세 모듈 중 가장 원활한 액세스입니다.
활성화 방법은 매우 간단합니다.iFlytek 오픈 플랫폼에 가서 계정을 등록하고, 애플리케이션을 만들고, 뉴비 혜택을 받으면 50,000개 이상의 서비스 볼륨을 얻을 수 있습니다. 이것은 확실히 개발 및 테스트에 충분합니다. 특정 방법은 기술적 문제와 관련이 없으므로 여기서는 반복하지 않습니다.
여기에는 두 가지 문제가 관련되어 있습니다.
- 자동으로 녹음하는 방법. 내 방법은 마이크가 실제로 항상 녹음하고 있지만 일정 기간 동안 마이크의 평균 볼륨을 모니터링하여 임계값에 도달하면 이 시점을 기록합니다(완전히 녹음하려면 약간의 시간을 뺍니다). , 그리고 평균 볼륨이 특정 임계값보다 낮고 일정 시간 동안 낮은 볼륨을 유지하는 경우 이 시점을 종료 지점으로 기록하고 시작 지점과 종료 지점에서 녹음을 차단하여 음성이 남고 유한 자동 상태 기계가 사용됩니다. 위의 논리를 구현하려면 명확하고 간결합니다.
알고리즘 코드는 다음과 같습니다.
public event OnAudioToTextConvertedHandler OnAudioToTextConverted; // 语音转为文字成功事件
public event OnRecordStartedHandler OnRecordStarted; // 开始录制事件
public event OnRecordStopedHandler OnRecordStoped; // 停止录制事件
private enum State // 状态机三种状态
{
Listening, // 监听状态,没有录音
Recording, // 正在录音
PreStop // 准备停止录音
}
// 初始状态处于监听状态
private State state = State.Listening;
private void Update()
{
volume = GetVolume(); // 获取平均音量
switch (state)
{
case State.Listening: // 处于监听状态时,音量大于阈值,就开始录音
if (volume > BeginRecordThreshold)
{
state = State.Recording;
startPos = Microphone.GetPosition(null) - 2000;
// OnStartRecord;
OnRecordStarted?.Invoke();
}
break;
case State.Recording: // 处于录音状态时,音量小于阈值,就准备停止
if (volume < StopRecordThreshold)
{
state = State.PreStop;
waitTime = 0;
}
break;
case State.PreStop: // 处于准备停止状态时,超过时间,就真的停止
if (volume < StopRecordThreshold)
{
if (waitTime > StopWaitTime)
{
var end = Microphone.GetPosition(null);
XunFeiMSC.AudioToText( clip.ToBytes(startPos, end ));
state = State.Listening;
// OnStopRecord;
OnRecordStoped?.Invoke();
}
else
waitTime += Time.deltaTime;
}
else
state = State.Recording;
break;
}
}
private float GetVolume()
{
if (Microphone.IsRecording(null))
{
var offset = Microphone.GetPosition(null) - ( TestDataLength + 1 );
if (offset < 0)
return 0;
clip.GetData(datas, offset);
float av = datas.Sum() / datas.Length;
return Mathf.Max(0, av);
}
return 0;
}
- C#을 iFlytek Voice SDK에 연결하는 방법
C/C++만 지원하기 때문에 실제로 친숙하지 않은 SDK입니다. Unity에서 사용하려면 그 사람이 공부해야 합니다. 먼저 정확히 무엇을 사용하고 있는지 공부해야 합니다. 그런 다음 헤더 파일을 확인하여 해당 정의를 확인하고 DLL에서 가져옵니다. 둘째, C/C++ DLL에서 import하는 것은 매개변수가 많아 쉽지 않고, 이를 C#으로 변환해야 하는데, 이 때 타입 대응 문제가 발생하는데, 선택의 여지가 없다면 GTP4나 구글 검색에 문의하세요. . 그러나 나는 내가 편집한 내용을 여기에 제공할 것입니다 :
namespace HexuXunFeiMSC
{
public static class XunFeiMSC
{
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int MSPLogin(string usr, string pwd, string parameters);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int MSPLogout();
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr MSPUploadData(string dataName, IntPtr data, uint dataLen, string _params,
ref int errorCode);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int MSPAppendData(IntPtr data, uint dataLen, uint dataStatus);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr MSPDownloadData(string _params, ref uint dataLen, ref int errorCode);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int MSPSetParam(string paramName, string paramValue);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int MSPGetParam(string paramName, ref byte[] paramValue, ref uint valueLen);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr MSPGetVersion(string verName, ref int errorCode);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr QISRSessionBegin(string grammarList, string _params, ref int errorCode);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int QISRAudioWrite(IntPtr sessionID, byte[] waveData, uint waveLen,
AudioStatus audioStatus, ref EpStatus epStatus, ref RecogStatus recogStatus);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr QISRGetResult(IntPtr sessionID, ref RecogStatus rsltStatus, int waitTime,
ref int errorCode);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int QISRSessionEnd(IntPtr sessionID, string hints);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int QISRGetParam(string sessionID, string paramName, ref byte[] paramValue,
ref uint valueLen);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr QTTSSessionBegin(string _params, ref int errorCode);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int QTTSTextPut(IntPtr sessionID, string textString, uint textLen, string _params);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr QTTSAudioGet(IntPtr sessionID, ref uint audioLen, ref SynthStatus synthStatus,
ref int errorCode);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr QTTSAudioInfo(IntPtr sessionID);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int QTTSSessionEnd(IntPtr sessionID, string hints);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int QTTSSetParam(IntPtr sessionID, string paramName, byte[] paramValue);
[DllImport("msc_x64", CallingConvention = CallingConvention.StdCall)]
public static extern int QTTSGetParam(IntPtr sessionID, string paramName, ref byte[] paramValue,
ref uint valueLen);
}
}
그런 다음 녹음을 보내고 변환된 텍스트를 가져오기만 하면 됩니다.
// 这里是多线程版本,这个函数将在非主线程运行
private static void RealQueryAudioToText(object obj)
{
try
{
byte[] data = (byte[])obj;
// 首先登录讯飞平台
int res = MSPLogin(null, null, appId);
if (res != 0)
throw new Exception($"Can't Login {
res}");
// 获取会话ID
IntPtr sessionID = QISRSessionBegin(null, sessionBeginParams, ref res);
if (res != 0)
throw new Exception($"SessionBegin Error {
res}");
EpStatus epStatus = EpStatus.MSP_EP_LOOKING_FOR_SPEECH;
RecogStatus recognizeStatus = RecogStatus.MSP_REC_STATUS_SUCCESS;
// 发送语音数据,并标明这是最后一段(表示这句话完整了,后续没有别的录音了)
res = QISRAudioWrite(sessionID, data, (uint)data.Length, AudioStatus.MSP_AUDIO_SAMPLE_LAST, ref epStatus, ref recognizeStatus);
if (res != 0)
throw new Exception($"Write failed {
res}");
StringBuilder sb = new StringBuilder();
// 获取结果,直到结束
while (recognizeStatus != RecogStatus.MSP_REC_STATUS_COMPLETE)
{
IntPtr curtRslt = QISRGetResult(sessionID, ref recognizeStatus, 0, ref res);
if (res != 0)
throw new Exception($"get result failed. error code: {
res}");
sb.Append(Marshal.PtrToStringUTF8(curtRslt));
}
// 会话结束
res = QISRSessionEnd(sessionID, "Finish");
if (res != 0)
throw new Exception($"end failed. error code: {
res}");
// 退出登录
res = MSPLogout();
if (res != 0)
throw new Exception($"logout failed. error code {
res}");
// 结果回调
OnAudioToText?.Invoke(sb.ToString());
}
catch (Exception e)
{
OnError?.Invoke(e.Message);
}
}
2. iFlytek Spark 모델에 액세스
이건 iFlytek 콘솔에서 미리 적용해 줘야 하는데 통과는 쉽지만 하루 정도 기다려야 합니다. 합격 후 그는 500,000개의 토큰을 제공합니다. 토큰은 "단어"와 유사합니다. 평균적으로 한 문장은 기본적으로 수십에서 수백 개의 토큰을 소비합니다. 테스트에는 50W이면 충분합니다.
여기에는 많은 함정이 있습니다.
원래는 제공되는 SDK를 계속 사용하고 싶었으나 언뜻 보기에는 C/C++밖에 없었고, 음성인식 시 C#에 DLL을 도입하는 수고로움을 생각하면 조금 흔들렸지만 웹접근도 제공해주기 때문에 저는 Web을 선택하기로 결정했는데, 예상외로 Web에도 많은 문제가 생겼습니다.
저에게 주신 Python 사례를 보고 제가 이해한대로 C#으로 다시 작성해서 자신감있게 테스트했는데, 의외로 어떻게 테스트를 하여도 실패했습니다. 먼저 UnityWebRequest를 사용해 요청했는데 실패했고, UnityWebRequest가 WSS 프로토콜을 지원하지 않는 줄 알고 HttpWebRequest로 변경했는데도 실패했고, 마지막으로 WebSocket으로 요청 로직을 다시 작성했는데 여전히 실패했습니다. . 젠장, 요청을 보내기 전에 진정하고 코드를 봐야 했고, 마침내 인증 URL을 생성할 때 C#에서 생성된 URL이 Python 사례에서 생성된 URL보다 몇 바이트 적다는 것을 발견했습니다. 이유를 찾지 못했습니다. 매우 이상합니다. 수수께끼입니다. 주의 깊게 비교한 결과 누락된 몇 바이트가 AI==의 4개 문자가 모두 고정되어 있음을 확인했습니다. 그냥 적어서 요청했는데 의외로 해결됐네요. UnityWebRequest 및 HttpWebRequest가 이러한 이유로 발생하는지 여부에 대해서는 더 이상 다시 테스트하고 싶지 않습니다. 코드는 아래와 같습니다:
// 生成鉴权URL
private static string BuildURL()
{
Uri uri = new Uri(gptUrl);
string host = uri.Host;
string date = DateTime.UtcNow.ToString("R");
var auth = $"host: {
host}\ndate: {
date}\nGET {
uri.PathAndQuery} HTTP/1.1";
var sha256 = HmacSha256(auth, apiSecret);
var signature = Convert.ToBase64String(sha256);
string authorizationOrigin =
$"api_key=\"{
apiKey}\", algorithm=\"hmac-sha256\", headers=\"host date request-line\", signature=\"{
signature}\"";
string authorization = Convert.ToBase64String(Encoding.UTF8.GetBytes(authorizationOrigin));
return gptUrl + "?authorization=" + authorization + UnityWebRequest.EscapeURL("IA==") + "&date=" +
UnityWebRequest.EscapeURL(date) + "&host=" + UnityWebRequest.EscapeURL(host);
}
// 发起问题请求,这也是多线程版本,此函数会在非主线程中调用
private static void RealRequestQuestion(object obj)
{
try
{
// 问新问题之前,把前面的问题也发过去,这样它才能联系上下文,它限制8Ktoken,所以只传前面20个问题
if (contentTextList.Count > 20)
contentTextList.RemoveAt(0);
contentTextList.Add(new QueryText() {
role = "user", content = (string)obj });
// 生成鉴权URL,生成问题数据
string url = BuildURL();
string dataString = JsonConvert.SerializeObject(
new
{
header = new {
app_id = appID },
parameter = new {
chat = new {
domain = "general" } },
payload = new
{
message = new
{
text = contentTextList.ToArray()
}
}
});
// 发起请求,写入问题,获取结果
using ClientWebSocket webSocket = new ClientWebSocket();
webSocket.Options.Proxy = null;
webSocket.ConnectAsync(new Uri(url), CancellationToken.None).Wait();
ArraySegment<byte> buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(dataString));
webSocket.SendAsync(buffer, WebSocketMessageType.Binary, true, CancellationToken.None).Wait();
StringBuilder sbResult = new StringBuilder();
byte[] receiveBuffer = new byte [2048];
while (true)
{
WebSocketReceiveResult result = webSocket.ReceiveAsync(receiveBuffer, CancellationToken.None)
.GetAwaiter().GetResult();
string text = Encoding.UTF8.GetString(receiveBuffer, 0, result.Count);
if (string.IsNullOrEmpty(text))
{
break;
}
JObject res = JsonConvert.DeserializeObject<JObject>(text);
if (res == null)
{
break;
}
int code = (int)res["header"]?["code"];
if (code != 0)
{
break;
}
JObject choices = (JObject)res["payload"]?["choices"];
if (choices == null)
break;
int status = (int)choices["status"];
string content = (string)choices["text"]?[0]?["content"];
if(!string.IsNullOrEmpty(content))
sbResult.Append(content);
// 如果status不是2,表示回答还没有完成。
if (status == 2)
{
string answer = sbResult.ToString();
contentTextList.Add(new QueryText()
{
role = "assistant", content = answer
});
OnResponeQuestion?.Invoke(answer);
break;
}
}
}
catch
{
// ignored
}
}
3. 모션버스 접속
이것이 전체의 가장 큰 함정이라고 할 수 있습니다. 제공하는 Unity 플러그인은 문제 없이 Unity에서 가져와서 실행할 수 있지만, 패키징하려는 경우 오류가 보고됩니다. 내 솔루션은 다음과 같습니다
// Packages\cn.deepscience.motionverse\Runtime\Interface\EngineInterface.cs
private const string DllName =
#if UNITY_EDITOR
"libMotionEngine";
#elif UNITY_IOS || UNITY_WEBGL
"__Internal";
#elif UNITY_ANDROID
"libMotionEngine";
#endif
// 改为:
private const string DllName = "libMotionEngine";
또한, 위 코드를 변경하더라도 Unity 2020 이상 버전에서는 패키징이 불가능합니다. 2020 버전에서만 패키징에 성공할 수 있습니다. 게다가 이 Motionverse 패키지를 부팅하고 가져올 때 많은 경고가 표시되며 일부 메서드에는 사용되지 않는 변수가 정의되어 있습니다. 젠장, 이런 낮은 수준의 문제도 있습니다. 따라서 모션버스의 코드 품질은 높지 않음을 알 수 있다.
그러나 사용하기가 가장 쉽습니다. 캐릭터와 뼈대가 바인딩되어 있는 한 전체 프로세스에서 하나의 함수만 호출하면 됩니다.
TextDrive.GetDrive(text);
요약하다
전반적으로 이 프로젝트에는 다음과 같이 개선해야 할 영역이 여전히 많이 있습니다.
- iFlytek Voice는 실시간 변환을 지원하므로 문장을 완성할 필요가 없습니다.대신 말하는 동안 계속 변환하고 동적으로 수정합니다.이런 방식으로 말한 후에 변환이 완료되므로 더욱 효율적입니다. , 하지만 해당 정보가 없습니다. 실시간 전환과 즉각적인 수정에 대해 더 자세히 알아보려면 더 나은 인내심이 필요합니다.
- Motionverse 드라이버의 효율성은 상대적으로 낮습니다. 매번 텍스트를 배경으로 전송해야 합니다. 작업이 완료된 후 음성 데이터와 모션 데이터를 생성한 다음 다시 전송합니다. 지연이 상대적으로 크고 모션이 운전자는 상대적으로 뻣뻣하고 유휴 애니메이션에서 말하기로의 전환은 액션이며 과도하지 않고 매우 불친절합니다.