【Unity AI】基于 WebSocket 和 讯飞星火大模型

devtools/2024/10/4 12:48:18/

文章目录

    • 整体
      • AIManager
      • DialogueManager
      • UIManager
      • ModelManager
      • AudioManager
      • SaveManager
    • 详细部分
      • AI
      • UI
      • 动画
      • 音频

在这里插入图片描述
在这里插入图片描述

整体

AIManager

负责配置讯飞的appId,生成鉴权URL,通过WebSocket向服务器请求并返回数据(分为最终返回和流式返回)

 public async void RequestAnswer(List<Content> dialogueContext, Action<string> completeCallback, Action<string> streamingCallback = null){//获取鉴权URLstring authUrl = GetAuthURL();string url = authUrl.Replace("http://", "ws://").Replace("https://", "wss://");using(webSocket = new ClientWebSocket()){try{//向服务器发起连接请求await webSocket.ConnectAsync(new Uri(url), cancellation);Debug.Log("成功连接服务器");//改变参数的上下文对话(这一块外包给了dialogueManager进行控制,因为要控制上下文长度)request.payload.message.text = dialogueContext;//将Json序列化为byte流string jsonString = JsonConvert.SerializeObject(request);byte[] binaryJsonData = Encoding.UTF8.GetBytes(jsonString.ToString());//向服务器发送数据_=webSocket.SendAsync(new ArraySegment<byte>(binaryJsonData), WebSocketMessageType.Text, true, cancellation);Debug.Log("已向服务器发送数据,正在等待消息返回...");//循环等待服务器返回内容byte[] receiveBuffer = new byte[1024];WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), cancellation);String resp = "";while (!result.CloseStatus.HasValue){if (result.MessageType == WebSocketMessageType.Text){string receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, result.Count);//将结果解释为Json(没有指定具体类型)JObject jsonObj = JObject.Parse(receivedMessage);int code = (int)jsonObj["header"]["code"];if (0 == code){int status = (int)jsonObj["payload"]["choices"]["status"];JArray textArray = (JArray)jsonObj["payload"]["choices"]["text"];string content = (string)textArray[0]["content"];resp += content;if (status == 2){Debug.Log($"最后一帧: {receivedMessage}");int totalTokens = (int)jsonObj["payload"]["usage"]["text"]["total_tokens"];Debug.Log($"整体返回结果: {resp}");Debug.Log($"本次消耗token数: {totalTokens}");completeCallback(resp.TrimStart('\n'));return;}else{streamingCallback?.Invoke(resp.TrimStart('\n'));}}else{Debug.Log($"请求报错: {receivedMessage}");if (code == 10013)OnRequestBan?.Invoke();break;}}else if (result.MessageType == WebSocketMessageType.Close){Debug.Log("已关闭WebSocket连接");break;}result = await webSocket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), cancellation);}}catch (Exception e){Debug.LogError(e.ToString());}completeCallback(null);}}string GetAuthURL(){//date参数生成string date = DateTime.UtcNow.ToString("r");//authorization参数生成StringBuilder stringBuilder = new StringBuilder("host: spark-api.xf-yun.com\n");stringBuilder.Append("date: ").Append(date).Append("\n");stringBuilder.Append("GET /").Append(llmVersion).Append(" HTTP/1.1");//利用hmac-sha256算法结合APISecret对上一步的tmp签名,获得签名后的摘要tmp_sha。//将上方的tmp_sha进行base64编码生成signaturestring signature = HMACsha256(apiSecret, stringBuilder.ToString());//利用上面生成的签名,拼接下方的字符串生成authorization_originstring authorization_origin = string.Format("api_key=\"{0}\", algorithm=\"{1}\", headers=\"{2}\", signature=\"{3}\"", apiKey, "hmac-sha256", "host date request-line", signature);//最后再将上方的authorization_origin进行base64编码,生成最终的authorizationstring authorization = Convert.ToBase64String(Encoding.UTF8.GetBytes(authorization_origin));//将鉴权参数组合成最终的键值对,并urlencode生成最终的握手URL。string path1 = "authorization=" + authorization;string path2 = "date=" + WebUtility.UrlEncode(date);string path3 = "host=" + "spark-api.xf-yun.com";return "wss://spark-api.xf-yun.com/" + llmVersion + "?" + path1 + "&" + path2 + "&" + path3;}public string HMACsha256(string apiSecretIsKey, string buider){byte[] bytes = Encoding.UTF8.GetBytes(apiSecretIsKey);System.Security.Cryptography.HMACSHA256 hMACSHA256 = new System.Security.Cryptography.HMACSHA256(bytes);byte[] date = Encoding.UTF8.GetBytes(buider);date = hMACSHA256.ComputeHash(date);hMACSHA256.Clear();//将上方的tmp_sha进行base64编码生成signaturereturn Convert.ToBase64String(date);}

DialogueManager

作为UI和AI的中间层,存储历史的用户和AI对话,对历史对话进行裁切,保存到本地。

//说话
public void Talk(string contentText, Action<string> completeCallback, Action<string> streamingCallback)
{//添加到历史记录AddHistory(Role.User, contentText);//用于发送到AIManager,拼接上玩家的角色设定List<Content> dialogue = new List<Content>() { CharacterSettingContent };dialogue.AddRange(historyDialogue);//日志打印StringBuilder sb = new StringBuilder();foreach(Content content in dialogue){sb.Append(content.content).Append("\n");}Debug.LogWarning(sb.ToString());//发送给AIAIManager.Instance.RequestAnswer(dialogue, (answer) =>{if(answer != null)AddHistory(Role.AI, answer);completeCallback(answer);}, streamingCallback);
}public void AddHistory(Content content)
{Role role = Role.User;switch(content.role){case "system":role = Role.System;break;case "user":role = Role.User;break;case "assistant":role = Role.AI;break;}AddHistory(role, content.content);
}public void AddHistory(Role role, string contentText)
{//确定当前加入历史记录的Rolestring roleStr = "";switch(role){case Role.System:roleStr = "system";break;case Role.User:roleStr = "user";break;case Role.AI:roleStr = "assistant";break;default:break;}//添加到历史记录historyDialogue.AddLast(new Content() { role = roleStr, content = contentText });currentTokens += contentText.Length;//如果是用户发送的,就进行裁切if(role == Role.User){//裁切历史文本while (currentTokens >= MaxHistoryTokens){//发送删除委托OnDialogueRemoved?.Invoke(historyDialogue.First());currentTokens -= historyDialogue.First.Value.content.Length;historyDialogue.RemoveFirst();}}//序列化后保存到本地SaveManager.Instance.data.dialogues = historyDialogue;SaveManager.Instance.Save();StringBuilder sb = new StringBuilder();foreach (Content content in historyDialogue){sb.Append(content.content).Append("\n");}Debug.Log(sb.ToString());//发送委托OnDialogueAdded?.Invoke(historyDialogue.Last());
}

UIManager

负责跨UI的交互,如输入发送用户文本,需要触发接收显示对话气泡文本,涉及到两个UI类。配合DOTween实现打字机效果,无代码实现随字体伸展的对话框效果。

下面是对话气泡的显示效果

using DG.Tweening;
using DG.Tweening.Core;
using DG.Tweening.Plugins.Options;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;public class DialogueBubbleUI : MonoBehaviour
{//UI引用public TMP_Text shortTextUI, longTextUI;public GameObject shortUI, longUI;private ScrollRect longUIScrollRect;//分割属性public int splitLength = 360;public int limitShortTextUIHeigt = 171;//打字机效果TweenerCore<string, string, StringOptions> tweener;public float typeSpeed = .01f;//临时存储private string lastTextContent = "";private void Awake(){longUIScrollRect = longUI.GetComponent<ScrollRect>();}//隐藏和复位所有对话框public void ResetUI(){shortUI.SetActive(false);shortTextUI.text = string.Empty;longUI.SetActive(false);longTextUI.text = string.Empty;}public void PlayFromStart(string endText){ResetUI();Play(endText);}public void Play(string endText){string startText;if (longTextUI.text != string.Empty)startText = longTextUI.text;elsestartText = shortTextUI.text;//DoTween实现打字机效果tweener?.Kill();tweener = DOTween.To(() => startText, value =>{//中间判断说的话的长度,确定是否需要转到大的对话框(可下拉的那种)//达到阈值后转为显示大对话框if (shortTextUI.rectTransform.rect.height > limitShortTextUIHeigt && shortTextUI.text.Length > 0){if (!longUI.activeSelf){shortUI.SetActive(false);longUI.SetActive(true);}longTextUI.text = value;longUIScrollRect.verticalNormalizedPosition = 0f;}else{if (!shortUI.activeSelf){shortUI.SetActive(true);longUI.SetActive(false);}shortTextUI.text = value;}//播放UI音效if (lastTextContent != value)AudioManager.Instance.PlayUIAudio(UIAudioType.DialoguePopText);lastTextContent = value;}, endText, endText.Length * typeSpeed).SetUpdate(true).SetEase(Ease.Linear);}
}

ModelManager

负责触发模型的动画,使用枚举值选择动画,枚举名称对应了animator中的对应名称的参数。

负责定时随机播放模型的待机动画,问问题时的思考动画播放

AudioManager

负责播放UI音效(文字冒泡音效,),内部使用池化的思想,构建了一个大小固定的AudioSource池,对外暴露的Play接口会从池中获取闲置的或者最早使用的AudioSource,避免每次都重新创建该组件。

使用AudioMixer配合暴露的参数来统一控制播放的音量。

负责背景音乐的播放,开始时随机选择一首,播放完一首后播放下一首。对外暴露接口供播放器UI调用。

using DG.Tweening;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;public class AudioManager : UnitySingleton<AudioManager>
{[Header("UI Audio")]//UI音效对象池public int AudioPoolSize = 10;private GameObject audioSourceParent;private Queue<AudioSource> audioSources;//UI音量组public AudioMixerGroup uiAudioMixerGroup;private float initUIAudioVolume;//配置UI音频列表[System.Serializable]public class UIAudioClipConfig{public UIAudioType type;public AudioClip clip;}public List<UIAudioClipConfig> uiAudioClips;private Dictionary<UIAudioType, AudioClip> uiAudioClipsDict = new Dictionary<UIAudioType, AudioClip>();[Header("BGM")]//背景音量组(背景使用单独的AudioSource)public AudioMixerGroup bgmAudioMixerGroup;private float initBGMAudioVolume;private AudioSource bgmAudioSource;//配置背景音频列表[System.Serializable]public class BGAudioClipConfig{public string SongTitle, Singer;public AudioClip song;}public List<BGAudioClipConfig> BGMs;private int currentBgmIndex;private bool isMusicPlay = true;//委托public event System.Action<BGAudioClipConfig> OnBGMSelected;public event System.Action OnBGMPlay, OnBGMPause;protected override void Awake(){base.Awake();//初始化默认参数uiAudioMixerGroup.audioMixer.GetFloat("UIVolume", out initUIAudioVolume);bgmAudioMixerGroup.audioMixer.GetFloat("BGMVolume", out initBGMAudioVolume);//编辑器面板配置的UI参数数组转为字典foreach(var config in  uiAudioClips){uiAudioClipsDict.Add(config.type, config.clip);}uiAudioClips.Clear();//初始化一个AudioSources子级对象audioSourceParent = transform.Find("AudioSources")?.gameObject;if (audioSourceParent == null){GameObject obj = new GameObject("AudioSources");audioSourceParent = obj;audioSourceParent.transform.parent = transform;}//在子级对象上添加UIAudioSourceaudioSources = new Queue<AudioSource>(AudioPoolSize);for (int i = 0; i < AudioPoolSize; i++){audioSources.Enqueue(audioSourceParent.AddComponent<AudioSource>());}//在子级上添加背景的AudioSourcebgmAudioSource = audioSourceParent.AddComponent<AudioSource>();bgmAudioSource.outputAudioMixerGroup = bgmAudioMixerGroup;}private void Start(){//读取UI音频设置SetUIAudioVolume(SaveManager.Instance.data.uiVolumeMultiper);//读取背景音频设置SetBGMAudioVolume(SaveManager.Instance.data.bgmVolumeMultiper);//读取播放设置isMusicPlay = SaveManager.Instance.data.isMusicPlay;//随机选择背景音乐SelectBGM(Random.Range(0, BGMs.Count));}//设置UI音量public void SetUIAudioVolume(float multiper){//修改AudioMixerGroupuiAudioMixerGroup.audioMixer.SetFloat("UIVolume", multiper < 0.05 ? -1000 : Mathf.LerpUnclamped(initUIAudioVolume * 2, initUIAudioVolume, multiper));//存储到本地SaveManager.Instance.data.uiVolumeMultiper = multiper;SaveManager.Instance.Save();}//设置BGM音量public void SetBGMAudioVolume(float multiper){//修改AudioMixerGroupbgmAudioMixerGroup.audioMixer.SetFloat("BGMVolume", multiper < 0.05 ? -1000 : Mathf.LerpUnclamped(initBGMAudioVolume * 2, initBGMAudioVolume, multiper));//存储到本地SaveManager.Instance.data.bgmVolumeMultiper = multiper;SaveManager.Instance.Save();}public void PlayAudio(AudioClip audioClip, AudioSource audioSource){audioSource.PlayOneShot(audioClip);audioSources.Enqueue(audioSource);}public void PlayUIAudio(UIAudioType audioType){AudioSource audioSource = audioSources.Dequeue();audioSource.outputAudioMixerGroup = uiAudioMixerGroup;PlayAudio(uiAudioClipsDict[audioType], audioSource);}private void Update(){if(isMusicPlay){if(!bgmAudioSource.isPlaying){SelectNextBGM();}}}//BGMpublic void PlayBGM(){isMusicPlay = true;bgmAudioSource.clip = BGMs[currentBgmIndex].song;bgmAudioSource.Play();SaveManager.Instance.data.isMusicPlay = true;SaveManager.Instance.Save();OnBGMPlay?.Invoke();}public void PauseBGM(){isMusicPlay = false;bgmAudioSource.Pause();SaveManager.Instance.data.isMusicPlay = false;SaveManager.Instance.Save();OnBGMPause?.Invoke();}public void SelectBGM(int index){if (index >= BGMs.Count)return;currentBgmIndex = index;OnBGMSelected?.Invoke(BGMs[index]);if (isMusicPlay)PlayBGM();elsePauseBGM();}public void SelectLastBGM() => SelectBGM((currentBgmIndex - 1) % BGMs.Count);public void SelectNextBGM() => SelectBGM((currentBgmIndex + 1) % BGMs.Count);
}public enum UIAudioType
{DialoguePopText,SendBtn,ButtonPress
}

SaveManager

内部有一个Data可序列化类。外部可修改该类内部数据,并调用Save方法将其序列化到本地。

当前序列化的内容有历史对话,音量调整,大模型设置,光线调整。

protected override void Awake()
{base.Awake();//获取文件,读取所需的Json文件并序列化为DatadataPath = Application.persistentDataPath + "/" + dataName;if(File.Exists(dataPath)){using(StreamReader sr = new StreamReader(dataPath)){string json = sr.ReadToEnd();data = JsonConvert.DeserializeObject<Data>(json);}}else{data = new Data();}
}public void Save()
{if (File.Exists(dataPath))File.Delete(dataPath);string json = JsonConvert.SerializeObject(data);File.WriteAllText(dataPath, json);
}

详细部分

AI

对外暴露最大回复数,模型的切换,内部根据官方文档实现了鉴权和websocket连接服务器。以及流式返回和最终返回的委托。

UI

通过Content Size Filter和Vertical Layout Group 和 Horizontal Layout Group 的多层嵌套,以及两种版本的气泡UI,通过同一个类对其进行显隐和更新的控制,实现了较少字体时气泡随字体更新改变自身大小,较多字体时气泡固定并允许内容滚动显示的功能。对外只需要调用Play并传入文字即可。

动画

使用Unity的Avatar动画重定向功能,将Mixamo骨骼动画映射到MMD骨骼上。

音频

使用枚举和字典来让外部确认要播放什么音频,通过在AudioManager下配置枚举值对应音频,再向外提供通过枚举值进行播放的接口

背景音乐通过将音频和音乐名,歌手名整合成一个类,更改时使用委托将组合类发送出去。UI可以根据接受到的类显示歌和歌手名


http://www.ppmy.cn/devtools/121259.html

相关文章

地产房源展示预约小程序制作线索发展

地产业每个城市中都有不少从业者和品牌&#xff0c;由于其交易金额较大&#xff0c;所以转化时间和路径相对较长&#xff0c;线上品牌宣传、客户获取转化和内容承载也是重要的点。 线上平台和渠道更加多样化&#xff0c;各种房产信息呈现利于分享和咨询&#xff0c;房地产房源展…

深入解析 RISC-V 递归函数的栈使用:以阶乘函数为例

在处理递归函数时&#xff0c;RISC-V 体系架构的寄存器数量有限。为了确保每次递归调用能正确保存和恢复寄存器的状态&#xff0c;栈&#xff08;stack&#xff09;提供了灵活的解决方案。本文将结合具体的汇编代码和递归的阶乘函数 fact 来讲解 RISC-V 中如何利用栈进行寄存器…

鸿蒙开发(NEXT/API 12)【硬件(获取智慧出行连接状态)】车载系统

获取智慧出行连接状态&#xff0c;用于应用UI呈现或基于HiCar认证汽车摄像头的业务交互等。 接口说明 接口名描述[getSmartMobilityStatus] (type: SmartMobilityType): SmartMobilityInfo获取智慧出行连接状态。 开发步骤 导入Car Kit模块。 import { smartMobilityCommon …

C/C++复习(一)

1.sizeof 关于sizeof我们是经常使用的&#xff0c;所以使用方法就不需要提及了&#xff0c;这里我们需要注意的是&#xff0c;sizeof 后面如果是表达式可以不用括号&#xff0c;并且sizeof实际上不参与运算&#xff0c;返回的是内容的类型大小&#xff08;size_t类型&#xff0…

react+antdMobie实现消息通知页面样式

一、实现效果 二、代码 import React, { useEffect, useState } from react; import style from ./style/index.less; import { CapsuleTabs, Ellipsis, Empty, SearchBar, Tag } from antd-mobile; //消息通知页面 export default function Notification(props) {const [opti…

排序算法之——归并排序,计数排序

文章目录 前言一、归并排序1. 归并排序的思想2. 归并排序时间复杂度及空间复杂度3. 归并排序代码实现1&#xff09;递归版本2&#xff09;非递归版本 二、计数排序1. 计数排序的思想2. 计数排序的时间复杂度及空间复杂度3. 计数排序代码实现 总结&#xff08;排序算法稳定性&am…

【Qt Quick】基础语法:变量类型

在本节中&#xff0c;我们将讨论 QML 中的变量类型。与 C 相似&#xff0c;QML 也有多种变量类型&#xff0c;但在 QML 中&#xff0c;主要分为值类型和对象类型。由于 QML 没有指针的概念&#xff0c;因此在值类型和对象类型的传递中有一些不同点。 值类型和对象类型 值类型…

《深度学习》OpenCV 图像拼接 拼接原理、参数解析、案例实现

目录 一、图像拼接 1、直接看案例 图1与图2展示&#xff1a; 合并完结果&#xff1a; 2、什么是图像拼接 3、图像拼接步骤 1&#xff09;加载图像 2&#xff09;特征点检测与描述 3&#xff09;特征点匹配 4&#xff09;图像配准 5&#xff09;图像变换和拼接 6&am…