这里我们需要在火山云语音控制台开通大模型的流式语音对话、获取豆包模型的apiKey,开通语音合成项目。
这里使用的豆包模型是Doubao-lite,延迟会更低一些
配置说明
这里一共有四个文件,分别是主要的fastAPI、LLM、STT、文件
TTS中需要配置
appid = "123" #填写控制台的APPID
token = "XXXX" #填写控制台上的Access Token
cluster = "XXXXX" #填写语音生成的组id
voice_type = "BV034_streaming" #这里是生成声音的类型选择
host = "openspeech.bytedance.com" #无需更改
api_url = f"wss://{host}/api/v1/tts/ws_binary" #无需更改
LLM中配置
# 初始化客户端,传入 API 密钥
self.client = Ark(api_key="XXXX")
在STT的146行中配置
header = {
"X-Api-Resource-Id": "volc.bigasr.sauc.duration",
"X-Api-Access-Key": "XXXXX", #和TTS配置内容相同
"X-Api-App-Key": "123", #和TTS配置内容相同
"X-Api-Request-Id": reqid
}
还有前端HTML的配置中记得根据自己服务的所在ip更改配置
前端测试html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket 音频传输测试</title>
<style>
body {
font-family: Arial, sans-serif;
}
#status {
margin-bottom: 10px;
}
#messages {
border: 1px solid #ccc;
height: 200px;
overflow-y: scroll;
padding: 10px;
}
#controls {
margin-top: 10px;
}
#controls button {
margin-right: 5px;
}
#latency {
margin-top: 10px;
font-weight: bold;
}
</style>
</head>
<body>
<h1>WebSocket 音频传输测试</h1>
<!-- 显示当前连接状态 -->
<div id="status">状态:未连接</div>
<!-- 显示日志消息 -->
<div id="messages"></div>
<!-- 控制按钮 -->
<div id="controls">
<button id="startButton">开始录音并发送</button>
<button id="stopButton" disabled>停止录音</button>
</div>
<!-- 延迟显示区域 -->
<div id="latency"></div>
<script>
// WebSocket 服务器地址,请根据实际情况替换
const wsUrl = 'ws://127.0.0.1:8000/ws';
// 全局变量
let socket = null; // WebSocket 实例
const messagesDiv = document.getElementById('messages'); // 日志消息显示区域
const statusDiv = document.getElementById('status'); // 连接状态显示区域
const startButton = document.getElementById('startButton'); // 开始录音按钮
const stopButton = document.getElementById('stopButton'); // 停止录音按钮
let recordingAudioContext; // 音频录制上下文
let audioInput; // 音频输入节点
let processor; // 音频处理节点
// 播放相关变量
let playbackAudioContext;
let playbackQueue = [];
let playbackTime = 0;
let isPlaying = false;
// 延迟测量变量
let overSentTime = null; // 记录发送 'over' 的时间
let latencyMeasured = false; // 标记是否已经测量延迟
/**
* 向日志区域添加消息
* @param {string} message - 要记录的消息
*/
function logMessage(message) {
const p = document.createElement('p');
p.textContent = message;
messagesDiv.appendChild(p);
messagesDiv.scrollTop = messagesDiv.scrollHeight; // 自动滚动到最新消息
}
/**
* 初始化Playback AudioContext
*/
function initializePlayback() {
playbackAudioContext = new (window.AudioContext || window.webkitAudioContext)();
logMessage('Playback AudioContext 已创建');
}
/**
* 解码并添加到播放队列
* @param {ArrayBuffer} data - 接收到的音频数据
*/
function appendToPlaybackQueue(data) {
playbackAudioContext.decodeAudioData(data, (audioBuffer) => {
playbackQueue.push(audioBuffer);
schedulePlayback();
}, (error) => {
logMessage('解码音频数据时出错:' + error);
});
}
/**
* 调度播放队列中的音频缓冲区
*/
function schedulePlayback() {
if (isPlaying) return;
if (playbackQueue.length === 0) return;
// 获取下一个缓冲区
const buffer = playbackQueue.shift();
// 创建一个缓冲源
const source = playbackAudioContext.createBufferSource();
source.buffer = buffer;
source.connect(playbackAudioContext.destination);
// 如果 playbackTime 小于当前时间,则更新为当前时间
if (playbackTime < playbackAudioContext.currentTime) {
playbackTime = playbackAudioContext.currentTime;
}
// 计划在 playbackTime 播放
source.start(playbackTime);
logMessage(`Scheduled buffer to play at ${playbackTime.toFixed(2)}s`);
// 更新 playbackTime
playbackTime += buffer.duration;
// 标记为正在播放
isPlaying = true;
// 当缓冲源播放结束时
source.onended = () => {
isPlaying = false;
// 继续播放队列中的下一个缓冲区
schedulePlayback();
};
}
/**
* 创建并连接 WebSocket
*/
function createWebSocket() {
if (socket !== null && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
logMessage('WebSocket 已经连接或正在连接中');
return;
}
socket = new WebSocket(wsUrl);
socket.binaryType = 'arraybuffer';
socket.onopen = function () {
statusDiv.textContent = '状态:已连接';
logMessage('WebSocket 连接已打开');
startButton.disabled = false; // 启用开始录音按钮
};
socket.onmessage = function (event) {
// 如果接收到的是字符串且内容为 'over'
if (typeof event.data === 'string' && event.data === 'over') {
logMessage('收到结束信号: over');
// 标记 MediaSource 结束
return;
}
// 如果接收到的是二进制数据(ArrayBuffer)
if (event.data instanceof ArrayBuffer) {
logMessage('接收到音频数据');
// 检查是否已经发送 'over' 并且尚未测量延迟
if (overSentTime !== null && !latencyMeasured) {
let receiveTime = performance.now();
let latency = receiveTime - overSentTime;
logMessage(`延迟时间:${latency.toFixed(2)} 毫秒`);
document.getElementById('latency').textContent = `延迟时间:${latency.toFixed(2)} 毫秒`;
latencyMeasured = true; // 标记为已测量
}
appendToPlayback