我之前一直在做Hololens2集成大语言模型(LLM)的开发,看我之前的博文有提过。今天主要是记录一下我在进行关键词检测功能开发中遇到的一些坎和经验,本次博文也是面向新手。(不过可能有些细节不注意到,可能解释的不是很详细,欢迎大家评论提问)
1.全流程介绍
首先LLM语音识别我目前将它分为几个部分进行功能开发:
语音转文本用的是Azure云服务,这个其实有很多教程都讲到过。然后后面文本转语音也是Azure云服务。至于LLM部分我之前的博文()提到了,本次只介绍关键词检测功能的实现。关键词检索这里的功能主要分为两个部分,
- 检索到有关键词则调用响应的方法(比如触发“打开聊天记录”关键词,则触发相关方法,并输出文本text2:“好的,已帮你打开聊天记录”。并将该文本转为语音进行输出。)
- 未检索到关键词,则将文本输入到LLM里,然后LLM再输出答案text3,最后文本转语音。这部分内容的完成可以看我之前的博文。
本篇文章主要是讲解其中的关键词检测部分的脚本思路,此外大家同理也可以全程用GPT来帮助写代码。
2.关键词检索初步方案
一开始我的打算就很简单,就是如下所示:
private int biaozhi = 0;
private void Update()
{
if(needPost)
{
Debug.Log("调用LlmTalk");
llmTalk.LlmTalk(inputText);
needPost = false;
}
// 检查biaozhi是否有变化
if (biaozhi != previousBiaozhi)
{
needPost = false;
// 如果biaozhi变化,执行相应的动作
if (biaozhi == 1)
{
*******
************
*********
animator.Play(animationName1);
Answer2Log("好的,已帮你打开聊天记录");
}
private void HandleRecognitionSuccess(string recognizedText)
{
string modifiedText = "user:" + recognizedText ;
TextLibrary.Instance.AddText(modifiedText);
Debug.Log($"识别到的文本: {modifiedText}");
//聊天UI
if (recognizedText.Contains("查看聊天记录")|| recognizedText.Contains("打开聊天记录") || recognizedText.Contains("打开聊天界面") || recognizedText.Contains("查看聊天界面"))
{
Debug.Log("触发了“查看聊天记录”关键词");
biaozhi = 1;
}
else
{
needPost = true;
inputText = recognizedText;
Debug.Log("无关键词,请求大语言模型");
}
}
首先先解释这个代码中的总体输入输出:
private void HandleRecognitionSuccess(string recognizedText)这里面的recognizedText就是从语音转文本模块获取到的文本,而llmTalk.LlmTalk(inputText);就是将文本送给大语言模型LLM的执行语句,其中inputText就是输出文本。其实这里recognizedText和inputText内容是一样的。
我用“状态”来表示关键词触发的情况,因为我的关键词触发算是一个Toggle类别(也就是开关类别)。比如开启目标检测呀,关闭目标检测呀之类的。所以我就在语音转文字后开始调用HandleRecognitionSuccess函数,recognizedText为语音转文本后的“文本”。通过Contains()来检测关键词。接着设置状态标准符:int biaozhi,若检测到状态变化则调用相应的函数。这里面的Answer2Log()方法是我自己编写的文本转语音的函数。
3.方案优化
但是我发现到最后需要识别的关键词太多了,而且,每个关键词检测到后调用的函数都需要在代码里面填写,太复杂了,最主要是有时候自己都忘了有哪些关键词,要到代码里找关键词。
后来关于这个问题我问了一下GPT,结果发现他给出了UnityEvent的方案,如下:
using UnityEngine.Events;
public class KeywordChecker : MonoBehaviour
{
public UnityEvent OpenLog;
public UnityEvent CloseLog;
}
if (biaozhi == 1)
{
OpenLog?.Invoke();
animator.Play(animationName1);
Answer2Log("好的,已帮你打开聊天记录");
}
首先如代码中所示的,有两个关键词检测:“打开聊天记录”,“关闭聊天记录”。所以我取名OpenLog,CloseLog。唯一的不同就是状态biaozhi变为“1”时,调用的函数我用OpenLog?.Invoke();进行调用,这里在界面中是这样的,大家发现了没,这个Unity的自带的功能就和我们在给按钮button设置点击事件一样简单,这样的方法不用在代码里面幸幸苦苦的调用了,直接在界面里面拖拽,一方面省事,另一方面还可以在界面中查看代码运行的流程。
但是最后我还是发现了几个问题:首先时这个UnityEvent事件不能自己增加,我们只能在一个OpenLog对象里添加。这样一来我想要增加其他的关键词绑定就很麻烦。
给GPT的问题:
现在我是用 public UnityEvent OpenLog;功能,可以在界面中拖拽对象进行方法调用,我觉得很方便,因为这样很省事,但是有一点不好,监听的事件只能在代码中添加,我希望事件数量同样可以在界面中添加
后来我修改代码如下:
public class EventList
{
public List<UnityEvent> events = new List<UnityEvent>();
}
public class docQADemo : MonoBehaviour
{
public EventList myEvents;
}
这样就可以添加事件的个数,不过我们还需要增加一个功能:那就是给每个element()都添加一个关键词内容的编辑功能。所以就改成了下面这个:
现在可以在界面中添加关键词内容以及自动性回复内容。
下面是我问GPT的问题:
private int biaozhi = 0;
private void Update()
{
if(needPost)
{
Debug.Log("调用LlmTalk");
llmTalk.LlmTalk(inputText);
needPost = false;
}
// 检查biaozhi是否有变化
if (biaozhi != previousBiaozhi)
{
needPost = false;
// 如果biaozhi变化,执行相应的动作
if (biaozhi == 1)
{
*******
************
*********
animator.Play(animationName1);
Answer2Log("好的,已帮你打开聊天记录");
}
private void HandleRecognitionSuccess(string recognizedText)
{
string modifiedText = "user:" + recognizedText ;
TextLibrary.Instance.AddText(modifiedText);
Debug.Log($"识别到的文本: {modifiedText}");
//聊天UI
if (recognizedText.Contains("查看聊天记录")|| recognizedText.Contains("打开聊天记录") || recognizedText.Contains("打开聊天界面") || recognizedText.Contains("查看聊天界面"))
{
Debug.Log("触发了“查看聊天记录”关键词");
biaozhi = 1;
}
else
{
needPost = true;
inputText = recognizedText;
Debug.Log("无关键词,请求大语言模型");
}
}
这是我现在的代码,我希望它能够通过拖拽的方式在界面中选择触发事件的函数。
但是关键词内容和监听事件的个数都需要在代码中修改,所以现在我添加了一个类:
public class KeywordEvent
{
public string keyword;
public UnityEvent responseEvent;
}
在主类中声明了:
[SerializeField]
private List<KeywordEvent> keywordEvents = new List<KeywordEvent>();
这样就可以在Unity界面中编辑事件个数和关键词内容了,而我现在需要你帮我修改源代码,
使得脚本能够对recognizedText进行关键词检测并执行相应的事件。
其实我们从最初版本到现在的版本都很好理解,就是从上图变成了下图
4.源码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 可在界面中自定义关键词以及触发的函数
/// </summary>
[System.Serializable]
public class KeywordEvent
{
public string keyword;
public string responseWord;
public UnityEvent responseEvent;
}
public class docQADemo : MonoBehaviour
{
public SpeechRec speechRec;
public LLMTalk llmTalk;
public TextToSpeech textToSpeech;
private string inputText = "";
[SerializeField]
private List<KeywordEvent> keywordEvents = new List<KeywordEvent>();
// 状态符,用于标记需要触发的事件ID,初始值为-1表示无事件
private int currentEventId = -1;
private bool needPost = false;
private void Start()
{
if (speechRec != null)
{
speechRec.OnRecognition2KeyWord += HandleRecognitionSuccess;
}
}
private void Update()
{
if (currentEventId != -1) // 检查是否有事件需要被触发
{
keywordEvents[currentEventId].responseEvent?.Invoke(); // 触发事件
if(keywordEvents[currentEventId].responseWord!=null)
{
Answer2Log(keywordEvents[currentEventId].responseWord);
}
currentEventId = -1; // 重置状态符,等待下一个事件
}
if (needPost) // 检查是否有事件需要被触发
{
Debug.Log("请求LLM:" + inputText);
llmTalk.LlmTalk(inputText); // 调用 llmTalk.LlmTalk 方法处理未触发关键词的文本
needPost = false;
}
}
private void HandleRecognitionSuccess(string recognizedText)
{
string modifiedText = "user:" + recognizedText;
TextLibrary.Instance.AddText(modifiedText);
for (int i = 0; i < keywordEvents.Count; i++)
{
if (recognizedText.Contains(keywordEvents[i].keyword))
{
Debug.Log($"触发了“{keywordEvents[i].keyword}”关键词");
currentEventId = i; // 设置状态符为当前关键词事件的索引
break; // 找到后立即中断循环
}
}
if (currentEventId == -1) // 检查是否有事件需要被触发
{
needPost = true;
inputText = recognizedText;
Debug.Log("无关键词,请求大语言模型");
}
}
public void Answer2Log(string aText)
{
string answerText = aText;
textToSpeech.ConvertTextToSpeech(answerText);
string modifiedText = "robot:" + "\n" + answerText + "\n";
TextLibrary.Instance.AddText(modifiedText);
}
private void OnDestroy()
{
if (speechRec != null)
{
speechRec.OnRecognition2KeyWord -= HandleRecognitionSuccess;
}
}
}
其中Answer2Log()是我写的一个文本转语音的函数。