Unity+iFlytek Voice+iFlytek Spark+Motionverse는 지능형 디지털 휴먼을 만듭니다.

Unity+iFlytek Voice+iFlytek Spark+Motionverse는 지능형 디지털 휴먼을 만듭니다.

더 이상 고민하지 말고 효과 영상부터 시작하겠습니다

유니티+아이플라이텍 음성인식+아이플라이텍 스파크 대형모델+모션버

그럼 원리를 얘기해봐

이를 달성하려면 주로 다음 세 가지 모듈의 액세스를 구현해야 합니다.

  1. 음성 인식. 마이크의 음성을 gpt 대형 모델이 인식할 수 있는 텍스트로 변환하는 기능입니다. 그런 다음 gpt로 보내십시오.
  2. GPT 대형 모델. 물론 1단계에서 생성된 질문 텍스트를 답변 텍스트로 변환하는 기능입니다.
  3. 디지털 사람들이 주도합니다. 이 기능은 텍스트 콘텐츠를 사용하여 디지털 휴먼의 움직임을 구동하고 방송용 음성을 생성합니다.

구체적인 접근 단계, 직면하게 되는 함정, 해결책에 대해 이야기해 보겠습니다.

1. iFlytek 보이스에 접속하세요

이는 세 모듈 중 가장 원활한 액세스입니다.
활성화 방법은 매우 간단합니다.iFlytek 오픈 플랫폼에 가서 계정을 등록하고, 애플리케이션을 만들고, 뉴비 혜택을 받으면 50,000개 이상의 서비스 볼륨을 얻을 수 있습니다. 이것은 확실히 개발 및 테스트에 충분합니다. 특정 방법은 기술적 문제와 관련이 없으므로 여기서는 반복하지 않습니다.
여기에는 두 가지 문제가 관련되어 있습니다.

  1. 자동으로 녹음하는 방법. 내 방법은 마이크가 실제로 항상 녹음하고 있지만 일정 기간 동안 마이크의 평균 볼륨을 모니터링하여 임계값에 도달하면 이 시점을 기록합니다(완전히 녹음하려면 약간의 시간을 뺍니다). , 그리고 평균 볼륨이 특정 임계값보다 낮고 일정 시간 동안 낮은 볼륨을 유지하는 경우 이 시점을 종료 지점으로 기록하고 시작 지점과 종료 지점에서 녹음을 차단하여 음성이 남고 유한 자동 상태 기계가 사용됩니다. 위의 논리를 구현하려면 명확하고 간결합니다.
    알고리즘 코드는 다음과 같습니다.
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;
}
  1. 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);

요약하다

전반적으로 이 프로젝트에는 다음과 같이 개선해야 할 영역이 여전히 많이 있습니다.

  1. iFlytek Voice는 실시간 변환을 지원하므로 문장을 완성할 필요가 없습니다.대신 말하는 동안 계속 변환하고 동적으로 수정합니다.이런 방식으로 말한 후에 변환이 완료되므로 더욱 효율적입니다. , 하지만 해당 정보가 없습니다. 실시간 전환과 즉각적인 수정에 대해 더 자세히 알아보려면 더 나은 인내심이 필요합니다.
  2. Motionverse 드라이버의 효율성은 상대적으로 낮습니다. 매번 텍스트를 배경으로 전송해야 합니다. 작업이 완료된 후 음성 데이터와 모션 데이터를 생성한 다음 다시 전송합니다. 지연이 상대적으로 크고 모션이 운전자는 상대적으로 뻣뻣하고 유휴 애니메이션에서 말하기로의 전환은 액션이며 과도하지 않고 매우 불친절합니다.

Supongo que te gusta

Origin blog.csdn.net/sdhexu/article/details/131844251
Recomendado
Clasificación