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

server/2024/10/4 13:16:34/

文章目录

    • 整体
      • 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/server/126949.html

相关文章

SSM人才信息招聘系统-计算机毕业设计源码28084

摘要 本研究旨在基于Java和SSM框架设计并实现一个人才信息招聘系统&#xff0c;旨在提升招聘流程的效率和精准度。通过深入研究Java和SSM框架在Web应用开发中的应用&#xff0c;结合人才招聘领域的需求&#xff0c;构建了一个功能完善、稳定高效的招聘系统。利用SSM框架的优势&…

leetcode 链表 203. 移除链表元素

链表是一种通过指针串联在一起的线性结构&#xff0c;每一个节点由两部分组成&#xff0c;一个是数据域一个是指针域&#xff08;存放指向下一个节点的指针&#xff09;&#xff0c;最后一个节点的指针域指向null&#xff08;空指针的意思&#xff09; 链表的入口节点称为链表…

Github 2024-09-28Rust开源项目日报Top10

根据Github Trendings的统计,今日(2024-09-28统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Rust项目10Starlark项目1Python项目1TypeScript项目1Pake: 利用 Rust 轻松构建轻量级多端桌面应用 创建周期:491 天开发语言:Rust协议类型:M…

CNN模型对CIFAR-10中的图像进行分类

代码功能 这段代码展示了如何使用 Keras 和 TensorFlow 构建一个卷积神经网络&#xff08;CNN&#xff09;模型&#xff0c;用于对 CIFAR-10 数据集中的图像进行分类。主要功能包括&#xff1a; 加载数据&#xff1a;从 CIFAR-10 数据集加载训练和测试图像。 数据预处理&#…

前端导出页面PDF

import html2canvas from html2canvas import { jsPDF } from jspdf import { Loading } from element-ui let downloadLoadingInstance// 导出页面为PDF格式---使用插件html2canvas和jspdf插件 export function exportPDF(fileName, node) {downloadLoadingInstance Loading.…

ARM嵌入式学习--第一天

-ARM核介绍 -CPU核 CPU又叫中央处理器&#xff0c;其主要功能是进行算数运算和逻辑运算&#xff0c;内部结构大概可以分为控制单元&#xff0c;算术逻辑单元和储存单元等几个部分 -ARM核 工作模式&#xff1a; user mode:用户模式是用户程序的工作模式&#xff0c;他运行在操作…

北京数字孪生工业互联网可视化技术,赋能新型工业化智能制造工厂

随着北京数字孪生工业互联网可视化技术的深入应用&#xff0c;新型工业化智能制造工厂正逐步迈向智能化、高效化的全新阶段。这项技术不仅实现了物理工厂与数字世界的精准映射&#xff0c;更通过大数据分析、人工智能算法等先进手段&#xff0c;为生产流程优化、资源配置合理化…

Tomcat监控与调优:比Tomcat Manager更加强大的Psi-Probe

这是一款 Tomcat 管理和监控工具&#xff0c;前身是 Lambda Probe。由于 Lambda Probe 2006不再更新&#xff0c;所以 PSI Probe 算是对其的一个 Fork 版本并一直更新至今。 Probe psi-probe是在相同的开源许可证(GPLV2)下分发的社区驱动的 Lambda Probe &#xff0c;psi-pro…