OpenAI ChatGPT Unity接入
- OpenAI ChatGPT Unity接入
- OpenAi-API-Unity 方法
- OpenAi-API-Unity 下载
- 本地配置
- Unity 模块
- URL接入
- gz 接入
- json 接入
- Open AI
- OpenAi-Api-Unity 插件文档
- OpenAi 本地化接入 Unity 方法
- Unity 关键字识别
- 语音合成 & 文字转语音
- 音频记录 & 实时音频处理
- 语音转文字
- Vosk 接受响应
- OpenAI
- 发送消息 示例
- 返回消息 示例
- Open AI 访问
- 发送 Json 数据支持类
- 接收 Json 数据支持类
- 脚本搭载 以及 层级结构
- 运行情况
OpenAI ChatGPT Unity接入
OpenAi-API-Unity 方法
OpenAi-API-Unity 下载
GitHub: OpenAi-API-Unity 主页
GitHub: OpenAi-API-Unity 安装包 下载地址
既然 Unity 都给了下载地址都给了,怎么能少了 UE 呢。都在下面了
GitHub: OpenAi-API-Unreal 主页
GitHub: OpenAi-API-Unreal 安装包 下载地址
反正我下载的是 0.2.9 版本 你们按需下载就行。
如果都下载不了的话 我 CSDN 上面上传的也有 巴拉一下就能找到。
为了防止出现意外 最好两个 安装包都下载。
本地配置
打开我的电脑 在地址栏键入:
Windows:%USERPROFILE%/
Mac:~/
找到 .openai 文件夹并打开。
如果没有就自己新建一个。
找到 auth.json 文件夹并打开。
如果没有就自己新建一个。
找到 auth.json 文件夹并打开。
如果没有就自己新建一个。
下面是里面的内容注意:填写自己OpenAI key 的时候没有尖括号!没有尖括号!没有尖括号!
重要的事情说三遍。
{
"private_api_key":"<YOUR_KEY>"
}
Unity 模块
新建一个新的项目 打开 Package Manager。
URL接入
点击 git URL 按钮
键入:https://github.com/hexthedev/OpenAi-Api-Unity.git
如果成功会有 Open AI 插件显示。
如果没下再试试下面的方法。
gz 接入
点击 gz 导入按钮
点击刚才下载好的 压缩包
如果成功会有 Open AI 插件显示。
如果没下再试试下面的的方法。
json 接入
这个绝对能成!
点击 磁盘导入按钮。
解压刚才下载 OpenAi-Api-Unity-0.2.9.zip 压缩包
打开文件夹到根目录。
点击 package.json 文本并打开
你就会发现 成功了!旋转 跳跃。
Open AI
在菜单栏 点击 OpenAi -> Examples -> Chat at Runtime
然后就会打开一个新的场景。
好了,点击运行。在这里你就可以,跟 OpenAI 对话了。
OpenAi-Api-Unity 插件文档
GitHub: OpenAi-Api-Unity 插件文档 地址
OpenAi 本地化接入 Unity 方法
Unity 关键字识别
当前脚本主要作用是:使用 Unity 自带的 PhraseRecognizer 类 进行关键字识别。
识别通过之后响应 之后的逻辑执行。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Windows.Speech;
/// <summary>
/// 关键字识别
/// </summary>
public class KeywordRecognition_ZH : MonoBehaviour
{
public static KeywordRecognition_ZH _Instance;
//语音识别短句
private PhraseRecognizer _PhraseASR;
[Header("精度")]
public ConfidenceLevel _PrecisionASR = ConfidenceLevel.Medium;
[Header("关键字数组")]
public string[] _ListStrASR = { "启动", "关闭", "小哟" };
//麦克风
private string _Device;
[Header("智能回答布尔")]
public bool _OpenASRbool;
void Start()
{
_Instance = this;
//获取麦克风设备
_Device = Microphone.devices[0];
print(_Device);
//用设备开始录音
//设备的名称
//指示当达到长度秒时录音是否应该继续录制,并从音频剪辑开始绕圈录制
//录音产生的音频剪辑的长度
//44100 音频采样率
//_AuduioASR = Microphone.Start(_Device, true, 999, 44100);
if (_PhraseASR == null)
{
//创建语音识别
_PhraseASR = new KeywordRecognizer(_ListStrASR, _PrecisionASR);
//添加广播事件
_PhraseASR.OnPhraseRecognized += MonitorASR;
//开启语音识别
_PhraseASR.Start();
Debug.Log("创建识别器成功");
}
}
/// <summary>
/// 语音识别监听
/// 提供关于短语识别事件的信息
/// </summary>
/// <param 识别信息="_Args"></param>
private void MonitorASR(PhraseRecognizedEventArgs _Args)
{
switch (_Args.text)
{
case "启动":
//每次回复前清空 输入问题
GetOpenAI_ZH._Instance._InputProblemText.text = "";
VoskSpeechToText._Instance._SpeakBool = true;
VoskSpeechToText._Instance.ToggleRecording();
_OpenASRbool = true;
break;
case "小哟":
VoskSpeechToText._Instance._SpeakBool = true;
VoskSpeechToText._Instance.ToggleRecording();
_OpenASRbool = true;
break;
case "关闭":
VoskSpeechToText._Instance._SpeakBool = false;
VoskSpeechToText._Instance.ToggleRecording();
break;
default:
break;
}
print(_Args.text);
}
private void OnDestroy()
{
//判断场景中是否存在语音识别
if (_PhraseASR != null)
{
//语音识别释放
_PhraseASR.Dispose();
}
}
}
语音合成 & 文字转语音
根据百度提供的 API 文档进行文字转语音输出。
using Baidu.Aip.Speech;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Http;
using UnityEngine;
using UnityEngine.Networking;
/// <summary>
/// 语音合成 & 文字转语音
/// </summary>
public class VoiceSynthesis_ZH : MonoBehaviour
{
[Header("音源")]
public AudioSource _Audio;
[Header("AI 发声器")]
public Pronouncer _Pronouncer = Pronouncer.Duyaya;
//全局变量
public static VoiceSynthesis_ZH _World;
//网页文字转语音
private string _Url;
//百度语音识别SDK
private Asr _AipClient;
private void Start()
{
_World = this;
//StartCoroutine(GetAudioClip("开始"));
}
//获取 Web网页音源信息并播放
public IEnumerator GetAudioClip(string AudioText)
{
if (AudioText!=null)
{
_Url = "http://tsn.baidu.com/text2audio?tex=" + AudioText +
"&tok=自己的Token" +
"&cuid=a7a0e3326da873c6fb0609e6385a82b934c9cb11" +
"&ctp=1" +
"&lan=zh" +
"&spd=5" +
"&pit=5" +
"&vol=10" +
"&per=" + (((int)_Pronouncer).ToString()) +
"&aue=6";
using (UnityWebRequest _AudioWeb = UnityWebRequestMultimedia.GetAudioClip(_Url, AudioType.WAV))
{
yield return _AudioWeb.SendWebRequest();
if (_AudioWeb.isNetworkError)
{
yield break;
}
AudioClip _Cli = DownloadHandlerAudioClip.GetContent(_AudioWeb);
_Audio.clip = _Cli;
_Audio.Play();
}
}
}
/// <summary>
/// 获取 Web网页音源信息并播放 附带延迟时间
/// </summary>
/// <param 播放文字="AudioText"></param>
/// <param 延迟时间="_DelayedTimer"></param>
/// <returns></returns>
public IEnumerator GetAudioClip(string AudioText, float _DelayedTimer)
{
yield return new WaitForSeconds(_DelayedTimer);
_Url = "http://tsn.baidu.com/text2audio?tex=" + AudioText +
"&tok=自己的Token" +
"&cuid=a7a0e3326da873c6fb0609e6385a82b934c9cb11" +
"&ctp=1" +
"&lan=zh" +
"&spd=5" +
"&pit=5" +
"&vol=10" +
"&per=" + (((int)_Pronouncer).ToString()) +
"&aue=6";
using (UnityWebRequest _AudioWeb = UnityWebRequestMultimedia.GetAudioClip(_Url, AudioType.WAV))
{
yield return _AudioWeb.SendWebRequest();
if (_AudioWeb.isNetworkError)
{
yield break;
}
AudioClip _Cli = DownloadHandlerAudioClip.GetContent(_AudioWeb);
_Audio.clip = _Cli;
_Audio.Play();
}
}
/// <summary>
/// 语音识别
/// </summary>
/// <param 录取音频 = "_Clip" ></ param >
/// < returns ></ returns >
private IEnumerator Recognition(AudioClip _Clip)
{
//开放 音频 长度
float[] _Sample = new float[_Clip.samples];
//用片段中的样本数据填充数组
_Clip.GetData(_Sample, 0);
//数据转换
short[] _IntData = new short[_Sample.Length];
byte[] _ByteData = new byte[_IntData.Length * 2];
for (int i = 0; i < _Sample.Length; i++)
{
_IntData[i] = (short)(_Sample[i] * short.MaxValue);
}
Buffer.BlockCopy(_IntData, 0, _ByteData, 0, _ByteData.Length);
//返回Json数据 (数据 格式 码率)
var _Result = _AipClient.Recognize(_ByteData, "pcm", 16000);
//获取Json 数据中的 讲话内容
var _Speaking = _Result.GetValue("result");
//检测是否有内容
if (_Speaking == null)
{
StopAllCoroutines();
yield return null;
}
//讲话内容转换为 字符串
string _UsefulText = _Speaking.First.ToString();
print(_UsefulText);
yield return null;
}
/// <summary>
/// 访问令牌获取
/// </summary>
/// <returns></returns>
private static class AccessToken
{
// 调用getAccessToken()获取的 access_token建议根据expires_in 时间 设置缓存
// 返回token示例
public static String TOKEN = "自己的Token";
// 百度云中开通对应服务应用的 API Key 建议开通应用的时候多选服务
private static String clientId = "百度云应用的AK";
// 百度云中开通对应服务应用的 Secret Key
private static String clientSecret = "百度云应用的SK";
public static String GetAccessToken()
{
String authHost = "https://aip.baidubce.com/oauth/2.0/token";
HttpClient client = new HttpClient();
List<KeyValuePair<string, string>> paraList = new List<KeyValuePair<string, string>>();
paraList.Add(new KeyValuePair<string, string>("grant_type", "client_credentials"));
paraList.Add(new KeyValuePair<string, string>("client_id", clientId));
paraList.Add(new KeyValuePair<string, string>("client_secret", clientSecret));
HttpResponseMessage response = client.PostAsync(authHost, new FormUrlEncodedContent(paraList)).Result;
String result = response.Content.ReadAsStringAsync().Result;
Console.WriteLine(result);
return result;
}
}
/// <summary>
/// AI 发音器
/// </summary>
public enum Pronouncer
{
//普通女声
Female,
//普通男生
Male,
//特殊男声
Teshunan,
//情感合成男生
Duxiaoyao,
//情感合成女生
Duyaya
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
Application.Quit();
}
}
}
音频记录 & 实时音频处理
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 记录音频并为实时音频处理提供帧
/// </summary>
public class VoiceProcessor : MonoBehaviour
{
/// <summary>
/// 指示麦克风是否被捕获
/// </summary>
public bool IsRecording
{
get { return _audioClip != null && Microphone.IsRecording(CurrentDeviceName); }
}
[Header("麦克风")]
[SerializeField] private int _MicrophoneIndex;
/// <summary>
/// 录制音频的采样率
/// </summary>
public int SampleRate { get; private set; }
/// <summary>
/// 传送的音频帧的大小
/// </summary>
public int FrameLength { get; private set; }
/// <summary>
/// 传递音频帧的事件
/// </summary>
public event Action<short[]> OnFrameCaptured;
/// <summary>
/// 事件,当音频捕获线程停止时
/// </summary>
public event Action OnRecordingStop;
/// <summary>
/// 事件,当音频捕获线程启动时
/// </summary>
public event Action OnRecordingStart;
/// <summary>
/// 可用的录音设备
/// </summary>
public List<string> Devices { get; private set; }
/// <summary>
/// 所选录音设备的索引
/// </summary>
public int CurrentDeviceIndex { get; private set; }
/// <summary>
/// 所选录音设备的名称
/// </summary>
public string CurrentDeviceName
{
get
{
if (CurrentDeviceIndex < 0 || CurrentDeviceIndex >= Microphone.devices.Length)
return string.Empty;
return Devices[CurrentDeviceIndex];
}
}
[Header("语音最小音量检测")]
[SerializeField, Tooltip("检测语音输入的最小音量"), Range(0.0f, 1.0f)]
private float _MinimumSpeakingSampleValue = 0.05f;
[SerializeField, Tooltip("发送语音请求前检测到的沉默时间(以秒为单位)")]
[Header("语音发送沉默时间")]
private float _SilenceTimer = 1.0f;
[SerializeField, Tooltip("使用音量阈值自动检测语音。")]
[Header("语音检测布尔")]
private bool _AutoDetect;
private float _TimeAtSilenceBegan;
private bool _AudioDetected;
private bool _DidDetect;
private bool _Transmit;
AudioClip _audioClip;
private event Action RestartRecording;
void Awake()
{
UpdateDevices();
}
#if UNITY_EDITOR
void Update()
{
if (CurrentDeviceIndex != _MicrophoneIndex)
{
ChangeDevice(_MicrophoneIndex);
}
}
#endif
/// <summary>
/// 更新可用音频设备列表
/// </summary>
public void UpdateDevices()
{
Devices = new List<string>();
foreach (var device in Microphone.devices)
Devices.Add(device);
if (Devices == null || Devices.Count == 0)
{
CurrentDeviceIndex = -1;
Debug.LogError("没有连接有效的录音设备");
return;
}
CurrentDeviceIndex = _MicrophoneIndex;
}
/// <summary>
/// 更换录音设备
/// </summary>
/// <param name="deviceIndex">新音频捕获设备的索引</param>
public void ChangeDevice(int deviceIndex)
{
if (deviceIndex < 0 || deviceIndex >= Devices.Count)
{
Debug.LogError(string.Format("指定的设备索引{0}不是有效的记录设备", deviceIndex));
return;
}
if (IsRecording)
{
// 用新设备重新开始录制的一次性事件
// 最后一个会话完成的时刻
RestartRecording += () =>
{
CurrentDeviceIndex = deviceIndex;
StartRecording(SampleRate, FrameLength);
RestartRecording = null;
};
StopRecording();
}
else
{
CurrentDeviceIndex = deviceIndex;
}
}
/// <summary>
/// 开始录音
/// </summary>
/// <param name="sampleRate">记录的采样率</param>
/// <param name="frameSize">要传送的音频帧的大小</param>
/// <param name="autoDetect">音频是否应该根据音量连续记录</param>
public void StartRecording(int sampleRate = 16000, int frameSize = 512, bool ?autoDetect = null)
{
if (autoDetect != null)
{
_AutoDetect = (bool) autoDetect;
}
if (IsRecording)
{
// 如果采样率或帧大小已经改变,重新开始记录
if (sampleRate != SampleRate || frameSize != FrameLength)
{
RestartRecording += () =>
{
StartRecording(SampleRate, FrameLength, autoDetect);
RestartRecording = null;
};
StopRecording();
}
return;
}
SampleRate = sampleRate;
FrameLength = frameSize;
_audioClip = Microphone.Start(CurrentDeviceName, true, 1, sampleRate);
StartCoroutine(RecordData());
}
/// <summary>
/// 停止录音
/// </summary>
public void StopRecording()
{
if (!IsRecording)
return;
Microphone.End(CurrentDeviceName);
Destroy(_audioClip);
_audioClip = null;
_DidDetect = false;
StopCoroutine(RecordData());
}
/// <summary>
/// 用于缓冲传入音频数据和传送帧的循环
/// </summary>
IEnumerator RecordData()
{
float[] sampleBuffer = new float[FrameLength];
int startReadPos = 0;
if (OnRecordingStart != null)
OnRecordingStart.Invoke();
while (IsRecording)
{
int curClipPos = Microphone.GetPosition(CurrentDeviceName);
if (curClipPos < startReadPos)
curClipPos += _audioClip.samples;
int samplesAvailable = curClipPos - startReadPos;
if (samplesAvailable < FrameLength)
{
yield return null;
continue;
}
int endReadPos = startReadPos + FrameLength;
if (endReadPos > _audioClip.samples)
{
// 碎片式读取(绕到片段的开头)
// 在片段末尾读取位
int numSamplesClipEnd = _audioClip.samples - startReadPos;
float[] endClipSamples = new float[numSamplesClipEnd];
_audioClip.GetData(endClipSamples, startReadPos);
// 在剪辑开始时读取位
int numSamplesClipStart = endReadPos - _audioClip.samples;
float[] startClipSamples = new float[numSamplesClipStart];
_audioClip.GetData(startClipSamples, 0);
// 组合成全画框
Buffer.BlockCopy(endClipSamples, 0, sampleBuffer, 0, numSamplesClipEnd);
Buffer.BlockCopy(startClipSamples, 0, sampleBuffer, numSamplesClipEnd, numSamplesClipStart);
}
else
{
_audioClip.GetData(sampleBuffer, startReadPos);
}
startReadPos = endReadPos % _audioClip.samples;
if (_AutoDetect == false)
{
_Transmit =_AudioDetected = true;
}
else
{
float maxVolume = 0.0f;
for (int i = 0; i < sampleBuffer.Length; i++)
{
if (sampleBuffer[i] > maxVolume)
{
maxVolume = sampleBuffer[i];
}
}
if (maxVolume >= _MinimumSpeakingSampleValue)
{
_Transmit= _AudioDetected = true;
_TimeAtSilenceBegan = Time.time;
}
else
{
_Transmit = false;
if (_AudioDetected && Time.time - _TimeAtSilenceBegan > _SilenceTimer)
{
_AudioDetected = false;
}
}
}
if (_AudioDetected)
{
_DidDetect = true;
// 转换为16位int样本
short[] pcmBuffer = new short[sampleBuffer.Length];
for (int i = 0; i < FrameLength; i++)
{
pcmBuffer[i] = (short) Math.Floor(sampleBuffer[i] * short.MaxValue);
}
// 引发缓冲区事件
if (OnFrameCaptured != null && _Transmit)
OnFrameCaptured.Invoke(pcmBuffer);
}
else
{
if (_DidDetect)
{
if (OnRecordingStop != null)
OnRecordingStop.Invoke();
_DidDetect = false;
}
}
}
if (OnRecordingStop != null)
OnRecordingStop.Invoke();
if (RestartRecording != null)
RestartRecording.Invoke();
}
}
语音转文字
使用麦克风 录取音频片段并根据模型进行 转录文字输出。
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Ionic.Zip;
using Unity.Profiling;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using Vosk;
/// <summary>
/// 语音转文字
/// </summary>
public class VoskSpeechToText : MonoBehaviour
{
//单例
public static VoskSpeechToText _Instance;
[Header("模型路径")]
[Tooltip("模型的位置,相对于Streaming Assets文件夹。")]
public string ModelPath = "vosk-model-small-cn-0.22.zip";
[Header("麦克风")]
[Tooltip("麦克风输入的源")]
public VoiceProcessor VoiceProcessor;
[Header("处理最大数量")]
[Tooltip("将要处理的备选项的最大数量。")]
public int MaxAlternatives = 3;
[Header("最长记录时间")]
[Tooltip("在重新开始之前我们应该记录多长时间?")]
public float MaxRecordLength = 5;
[Header("识别器启动布尔")]
[Tooltip("识别器是否应该在应用程序启动时启动?")]
public bool AutoStart = true;
[Header("检测短语数组")]
[Tooltip("将被检测到的短语。如果为空,将检测所有单词。")]
public List<string> KeyPhrases = new List<string>();
//Vosk模型的缓存版本。
private Model _Model;
//Vosk识别器的缓存版本。
private VoskRecognizer _Recognizer;
//条件标志,用于查看是否已经创建了识别器。
//TODO:允许对识别器进行运行时更改。
private bool _RecognizerReady;
//保留所有音频数据,直到用户停止通话。
private readonly List<short> _Buffer = new List<short>();
//当控制器的状态改变时调用。
public Action<string> OnStatusUpdated;
//在用户完成讲话后调用,vosk处理音频。
public Action<string> OnTranscriptionResult;
//解压缩模型文件夹的绝对路径。
private string _DecompressedModelPath;
//字符串,包含Json Array格式的关键字
private string _Grammar = "";
//用于等待模型文件成功解压缩的标志。
private bool _IsDecompressing;
//用于等待脚本成功启动的标志。
private bool _IsInitializing;
//用于检查Vosk是否启动的标志。
private bool _DidInit;
//关键字识别布尔
[Header("关键字识别布尔")]
public bool _SpeakBool = true;
//线程的逻辑
//旗帜表示我们结束了
private bool _Running;
//麦克风数据的线程安全队列。
private readonly ConcurrentQueue<short[]> _ThreadedBufferQueue = new ConcurrentQueue<short[]>();
//线程安全的结果队列
private readonly ConcurrentQueue<string> _ThreadedResultQueue = new ConcurrentQueue<string>();
static readonly ProfilerMarker _VoskRecognizerCreateMarker = new ProfilerMarker("VoskRecognizer.Create");
static readonly ProfilerMarker _VoskRecognizerReadMarker = new ProfilerMarker("VoskRecognizer.AcceptWaveform");
//如果启用了“自动启动”,则可以启动语音转文本。
void Start()
{
_Instance = this;
if (AutoStart)
{
StartVoskStt();
}
}
/// <summary>
/// 启动Vosk语音转文本
/// </summary>
/// <param name="keyPhrases">关键字/短语列表。关键字需要存在于模型字典中,所以像“webview”这样的词被更好地检测为两个更常见的词“webview”.</param>
/// <param name="modelPath">模型文件夹相对于StreamingAssets的路径。如果路径以.zip结尾,它将被解压缩到应用程序数据持久文件夹中</param>
/// <param name="startMicrophone">"麦克风应该在vosk初始化之后吗?</param>
/// <param name="maxAlternatives">检测到的可选短语的最大数目</param>
public void StartVoskStt(List<string> keyPhrases = null, string modelPath = default, bool startMicrophone = false, int maxAlternatives = 3)
{
if (_IsInitializing)
{
Debug.LogError("正在初始化!");
return;
}
if (_DidInit)
{
Debug.LogError("Vosk 已经初始化!");
return;
}
//语言模型加载
if (!string.IsNullOrEmpty(modelPath))
{
ModelPath = modelPath;
}
//关键字加载
if (keyPhrases != null)
{
KeyPhrases = keyPhrases;
}
MaxAlternatives = maxAlternatives;
StartCoroutine(DoStartVoskStt(startMicrophone));
}
//解压模型,加载设置,启动Vosk和可选地启动麦克风
private IEnumerator DoStartVoskStt(bool startMicrophone)
{
_IsInitializing = true;
yield return WaitForMicrophoneInput();
yield return Decompress();
OnStatusUpdated?.Invoke("加载模型来自: " + _DecompressedModelPath);
//Vosk.Vosk.SetLogLevel(0);
_Model = new Model(_DecompressedModelPath);
yield return null;
OnStatusUpdated?.Invoke("初始化");
VoiceProcessor.OnFrameCaptured += VoiceProcessorOnOnFrameCaptured;
VoiceProcessor.OnRecordingStop += VoiceProcessorOnOnRecordingStop;
if (startMicrophone)
VoiceProcessor.StartRecording();
_IsInitializing = false;
_DidInit = true;
ToggleRecording();
_SpeakBool = false;
ToggleRecording();
}
//将keyphrases转换为json数组,并在末尾附加' [unk] '关键字,以告诉vosk过滤其他短语。
private void UpdateGrammar()
{
if (KeyPhrases.Count == 0)
{
_Grammar = "";
return;
}
JSONArray keywords = new JSONArray();
foreach (string keyphrase in KeyPhrases)
{
keywords.Add(new JSONString(keyphrase.ToLower()));
}
keywords.Add(new JSONString("[unk]"));
_Grammar = keywords.ToString();
}
//解压缩模型zip文件或返回解压缩文件的位置。
private IEnumerator Decompress()
{
if (!Path.HasExtension(ModelPath)
|| Directory.Exists(
Path.Combine(Application.persistentDataPath, Path.GetFileNameWithoutExtension(ModelPath))))
{
OnStatusUpdated?.Invoke("使用现有的解压缩模型");
_DecompressedModelPath =
Path.Combine(Application.persistentDataPath, Path.GetFileNameWithoutExtension(ModelPath));
Debug.Log(_DecompressedModelPath);
yield break;
}
OnStatusUpdated?.Invoke("模型解压中...");
string dataPath = Path.Combine(Application.streamingAssetsPath, ModelPath);
Stream dataStream;
// 从流资产路径读取数据。你不能直接在Android上访问流媒体资源。
if (dataPath.Contains("://"))
{
UnityWebRequest www = UnityWebRequest.Get(dataPath);
www.SendWebRequest();
while (!www.isDone)
{
yield return null;
}
dataStream = new MemoryStream(www.downloadHandler.data);
}
// 在有效的平台上直接读取文件。
else
{
dataStream = File.OpenRead(dataPath);
}
//读取Zip文件
var zipFile = ZipFile.Read(dataStream);
//等待zip文件完成解压缩
zipFile.ExtractProgress += ZipFileOnExtractProgress;
//更新状态文本
OnStatusUpdated?.Invoke("读取Zip文件");
//开始提取
zipFile.ExtractAll(Application.persistentDataPath);
//等到它完成
while (_IsDecompressing == false)
{
yield return null;
}
//覆盖ZipFileOnExtractProgress中给出的路径以防止崩溃
_DecompressedModelPath = Path.Combine(Application.persistentDataPath, Path.GetFileNameWithoutExtension(ModelPath));
//更新状态文本
OnStatusUpdated?.Invoke("解压完成!");
//稍等一下,以防我们需要初始化另一个对象。
yield return new WaitForSeconds(1);
//处理zipfile读取器。
zipFile.Dispose();
}
///更新zip文件提取过程时调用的函数。
private void ZipFileOnExtractProgress(object sender, ExtractProgressEventArgs e)
{
if (e.EventType == ZipProgressEventType.Extracting_AfterExtractAll)
{
_IsDecompressing = true;
_DecompressedModelPath = e.ExtractLocation;
}
}
//等待麦克风初始化
private IEnumerator WaitForMicrophoneInput()
{
while (Microphone.devices.Length <= 0)
yield return null;
}
//可以从脚本或GUI按钮中调用以启动检测。
public void ToggleRecording()
{
Debug.Log("Toogle记录");
if (!VoiceProcessor.IsRecording)
{
if (_SpeakBool )
{
Debug.Log("开始记录");
_Running = true;
VoiceProcessor.StartRecording();
Task.Run(ThreadedWork).ConfigureAwait(false);
}
else
{
Debug.Log("停止记录");
_Running = false;
VoiceProcessor.StopRecording();
}
}
else
{
Debug.Log("停止记录");
_Running = false;
VoiceProcessor.StopRecording();
}
}
//调用Unity线程上的On短语识别事件
void Update()
{
if (_ThreadedResultQueue.TryDequeue(out string voiceResult))
{
OnTranscriptionResult?.Invoke(voiceResult);
}
}
/// <summary>
/// 当检测到新的音频时,从语音处理器回调
/// </summary>
/// <param name="samples"></param>
private void VoiceProcessorOnOnFrameCaptured(short[] samples)
{
_ThreadedBufferQueue.Enqueue(samples);
}
/// <summary>
/// 录音停止时从语音处理器回调
/// </summary>
private void VoiceProcessorOnOnRecordingStop()
{
Debug.Log("停止");
}
//将音频逻辑输入语音识别器
private async Task ThreadedWork()
{
_VoskRecognizerCreateMarker.Begin();
if (!_RecognizerReady)
{
UpdateGrammar();
//仅检测指定的已定义关键字。
if (string.IsNullOrEmpty(_Grammar))
{
_Recognizer = new VoskRecognizer(_Model, 16000.0f);
}
else
{
_Recognizer = new VoskRecognizer(_Model, 16000.0f, _Grammar);
}
_Recognizer.SetMaxAlternatives(MaxAlternatives);
//_recognizer.SetWords(true);
_RecognizerReady = true;
Debug.Log("识别器准备好了");
}
_VoskRecognizerCreateMarker.End();
_VoskRecognizerReadMarker.Begin();
while (_Running)
{
if (_ThreadedBufferQueue.TryDequeue(out short[] voiceResult))
{
if (_Recognizer.AcceptWaveform(voiceResult, voiceResult.Length))
{
var result = _Recognizer.Result();
_ThreadedResultQueue.Enqueue(result);
}
}
else
{
// 等待一些数据
await Task.Delay(100);
}
}
_VoskRecognizerReadMarker.End();
}
}
Vosk 接受响应
针对模型输出结果的响应输出。
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// Vosk 返回结果
/// </summary>
public class VoskResultText : MonoBehaviour
{
[Header("Vosk 语音转文本")]
public VoskSpeechToText _VoskSpeechToText;
[Header("返回文本")]
public InputField _ResultText;
void Awake()
{
//监听
_VoskSpeechToText.OnTranscriptionResult += OnTranscriptionResult;
}
/// <summary>
/// 语音转录文字方法
/// </summary>
/// <param name="obj"></param>
private void OnTranscriptionResult(string obj)
{
//Debug.Log(obj);
var result = new RecognitionResult(obj);
for (int i = 0; i < result.Phrases.Length; i++)
{
//说话内容 不为空
if (result.Phrases[i].Text!=""|| result.Phrases[i].Text!=" ")
{
if (i > 0)
{
_ResultText.text += ", ";
}
//如果是多个结果的话 只要第一个结果
_ResultText.text += result.Phrases[0].Text.Replace(" ", "");
//说话内容 并剔除所有空格
Debug.Log(_ResultText.text);
var _StrRe = result.Phrases[0].Text.Replace(" ", "");
//ResultText.text = _StrRe;
if (_StrRe != "小哟" || _StrRe != "小哟小哟")
{
//如果是多个结果的话 只要第一个结果
_ResultText.text = _StrRe;
}
if (KeywordRecognition_ZH._Instance._OpenASRbool)
{
//智能回答
GetOpenAI_ZH._Instance._InputProblemText.text = _StrRe;
GetOpenAI_ZH._Instance.SendData();
//录音关闭
VoskSpeechToText._Instance._SpeakBool = false;
VoskSpeechToText._Instance.ToggleRecording();
//回答响应布尔
KeywordRecognition_ZH._Instance._OpenASRbool = false;
}
return;
}
}
}
}
OpenAI
访问 Open AI 需要一个接收 Json 数据的类,一个发送 Json 数据的类,我都会放在下面。
还有几个文档链接大家注意看。
Chat API 访问 URL:https://api.openai.com/v1/chat/completions
极简回答:
《核心》:访问 OpenAI 网址、使用 POST 方法进行参数传递、等待响应、可视化显示。
链接: Open AI API 文档地址
链接: Open AI API 示例地址
链接: Open AI API 开放平台地址
发送消息 示例
//发送消息 示例
{
"model":"gpt-3.5-turbo",
"messages":
[
{
"role":"user",
"content":"你是谁"
}
],
"max_tokens":2048,
"temperature":0.5,
"top_p":1,
"frequency_penalty":1,
"presence_penalty":1,
"stop":"stop"
}
返回消息 示例
//返回消息 示例
{
"id":"chatcmpl-7CQ1C0ZL7XUOdqPcblJDxxPnOmI1c",
"object":"chat.completion",
"created":1683194306,
"model":"gpt-3.5-turbo-0301",
"usage":{
"prompt_tokens":12,
"completion_tokens":15,
"total_tokens":27
},
"choices":
[
{
"message":
{
"role":"assistant",
"content":"我是一个AI语言模型,由OpenAI开发。"
},
"finish_reason":"stop",
"index":0
}
]
}
Open AI 访问
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using static AcceptJson_ZH;
using static PostDataJson_ZH;
/// <summary>
/// Open AI 访问
/// </summary>
public class GetOpenAI_ZH : MonoBehaviour
{
public static GetOpenAI_ZH _Instance;
//API key
private string _OpenAI_Key = "";
// Chat API 访问 URL
private string _OpenAIUrl = "https://api.openai.com/v1/chat/completions";
//配置参数
[SerializeField] private PostDataJson_ZH _PostDataSetting;
[Header("输入的信息")]
[SerializeField] public InputField _InputProblemText;
[Header("回复的信息")]
[SerializeField] public Text _RobotChatText;
//回答模型
[HideInInspector]
public ModelType _ModelType { get; set; }
//模型菜单显示 布尔
private bool _ModelMenuActive;
private void Awake()
{
_Instance = this;
}
/// <summary>
/// 回答模型选择方法
/// </summary>
/// <param 当前执行 Button="_ModelTypeButton"></param>
public void ModelTypeSet(Button _ModelTypeButton)
{
if (_ModelTypeButton.name == "gpt-3.5-turbo")
{
_ModelType = ModelType.gpt35turbo;
}
else if (_ModelTypeButton.name == "gpt-3.5-turbo-0301")
{
_ModelType = ModelType.gpt35turbo0301;
}
else if (_ModelTypeButton.name == "gpt-4")
{
_ModelType = ModelType.gpt4;
}
else if (_ModelTypeButton.name == "gpt-4-0314")
{
_ModelType = ModelType.gpt40314;
}
else if (_ModelTypeButton.name == "gpt-4-32k")
{
_ModelType = ModelType.gpt432k;
}
else if (_ModelTypeButton.name == "gpt-4-32k-0314")
{
_ModelType = ModelType.gpt432k0314;
}
else if (_ModelTypeButton.name == "text-davinci-003")
{
_ModelType = ModelType.textdavinci003;
}
}
/// <summary>
/// 发送信息
/// </summary>
public void SendData()
{
if (_InputProblemText.text.Equals(""))
return;
//问题
string _Problem = _InputProblemText.text;
StartCoroutine(GetPostData(_Problem));
//_InputText.text = "";
}
/// <summary>
/// POST 方法请求
/// </summary>
/// <param 问题="_SendMessage"></param>
/// <returns></returns>
private IEnumerator GetPostData(string _SendMessage)
{
OpenAI 访问
//var _Request = new UnityWebRequest(_OpenAIUrl, "POST");
// OpenAI 访问
using (UnityWebRequest _Request = new UnityWebRequest(_OpenAIUrl, "POST"))
{
//规则归一化
PostDataJson_ZH _PostData = new PostDataJson_ZH
{
//model = "gpt-3.5-turbo",
max_tokens = _PostDataSetting.max_tokens,
temperature = _PostDataSetting.temperature,
top_p = _PostDataSetting.top_p,
frequency_penalty = _PostDataSetting.frequency_penalty,
presence_penalty = _PostDataSetting.presence_penalty,
stop = _PostDataSetting.stop
};
//模型设置
switch (_ModelType)
{
//gpt-3.5-turbo、gpt-3.5-turbo-0301、gpt-4、gpt-4-0314、gpt-4-32k、gpt-4-32k-0314、text-davinci-003
case ModelType.gpt35turbo:
_PostData.model = "gpt-3.5-turbo";
break;
case ModelType.gpt35turbo0301:
_PostData.model = "gpt-3.5-turbo-0301";
break;
case ModelType.gpt4:
_PostData.model = "gpt-4";
break;
case ModelType.gpt40314:
_PostData.model = "gpt-4-0314";
break;
case ModelType.gpt432k:
_PostData.model = "gpt-4-32k";
break;
case ModelType.gpt432k0314:
_PostData.model = "gpt-4-32k-0314";
break;
case ModelType.textdavinci003:
_PostData.model = "text-davinci-003";
break;
default:
break;
}
//消息赋予
PostDataJson_ZH.MessagesItem _Messages = new PostDataJson_ZH.MessagesItem();
_Messages.role = "user";
_Messages.content = _SendMessage;
_PostData.messages.Add(_Messages);
//数据转换
string _JsonText = JsonUtility.ToJson(_PostData);
print(_JsonText);
byte[] _Data = System.Text.Encoding.UTF8.GetBytes(_JsonText);
//数据上传 等待响应
_Request.uploadHandler = new UploadHandlerRaw(_Data);
_Request.downloadHandler = new DownloadHandlerBuffer();
//数据重定向
_Request.SetRequestHeader("Content-Type", "application/json");
_Request.SetRequestHeader("Authorization", string.Format("Bearer {0}", _OpenAI_Key));
//等待响应 开始与远程服务器通信
yield return _Request.SendWebRequest();
//数据返回
if (_Request.responseCode == 200)
{
//接收返回信息
string _Message = _Request.downloadHandler.text;
//数据转换
AcceptJson_ZH _Textback = JsonUtility.FromJson<AcceptJson_ZH>(_Message);
//确保当前有消息传回
if (_Textback != null && _Textback.choices.Count > 0)
{
//OpenAI 输出 清空
_RobotChatText.text = "";
//打印
print(_Textback.choices[0].message.content);
//回答消息填写
_RobotChatText.text = _Textback.choices[0].message.content;
//文字转语音 回答
StartCoroutine(VoiceSynthesis_ZH._World.GetAudioClip(_Textback.choices[0].message.content));
}
}
}
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
Application.Quit();
}
if (Input.GetKeyDown(KeyCode.Q))
{
_ModelMenuActive = !_ModelMenuActive;
//显示设置
if (_ModelMenuActive)
{
GameObject.Find("模型选择").transform.localScale = Vector3.one;
}
else
{
GameObject.Find("模型选择").transform.localScale = Vector3.zero;
}
}
}
}
发送 Json 数据支持类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Open AI 发送 数据
/// </summary>
[System.Serializable]
public class PostDataJson_ZH
{
[Header("使用模型")]
[Tooltip("gpt-3.5-turbo、gpt-3.5-turbo-0301、gpt-4、gpt-4-0314、gpt-4-32k、gpt-4-32k-0314、text-davinci-003")]
public ModelType _modelType = ModelType.gpt35turbo;
[HideInInspector]
public string model = "gpt-3.5-turbo";
[Header("发送信息 数据")]
public List<MessagesItem> messages = new List<MessagesItem>();
[Header("最大 Token 值")]
[Tooltip("可以在聊天完成中生成的最大令牌数")]
public int max_tokens = 2048;
[Header("采样温度")]
[Tooltip("温度 使用什么采样温度,介于 0 和 2 之间。较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定")]
[Range(0,2)]
public float temperature = 0.5f;
[Header("采样温度")]
[Tooltip("使用温度采样的替代方法称为核心采样,其中模型考虑具有top_p概率质量的令牌的结果。因此,0.1 意味着只考虑包含前 10% 概率质量的迭代")]
[Range(0, 1)]
public float top_p = 1;
[Header("重复度")]
[Tooltip("介于 -2.0 和 2.0 之间的数字。正值会根据新标记到目前为止在文本中的现有频率来惩罚新标记,从而降低模型逐字重复同一行的可能性")]
[Range(0, 2)]
public float frequency_penalty = 1.0f;
[Header("新主题")]
[Tooltip("介于 -2.0 和 2.0 之间的数字。正值会根据新标记到目前为止是否出现在文本中来惩罚它们,从而增加模型讨论新主题的可能性")]
[Range(0, 2)]
public float presence_penalty = 1.0f;
[Header("结束原因")]
[Tooltip("最多 4 个序列,其中 API 将停止生成更多令牌。")]
public string stop = "stop";
[System.Serializable]
public class MessagesItem
{
/// <summary>
/// 演员
/// </summary>
public string role;
/// <summary>
/// 信息
/// </summary>
public string content;
}
[System.Serializable]
public enum ModelType
{
//gpt-3.5-turbo
gpt35turbo,
//gpt-3.5-turbo-0301
gpt35turbo0301,
//gpt-4
gpt4,
//gpt-4-0314
gpt40314,
//gpt-4-32k
gpt432k,
//gpt-4-32k-0314
gpt432k0314,
//text-davinci-003
textdavinci003
}
}
接收 Json 数据支持类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Open AI 接收 数据
/// </summary>
[System.Serializable]
public class AcceptJson_ZH
{
/// <summary>
/// Open AI ID
/// </summary>
public string id;
/// <summary>
/// 支付方式
/// </summary>
public string @object;
/// <summary>
/// 建立 标识
/// </summary>
public int created;
/// <summary>
/// 使用模型
/// </summary>
public string model;
/// <summary>
/// 用法
/// </summary>
public Usage usage;
/// <summary>
/// 参数选择
/// </summary>
[SerializeField]
public List<ChoicesItem> choices;
[System.Serializable]
public class Usage
{
/// <summary>
/// 指示令牌
/// </summary>
public int prompt_tokens;
/// <summary>
/// 完成令牌
/// </summary>
public int completion_tokens;
/// <summary>
/// 总令牌
/// </summary>
public int total_tokens;
}
[System.Serializable]
public class Message
{
/// <summary>
/// 角色 一般是 user
/// </summary>
public string role;
/// <summary>
/// 回答 信息
/// </summary>
public string content;
}
[System.Serializable]
public class ChoicesItem
{
/// <summary>
/// 返回消息
/// </summary>
[SerializeField]
public Message message;
/// <summary>
/// 完成原因 一般是 stop
/// </summary>
public string finish_reason;
/// <summary>
/// 步数
/// </summary>
public int index;
}
}
脚本搭载 以及 层级结构
KeywordRecognition_ZH 以及 VoiceSynthesis_ZH 搭载情况
VoiceProcessor、VoskSpeechToText、VoskResultText 搭载情况
GetOpenAI_ZH 搭载情况
Unity 层次结构
运行情况
程序包含了语音输出模块、语音输出模块、ChatGPt回答、中英文双语对话、预留了二次开发接口。
就这吧 不想写了 人麻了。
好像忘了啥。啊对 Vosk的下载链接没给你们,我就不往上翻了就直接在下面写了。
太长了,抱歉 哈哈哈。
你们要是没有积分 就直接给我发私信 给我邮箱 我直接发你们邮箱 别说谢 受不了。(。・∀・)ノ゙
链接: Vosk 官网
链接: Vosk 下载地址
链接: Vosk 语言模型下载地址
暂时先这样吧,如果有时间的话就会更新我调教好大语言模型的,以后抽空再弄个一体化,散会吧。要是实在看不明白就留言,看到我会回复的。
路漫漫其修远兮,与君共勉。