技术交流群在文章底部
文章目录:
UnityHDRP写实数字人全流程基础课1---创建AIAgent
UnityHDRP写实数字人全流程基础课2---AIAgent连接和文本输出
UnityHDRP写实数字人全流程基础课3---查看智能体配置
UnityHDRP写实数字人全流程基础课5---语音输入和语音识别
UnityHDRP写实数字人全流程基础课6---UI自适应与美化
UnityHDRP写实数字人全流程基础课7---角色导入和渲染
UnityHDRP写实数字人全流程基础课8---动画驱动和眨眼
UnityHDRP写实数字人全流程基础课---企业级应用篇内容预告
工程用到的插件资源合集:

在语音输出连通后,我们的项目拥有了说的功能,作为项目核心的"对话",它要完成的功能肯定不能只有说,它还得听。
那么我们这节课需要完成的就有
- 创建一个声音录制的工具
- 创建一个button来使用这个录制工具
- 将录制的声音交给智能体去理解
- 将智能体理解的结果再发送回智能体
接下来我们开始
1. 创建一个声音录制的工具
在开始创建声音录制工具前我们要明确一下这个工具的核心功能(麦克风录音开始→麦克风录音结束→录音文件保存)
那我们首先要定义麦克风设备这个关键的交互硬件。
csharp
[Header("录音设置")]
public string deviceName = ""; // 麦克风设备名称
public int sampleRate = 44100; // 采样率
public int channels = 1; // 声道数
然后在开始时获取默认麦克风的名称,用于后续调用Microphone.Start时使用
csharp
private void Start()
{
// 获取默认麦克风设备名称
if (string.IsNullOrEmpty(deviceName) && Microphone.devices.Length > 0)
{
deviceName = Microphone.devices[0];
Debug.Log($"使用默认麦克风设备: {deviceName}");
}
}
在定义开始录制和结束录制方法前还要定义麦克风的使用状态
csharp
private bool isRecording = false; // 是否正在录音
接下来我们定义麦克风开始录制的方法
csharp
/// <summary>
/// 开始录音
/// </summary>
public void StartRecording()
{
if (isRecording)
{
Debug.LogWarning("已经在录音中");
return;
}
if (string.IsNullOrEmpty(deviceName))
{
Debug.LogError("未找到麦克风设备");
return;
}
Debug.Log("开始录音...");
// 开始录音
recordingClip = Microphone.Start(deviceName, false, 60, sampleRate); // 最大录音时长60秒
isRecording = true;
}
然后是麦克风结束录制的方法
csharp
/// <summary>
/// 停止录音
/// </summary>
public void StopRecordingAndTranscribe()
{
if (!isRecording)
{
Debug.LogWarning("没有在录音中");
return;
}
Debug.Log("停止录音...");
// 停止录音
int position = Microphone.GetPosition(deviceName);
Microphone.End(deviceName);
isRecording = false;
}
当我们录制结束时,需要将音频临时保存下来
首先我们要定义一个将AudioClip保存为音频文件的方法,这里选定的是Wav格式,转码这部分可以先不做理解,像套公式一样去用它。
csharp
/// <summary>
/// 编码并保存WAV文件
/// </summary>
private IEnumerator EncodeAndSaveWav(AudioClip clip, string filePath)
{
// 获取音频数据
float[] samples = new float[clip.samples * clip.channels];
clip.GetData(samples, 0);
// 转换为16位PCM数据
short[] pcmData = new short[samples.Length];
for (int i = 0; i < samples.Length; i++)
{
pcmData[i] = (short)(samples[i] * 32767);
}
// 写入WAV文件
using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fileStream))
{
// RIFF头
writer.Write(new char[] { 'R', 'I', 'F', 'F' });
writer.Write(36 + pcmData.Length * 2);
writer.Write(new char[] { 'W', 'A', 'V', 'E' });
// fmt块
writer.Write(new char[] { 'f', 'm', 't', ' ' });
writer.Write(16);
writer.Write((short)1); // PCM格式
writer.Write((short)clip.channels);
writer.Write(clip.frequency);
writer.Write(clip.frequency * clip.channels * 2); // 字节率
writer.Write((short)(clip.channels * 2)); // 块对齐
writer.Write((short)16); // 位深度
// data块
writer.Write(new char[] { 'd', 'a', 't', 'a' });
writer.Write(pcmData.Length * 2);
// 写入PCM数据
for (int i = 0; i < pcmData.Length; i++)
{
writer.Write(pcmData[i]);
}
}
yield return null;
}
然后我们定义一个临时保存音频文件的方法
csharp
/// <summary>
/// 保存AudioClip为WAV文件
/// </summary>
private IEnumerator SaveAudioClipAsWav(AudioClip clip, string filePath, System.Action<string> callback)
{
// 确保目录存在
string directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 转换为WAV格式并保存
yield return StartCoroutine(EncodeAndSaveWav(clip, filePath));
if (File.Exists(filePath))
{
Debug.Log($"音频文件保存成功: {filePath}");
callback?.Invoke(filePath);
}
else
{
Debug.LogError("音频文件保存失败");
callback?.Invoke(null);
}
}
最后我们在录音结束时调用这个临时保存音频文件的方法
csharp
public void StopRecordingAndTranscribe()
{
......前面的代码保持不变
// 裁剪音频片段到实际录制长度
AudioClip trimmedClip = AudioClip.Create("TrimmedRecording", position, channels, sampleRate, false);
float[] data = new float[position];
recordingClip.GetData(data, 0);
trimmedClip.SetData(data, 0);
// 保存音频为WAV文件
StartCoroutine(SaveAudioClipAsWav(trimmedClip, tempFilePath, (savedFilePath));
}
2. 创建一个button来使用这个录制工具
在页面上创建一个Button,命名为RecordBtn
在UIManager中定义这个button:
csharp
public Button recordButton; // 录音按钮
为录音按钮添加事件监听器,当按下button时开始录音,抬起时结束录音
csharp
// 添加录音按钮事件监听器
if (recordButton != null && audioRecorder != null)
{
// 获取或添加EventTrigger组件
EventTrigger eventTrigger = recordButton.GetComponent<EventTrigger>();
if (eventTrigger == null)
{
eventTrigger = recordButton.gameObject.AddComponent<EventTrigger>();
}
// 清除现有的事件
eventTrigger.triggers.Clear();
// 添加PointerDown事件
EventTrigger.Entry pointerDownEntry = new EventTrigger.Entry();
pointerDownEntry.eventID = EventTriggerType.PointerDown;
pointerDownEntry.callback.AddListener((data) => { audioRecorder.StartRecording(); });
eventTrigger.triggers.Add(pointerDownEntry);
// 添加PointerUp事件
EventTrigger.Entry pointerUpEntry = new EventTrigger.Entry();
pointerUpEntry.eventID = EventTriggerType.PointerUp;
pointerUpEntry.callback.AddListener((data) => { audioRecorder.StopRecordingAndTranscribe(); });
eventTrigger.triggers.Add(pointerUpEntry);
}
/// <summary>
/// 实现IPointerDownHandler接口
/// </summary>
public void OnPointerDown(PointerEventData eventData)
{
// 这个方法主要是为了满足接口要求,实际的按钮事件通过EventTrigger处理
}
/// <summary>
/// 实现IPointerUpHandler接口
/// </summary>
public void OnPointerUp(PointerEventData eventData)
{
// 这个方法主要是为了满足接口要求,实际的按钮事件通过EvenTrigger处理
}
同时我们需要在UIManager的继承里加上IPointerDownHandler,和IPointerUpHandler
csharp
public class UIManager : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
最后我们对按钮是否被按下加入一个明确的标识
csharp
/// <summary>
/// 更新录音按钮状态
/// </summary>
/// <param name="isRecording">是否正在录音</param>
public void UpdateRecordButtonState(bool isRecording)
{
if (recordButton != null)
{
ColorBlock colors = recordButton.colors;
if (isRecording)
{
recordButton.GetComponentInChildren<Text>().text = "松开识别";
}
else
{
recordButton.GetComponentInChildren<Text>().text = "按住说话";
}
recordButton.colors = colors;
}
}
将其绑定在按钮的事件上
csharp
// 添加PointerDown事件
EventTrigger.Entry pointerDownEntry = new EventTrigger.Entry();
pointerDownEntry.eventID = EventTriggerType.PointerDown;
pointerDownEntry.callback.AddListener((data) => { audioRecorder.StartRecording(); });
pointerDownEntry.callback.AddListener((data) => { UpdateRecordButtonState(true); });
eventTrigger.triggers.Add(pointerDownEntry);
// 添加PointerUp事件
EventTrigger.Entry pointerUpEntry = new EventTrigger.Entry();
pointerUpEntry.eventID = EventTriggerType.PointerUp;
pointerUpEntry.callback.AddListener((data) => { audioRecorder.StopRecordingAndTranscribe(); });
pointerUpEntry.callback.AddListener((data) => { UpdateRecordButtonState(false); });
eventTrigger.triggers.Add(pointerUpEntry);
这样我们就得到一个按下录音,松手发送的录音按钮了
3. 将录制的声音交给智能体去理解

跟之前一样,我们看到文档中需要构建的参数有:Token、端点和File。

在File中我们挑选了Wav作为我们构建文件的格式(上文中已经构建好了)
那么我们需要构建的就是如下参数
csharp
private string token;
private string apiUrl = "https://api.coze.cn/v1/audio/transcriptions";
Token我们从之前构建的智能体配置类CozeBotConfigService中获取
csharp
private CozeBotConfigService configService;
private void Start()
{
// 从CozeBotConfigService获取token
configService = FindObjectOfType<CozeBotConfigService>();
token = configService.token;
}
然后我们构建发送参数的协程
csharp
/// <summary>
/// 发送音频文件进行转录的协程
/// </summary>
private IEnumerator TranscribeAudioCoroutine(string audioFilePath, System.Action<string> callback)
{
if (string.IsNullOrEmpty(audioFilePath))
{
Debug.LogError("音频文件路径为空");
callback?.Invoke(null);
yield break;
}
if (!File.Exists(audioFilePath))
{
Debug.LogError($"音频文件不存在: {audioFilePath}");
callback?.Invoke(null);
yield break;
}
if (string.IsNullOrEmpty(token))
{
Debug.LogError("Token为空");
callback?.Invoke(null);
yield break;
}
// 创建表单数据
WWWForm form = new WWWForm();
// 读取音频文件
byte[] audioData = File.ReadAllBytes(audioFilePath);
// 添加文件到表单
form.AddBinaryData("file", audioData, Path.GetFileName(audioFilePath), "audio/mpeg");
// 创建UnityWebRequest
using (UnityWebRequest request = UnityWebRequest.Post(apiUrl, form))
{
// 设置Authorization头
request.SetRequestHeader("Authorization", "Bearer " + token);
// 发送请求
yield return request.SendWebRequest();
// 处理响应
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log("转录请求成功: " + request.downloadHandler.text);
// 解析响应
string transcriptionText = ParseTranscriptionResponse(request.downloadHandler.text);
callback?.Invoke(transcriptionText);
}
else
{
Debug.LogError($"转录请求失败: {request.error}");
Debug.LogError($"响应内容: {request.downloadHandler.text}");
callback?.Invoke(null);
}
}
}
从文档中我们可以知道返回的结构体是
由此构建下面的解析方法
csharp
/// <summary>
/// 解析转录响应
/// </summary>
/// <param name="responseText">响应文本</param>
/// <returns>转录文本</returns>
private string ParseTranscriptionResponse(string responseText)
{
try
{
JObject jsonObject = JObject.Parse(responseText);
// 检查code是否为0
if (jsonObject["code"] != null && jsonObject["code"].ToString() == "0")
{
// 提取data中的text字段
if (jsonObject["data"] != null && jsonObject["data"]["text"] != null)
{
return jsonObject["data"]["text"].ToString();
}
else
{
Debug.LogError("响应中缺少data.text字段");
}
}
else
{
string errorMsg = jsonObject["msg"]?.ToString() ?? "未知错误";
Debug.LogError($"转录失败: {errorMsg}");
}
}
catch (System.Exception ex)
{
Debug.LogError($"解析响应失败: {ex.Message}");
}
return null;
}
再定义一个公共方法给AudioRecorder调用
csharp
/// <summary>
/// 发送音频文件进行转录
/// </summary>
/// <param name="audioFilePath">音频文件路径</param>
/// <param name="callback">转录完成后的回调,参数为转录文本</param>
public void TranscribeAudio(string audioFilePath, System.Action<string> callback)
{
StartCoroutine(TranscribeAudioCoroutine(audioFilePath, callback));
}
在AudioRecorder类中我们获取这个事件的返回值
csharp
/// <summary>
/// 发送音频文件到转录API
/// </summary>
private void SendToTranscriptionAPI(string audioFilePath)
{
if (cozeSTTService == null)
{
Debug.LogError("CozeAudioTranscription服务未找到,无法发送转录请求");
return;
}
Debug.Log($"发送音频到转录API: {audioFilePath}");
// 调用转录服务
cozeSTTService.TranscribeAudio(audioFilePath, (transcriptionText) => {
if (!string.IsNullOrEmpty(transcriptionText))
{
Debug.Log($"转录结果: {transcriptionText}");
// 显示转录结果到UI
if (uiManager != null)
{
uiManager.AppendToScrollView("Transcription: " + transcriptionText);
}
}
else
{
Debug.LogError("转录失败");
if (uiManager != null)
{
uiManager.AppendToScrollView("Transcription failed: 无法识别音频内容");
}
}
// 转录完成后删除临时文件
try
{
if (File.Exists(audioFilePath))
{
File.Delete(audioFilePath);
Debug.Log($"临时文件已删除: {audioFilePath}");
}
}
catch (System.Exception ex)
{
Debug.LogError($"删除临时文件失败: {ex.Message}");
}
});
}
在StopRecordingAndTranscribe方法中调用它
csharp
/// <summary>
/// 停止录音并发送到转录API
/// </summary>
public void StopRecordingAndTranscribe()
{
...前面的代码不变
// 保存音频为WAV文件
StartCoroutine(SaveAudioClipAsWav(trimmedClip, tempFilePath, (savedFilePath) => {
if (!string.IsNullOrEmpty(savedFilePath))
{
// 发送到转录API
SendToTranscriptionAPI(savedFilePath);
}
}));
}
4. 将智能体理解的结果再发送回智能体
我们在UIManager中重新定义一个方法用于向智能体发送返回结果
csharp
public void SendMsg(string message)
{
// 通过CozePostAPI发送消息
if (cozeAPI != null)
{
StartCoroutine(cozeAPI.SendMsg(message));
}
}
csharp
在`AudioRecorder`的`SendToTranscriptionAPI`方法中调用它
/// <summary>
/// 发送音频文件到转录API
/// </summary>
private void SendToTranscriptionAPI(string audioFilePath)
{
...这里代码保持不变
if (uiManager != null)
{
uiManager.AppendToScrollView("You: " + transcriptionText);
uiManager.SendMsg(transcriptionText);
}
}
...这里代码保持不变
}
此时当我们按下按钮说话后,智能体就能自动返回答案了
5. 源码
UIManager
csharp
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using Unity.VisualScripting;
using UnityEngine.EventSystems;
public class UIManager : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
public Button SubmitBtn;
public InputField TextInput;
public Text CallBackText;
public ScrollRect CallBackscrollRect;
public GameObject aboutPanel;
public Button openAboutBtn;
public Button closeBtn;
public RawImage logoImg;
public RawImage bgImg;
public Text TitleText;
public Text AboutText;
public Button recordButton; // 录音按钮
private CozeBotConfigService cozeBotConfigService;
private CozePostAPI cozeAPI;
private AudioRecorder audioRecorder; // 音频录制器
private void Start()
{
// 获取CozePostAPI实例
cozeAPI = FindObjectOfType<CozePostAPI>();
cozeBotConfigService = FindObjectOfType<CozeBotConfigService>();
audioRecorder = FindObjectOfType<AudioRecorder>();
// 添加按钮点击事件监听器
if (SubmitBtn != null)
{
SubmitBtn.onClick.AddListener(OnSendButtonClick);
}
// 添加打开关闭按钮点击事件监听器
if (openAboutBtn != null)
{
openAboutBtn.onClick.AddListener(() => ObjIsOpen(aboutPanel, true));
}
if (closeBtn != null)
{
closeBtn.onClick.AddListener(() => ObjIsOpen(aboutPanel, false));
}
// 添加录音按钮事件监听器
if (recordButton != null && audioRecorder != null)
{
// 获取或添加EventTrigger组件
EventTrigger eventTrigger = recordButton.GetComponent<EventTrigger>();
if (eventTrigger == null)
{
eventTrigger = recordButton.gameObject.AddComponent<EventTrigger>();
}
// 清除现有的事件
eventTrigger.triggers.Clear();
// 添加PointerDown事件
EventTrigger.Entry pointerDownEntry = new EventTrigger.Entry();
pointerDownEntry.eventID = EventTriggerType.PointerDown;
pointerDownEntry.callback.AddListener((data) => { audioRecorder.StartRecording(); });
pointerDownEntry.callback.AddListener((data) => { UpdateRecordButtonState(true); });
eventTrigger.triggers.Add(pointerDownEntry);
// 添加PointerUp事件
EventTrigger.Entry pointerUpEntry = new EventTrigger.Entry();
pointerUpEntry.eventID = EventTriggerType.PointerUp;
pointerUpEntry.callback.AddListener((data) => { audioRecorder.StopRecordingAndTranscribe(); });
pointerUpEntry.callback.AddListener((data) => { UpdateRecordButtonState(false); });
eventTrigger.triggers.Add(pointerUpEntry);
}
// 初始化滚动视图文本
if (CallBackText != null)
{
CallBackText.text = "";
}
ObjIsOpen(aboutPanel, false);
logoImg.AddComponent<TextureLoader>();
bgImg.AddComponent<TextureLoader>();
// 加载图片
StartCoroutine(LoadImagesWhenReady());
// 加载文本
StartCoroutine(LoadTextWhenReady());
}
private IEnumerator LoadImagesWhenReady()
{
// 等待直到配置服务中的图片字段有内容
while (string.IsNullOrEmpty(cozeBotConfigService.logoImg) || string.IsNullOrEmpty(cozeBotConfigService.bgImg))
{
yield return null; // 等待下一帧
}
// 配置已准备好,加载图片
logoImg.GetComponent<TextureLoader>().LoadTextureToRawImage(cozeBotConfigService.logoImg, logoImg);
bgImg.GetComponent<TextureLoader>().LoadTextureToRawImage(cozeBotConfigService.bgImg, bgImg);
}
private IEnumerator LoadTextWhenReady()
{
// 等待直到配置服务中的文本字段有内容
while (string.IsNullOrEmpty(cozeBotConfigService.botName) || string.IsNullOrEmpty(cozeBotConfigService.aboutText))
{
yield return null; // 等待下一帧
}
// 配置已准备好,加载文本
TitleText.text = cozeBotConfigService.botName;
AboutText.text = cozeBotConfigService.aboutText;
}
// 发送按钮点击事件
private void OnSendButtonClick()
{
if (TextInput != null && !string.IsNullOrEmpty(TextInput.text))
{
string message = TextInput.text;
// 在滚动视图中显示用户输入
AppendToScrollView("You: " + message);
// 清空输入框
TextInput.text = "";
SendMsg(message);
}
}
public void SendMsg(string message)
{
// 通过CozePostAPI发送消息
if (cozeAPI != null)
{
StartCoroutine(cozeAPI.SendMsg(message));
}
}
// 向滚动视图添加文本
public void AppendToScrollView(string text)
{
if (CallBackText != null)
{
CallBackText.text += text + "\n";
// 延迟更新滚动位置,确保文本完全添加
StartCoroutine(UpdateScrollPosition());
}
}
// 更新滚动视图位置到底部
private IEnumerator UpdateScrollPosition()
{
yield return null;
if (CallBackscrollRect != null)
{
CallBackscrollRect.verticalNormalizedPosition = 0f;
}
}
//面板互动的方法
private void ObjIsOpen(GameObject obj,bool isOpen)
{
obj.SetActive(isOpen);
}
// 接收消息的方法,供外部调用
public void ReceiveMessage(string message)
{
AppendToScrollView("Coze: " + message);
}
/// <summary>
/// 更新录音按钮状态
/// </summary>
/// <param name="isRecording">是否正在录音</param>
public void UpdateRecordButtonState(bool isRecording)
{
if (recordButton != null)
{
ColorBlock colors = recordButton.colors;
if (isRecording)
{
recordButton.GetComponentInChildren<Text>().text = "松开识别";
}
else
{
recordButton.GetComponentInChildren<Text>().text = "按住说话";
}
recordButton.colors = colors;
}
}
/// <summary>
/// 实现IPointerDownHandler接口
/// </summary>
public void OnPointerDown(PointerEventData eventData)
{
// 这个方法主要是为了满足接口要求,实际的按钮事件通过EventTrigger处理
}
/// <summary>
/// 实现IPointerUpHandler接口
/// </summary>
public void OnPointerUp(PointerEventData eventData)
{
// 这个方法主要是为了满足接口要求,实际的按钮事件通过EventTrigger处理
}
}
AudioRecorder
csharp
using UnityEngine;
using System.Collections;
using System.IO;
public class AudioRecorder : MonoBehaviour
{
[Header("录音设置")]
public string deviceName = ""; // 麦克风设备名称
public int sampleRate = 44100; // 采样率
public int channels = 1; // 声道数
[Header("文件设置")]
public string tempFilePath = "temp_recording.wav"; // 临时音频文件路径
private AudioClip recordingClip; // 录制的音频片段
private bool isRecording = false; // 是否正在录音
private CozeSTTService cozeSTTService; // 音频转录服务
private UIManager uiManager; // UI管理器
private void Start()
{
// 获取CozeAudioTranscription服务
cozeSTTService = FindObjectOfType<CozeSTTService>();
if (cozeSTTService == null)
{
Debug.LogError("CozeSTTService服务未找到,请确保场景中有该组件");
}
// 获取UIManager
uiManager = FindObjectOfType<UIManager>();
if (uiManager == null)
{
Debug.LogError("UIManager未找到,请确保场景中有该组件");
}
// 获取默认麦克风设备名称
if (string.IsNullOrEmpty(deviceName) && Microphone.devices.Length > 0)
{
deviceName = Microphone.devices[0];
Debug.Log($"使用默认麦克风设备: {deviceName}");
}
}
/// <summary>
/// 开始录音
/// </summary>
public void StartRecording()
{
if (isRecording)
{
Debug.LogWarning("已经在录音中");
return;
}
if (string.IsNullOrEmpty(deviceName))
{
Debug.LogError("未找到麦克风设备");
return;
}
Debug.Log("开始录音...");
// 开始录音
recordingClip = Microphone.Start(deviceName, false, 60, sampleRate); // 最大录音时长60秒
isRecording = true;
}
/// <summary>
/// 停止录音并发送到转录API
/// </summary>
public void StopRecordingAndTranscribe()
{
if (!isRecording)
{
Debug.LogWarning("没有在录音中");
return;
}
Debug.Log("停止录音...");
// 停止录音
int position = Microphone.GetPosition(deviceName);
Microphone.End(deviceName);
isRecording = false;
// 裁剪音频片段到实际录制长度
AudioClip trimmedClip = AudioClip.Create("TrimmedRecording", position, channels, sampleRate, false);
float[] data = new float[position];
recordingClip.GetData(data, 0);
trimmedClip.SetData(data, 0);
// 保存音频为WAV文件
StartCoroutine(SaveAudioClipAsWav(trimmedClip, tempFilePath, (savedFilePath) => {
if (!string.IsNullOrEmpty(savedFilePath))
{
// 发送到转录API
SendToTranscriptionAPI(savedFilePath);
}
}));
}
/// <summary>
/// 保存AudioClip为WAV文件
/// </summary>
private IEnumerator SaveAudioClipAsWav(AudioClip clip, string filePath, System.Action<string> callback)
{
// 确保目录存在
string directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 转换为WAV格式并保存
yield return StartCoroutine(EncodeAndSaveWav(clip, filePath));
if (File.Exists(filePath))
{
Debug.Log($"音频文件保存成功: {filePath}");
callback?.Invoke(filePath);
}
else
{
Debug.LogError("音频文件保存失败");
callback?.Invoke(null);
}
}
/// <summary>
/// 编码并保存WAV文件
/// </summary>
private IEnumerator EncodeAndSaveWav(AudioClip clip, string filePath)
{
// 获取音频数据
float[] samples = new float[clip.samples * clip.channels];
clip.GetData(samples, 0);
// 转换为16位PCM数据
short[] pcmData = new short[samples.Length];
for (int i = 0; i < samples.Length; i++)
{
pcmData[i] = (short)(samples[i] * 32767);
}
// 写入WAV文件
using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fileStream))
{
// RIFF头
writer.Write(new char[] { 'R', 'I', 'F', 'F' });
writer.Write(36 + pcmData.Length * 2);
writer.Write(new char[] { 'W', 'A', 'V', 'E' });
// fmt块
writer.Write(new char[] { 'f', 'm', 't', ' ' });
writer.Write(16);
writer.Write((short)1); // PCM格式
writer.Write((short)clip.channels);
writer.Write(clip.frequency);
writer.Write(clip.frequency * clip.channels * 2); // 字节率
writer.Write((short)(clip.channels * 2)); // 块对齐
writer.Write((short)16); // 位深度
// data块
writer.Write(new char[] { 'd', 'a', 't', 'a' });
writer.Write(pcmData.Length * 2);
// 写入PCM数据
for (int i = 0; i < pcmData.Length; i++)
{
writer.Write(pcmData[i]);
}
}
yield return null;
}
/// <summary>
/// 发送音频文件到转录API
/// </summary>
private void SendToTranscriptionAPI(string audioFilePath)
{
if (cozeSTTService == null)
{
Debug.LogError("CozeAudioTranscription服务未找到,无法发送转录请求");
return;
}
Debug.Log($"发送音频到转录API: {audioFilePath}");
// 调用转录服务
cozeSTTService.TranscribeAudio(audioFilePath, (transcriptionText) => {
if (!string.IsNullOrEmpty(transcriptionText))
{
Debug.Log($"转录结果: {transcriptionText}");
// 显示转录结果到UI
if (uiManager != null)
{
uiManager.AppendToScrollView("You: " + transcriptionText);
uiManager.SendMsg(transcriptionText);
}
}
else
{
Debug.LogError("转录失败");
if (uiManager != null)
{
uiManager.AppendToScrollView("Transcription failed: 无法识别音频内容");
}
}
// 转录完成后删除临时文件
try
{
if (File.Exists(audioFilePath))
{
File.Delete(audioFilePath);
Debug.Log($"临时文件已删除: {audioFilePath}");
}
}
catch (System.Exception ex)
{
Debug.LogError($"删除临时文件失败: {ex.Message}");
}
});
}
/// <summary>
/// 获取可用的麦克风设备列表
/// </summary>
public void ListMicrophoneDevices()
{
if (Microphone.devices.Length > 0)
{
Debug.Log("可用的麦克风设备:");
for (int i = 0; i < Microphone.devices.Length; i++)
{
Debug.Log($"{i}: {Microphone.devices[i]}");
}
}
else
{
Debug.LogError("未找到可用的麦克风设备");
}
}
}
CozeSTTService
csharp
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using Newtonsoft.Json.Linq;
using System.IO;
public class CozeSTTService : MonoBehaviour
{
private string token;
private string apiUrl = "https://api.coze.cn/v1/audio/transcriptions";
private CozeBotConfigService configService;
private void Start()
{
// 从CozeBotConfigService获取token
configService = FindObjectOfType<CozeBotConfigService>();
token = configService.token;
}
/// <summary>
/// 发送音频文件进行转录
/// </summary>
/// <param name="audioFilePath">音频文件路径</param>
/// <param name="callback">转录完成后的回调,参数为转录文本</param>
public void TranscribeAudio(string audioFilePath, System.Action<string> callback)
{
StartCoroutine(TranscribeAudioCoroutine(audioFilePath, callback));
}
/// <summary>
/// 发送音频文件进行转录的协程
/// </summary>
private IEnumerator TranscribeAudioCoroutine(string audioFilePath, System.Action<string> callback)
{
if (string.IsNullOrEmpty(audioFilePath))
{
Debug.LogError("音频文件路径为空");
callback?.Invoke(null);
yield break;
}
if (!File.Exists(audioFilePath))
{
Debug.LogError($"音频文件不存在: {audioFilePath}");
callback?.Invoke(null);
yield break;
}
if (string.IsNullOrEmpty(token))
{
Debug.LogError("Token为空");
callback?.Invoke(null);
yield break;
}
// 创建表单数据
WWWForm form = new WWWForm();
// 读取音频文件
byte[] audioData = File.ReadAllBytes(audioFilePath);
// 添加文件到表单
form.AddBinaryData("file", audioData, Path.GetFileName(audioFilePath), "audio/mpeg");
// 创建UnityWebRequest
using (UnityWebRequest request = UnityWebRequest.Post(apiUrl, form))
{
// 设置Authorization头
request.SetRequestHeader("Authorization", "Bearer " + token);
// 发送请求
yield return request.SendWebRequest();
// 处理响应
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log("转录请求成功: " + request.downloadHandler.text);
// 解析响应
string transcriptionText = ParseTranscriptionResponse(request.downloadHandler.text);
callback?.Invoke(transcriptionText);
}
else
{
Debug.LogError($"转录请求失败: {request.error}");
Debug.LogError($"响应内容: {request.downloadHandler.text}");
callback?.Invoke(null);
}
}
}
/// <summary>
/// 解析转录响应
/// </summary>
/// <param name="responseText">响应文本</param>
/// <returns>转录文本</returns>
private string ParseTranscriptionResponse(string responseText)
{
try
{
JObject jsonObject = JObject.Parse(responseText);
// 检查code是否为0
if (jsonObject["code"] != null && jsonObject["code"].ToString() == "0")
{
// 提取data中的text字段
if (jsonObject["data"] != null && jsonObject["data"]["text"] != null)
{
return jsonObject["data"]["text"].ToString();
}
else
{
Debug.LogError("响应中缺少data.text字段");
}
}
else
{
string errorMsg = jsonObject["msg"]?.ToString() ?? "未知错误";
Debug.LogError($"转录失败: {errorMsg}");
}
}
catch (System.Exception ex)
{
Debug.LogError($"解析响应失败: {ex.Message}");
}
return null;
}
}