本文档详细分析项目开发过程中的核心技术重点和难点,为技术实现提供指导方案。
| 技术挑战 | 难度等级 | 优先级 | 对系统影响 |
|---|---|---|---|
| 实时流式音频传输 | ⭐⭐⭐⭐⭐ | P0 | 核心功能 |
| 0.3秒超低延迟 | ⭐⭐⭐⭐⭐ | P0 | 用户体验 |
| 豆包API连接池 | ⭐⭐⭐⭐ | P0 | 性能关键 |
| 音频格式实时转换 | ⭐⭐⭐⭐ | P0 | 技术基础 |
| 浏览器兼容性 | ⭐⭐⭐ | P1 | 应用范围 |
| 状态同步机制 | ⭐⭐⭐ | P1 | 交互体验 |
核心要求:
难点描述:
解决方案:
// 1. 使用AudioWorklet实现实时音频处理
class PCMProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
const input = inputs[0][0]; // 单声道
// 实时转换为PCM int16
const pcmData = this.floatToPCM16(input);
// 立即发送到主线程
this.port.postMessage({
type: 'audio_chunk',
data: pcmData
});
return true;
}
floatToPCM16(float32Array) {
const int16Array = new Int16Array(float32Array.length);
for (let i = 0; i < float32Array.length; i++) {
const s = Math.max(-1, Math.min(1, float32Array[i]));
int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
return int16Array;
}
}
// 2. 主线程实时发送音频数据
audioWorkletNode.port.onmessage = (event) => {
if (event.data.type === 'audio_chunk') {
// 立即通过WebSocket发送
websocket.send(event.data.data.buffer);
}
};
关键参数配置:
音频采集参数:
{
sampleRate: 16000, // 16kHz采样率
channelCount: 1, // 单声道
echoCancellation: true, // 回声消除
noiseSuppression: true, // 降噪
autoGainControl: true // 自动增益
}
难点描述:
解决方案:
# 使用异步架构实现流式转发
import asyncio
import websockets
class StreamingSession:
def __init__(self, client_ws, doubao_client):
self.client_ws = client_ws
self.doubao_client = doubao_client
self.is_streaming = False
async def start_streaming(self):
"""启动流式传输"""
self.is_streaming = True
# 并发处理:同时接收客户端数据和转发给豆包
await asyncio.gather(
self.receive_from_client(),
self.send_to_doubao(),
self.receive_from_doubao(),
self.send_to_client()
)
async def receive_from_client(self):
"""实时接收客户端音频流"""
async for message in self.client_ws:
if self.is_streaming:
# 立即放入发送队列
await self.to_doubao_queue.put(message)
async def send_to_doubao(self):
"""实时转发给豆包API"""
while self.is_streaming:
audio_chunk = await self.to_doubao_queue.get()
# 立即发送给豆包,不等待
await self.doubao_client.send_audio(audio_chunk)
关键设计:
asyncio.Queue缓冲音频块难点描述:
解决方案:
// 使用MediaSource API实现流式播放
class StreamingAudioPlayer {
constructor() {
this.mediaSource = new MediaSource();
this.audio = new Audio();
this.audio.src = URL.createObjectURL(this.mediaSource);
this.sourceBuffer = null;
this.queue = [];
}
async init() {
return new Promise((resolve) => {
this.mediaSource.addEventListener('sourceopen', () => {
// 配置为OGG/Opus格式
this.sourceBuffer = this.mediaSource.addSourceBuffer('audio/ogg; codecs=opus');
this.sourceBuffer.addEventListener('updateend', () => {
if (this.queue.length > 0 && !this.sourceBuffer.updating) {
this.sourceBuffer.appendBuffer(this.queue.shift());
}
});
resolve();
});
});
}
// 实时追加音频数据
appendAudioChunk(chunk) {
if (!this.sourceBuffer.updating) {
this.sourceBuffer.appendBuffer(chunk);
} else {
this.queue.push(chunk);
}
// 自动开始播放(处理首包)
if (this.audio.paused && this.sourceBuffer.buffered.length > 0) {
this.audio.play();
}
}
}
[前端]
按下按钮 → 开始拾音(0ms)
↓
采集音频块(8ms) → 转PCM → WSS发送(立即)
↓ ↓
采集音频块(16ms) → 转PCM → WSS发送(立即)
↓ ↓
持续循环...直到松开按钮
[后端]
接收音频块1 → 转发豆包API(立即)
↓
接收音频块2 → 转发豆包API(立即)
↓
持续转发...
↓
接收豆包返回 → WSS发送客户端(立即)
↓
边接收边发送...
[前端]
接收音频块1 → 追加到播放器 → 自动播放
↓
接收音频块2 → 追加到播放器
↓
持续播放...
| 指标 | 目标值 | 说明 |
|---|---|---|
| 前端采集延迟 | < 10ms | AudioWorklet处理延迟 |
| 单次传输延迟 | < 5ms | WebSocket发送延迟 |
| 后端转发延迟 | < 10ms | 接收后立即转发 |
| 首包播放延迟 | < 50ms | 收到第一个音频包到播放 |
| 端到端延迟 | < 300ms | 用户停止说话到听到回复 |
总延迟 = 前端采集延迟 + 网络上行延迟 + 后端处理延迟 +
豆包API延迟 + 网络下行延迟 + 前端播放延迟
详细分解:
| 环节 | 预估延迟 | 优化后目标 | 优化策略 |
|---|---|---|---|
| 前端音频采集 | 50-100ms | 10-20ms | AudioWorklet最小缓冲 |
| 网络上行 | 20-50ms | 10-20ms | WSS + CDN加速 |
| 后端转发 | 20-30ms | 5-10ms | 异步零拷贝转发 |
| 连接建立 | 100-200ms | 0ms | 连接池预热 |
| 豆包API处理 | 100-150ms | 100-150ms | 无法优化 |
| 网络下行 | 20-50ms | 10-20ms | WSS + CDN加速 |
| 前端播放准备 | 30-50ms | 10-20ms | MediaSource预加载 |
| 总计 | 340-630ms | 145-240ms | 满足0.3秒要求 |
问题分析:
解决方案:预建立连接池
import asyncio
from asyncio import Queue
from typing import List, Optional
import time
class DoubaoConnectionPool:
"""豆包API连接池"""
def __init__(self, pool_size=10, max_idle_time=300):
self.pool_size = pool_size
self.max_idle_time = max_idle_time # 最大空闲时间5分钟
# 空闲连接队列
self.idle_connections: Queue = Queue(maxsize=pool_size)
# 活跃连接集合
self.active_connections: set = set()
# 连接健康状态
self.connection_health: dict = {}
async def initialize(self):
"""初始化连接池 - 预创建所有连接"""
print(f"初始化连接池,创建 {self.pool_size} 个连接...")
tasks = []
for i in range(self.pool_size):
task = asyncio.create_task(self._create_connection(i))
tasks.append(task)
connections = await asyncio.gather(*tasks)
for conn in connections:
if conn:
await self.idle_connections.put(conn)
self.connection_health[id(conn)] = {
'created_at': time.time(),
'last_used': time.time(),
'request_count': 0
}
print(f"连接池初始化完成,可用连接: {self.idle_connections.qsize()}")
# 启动健康检查任务
asyncio.create_task(self._health_check_loop())
async def _create_connection(self, index):
"""创建单个豆包API连接"""
try:
# 这里使用实际的豆包API连接逻辑
from realtime_dialog_client import RealtimeDialogClient
client = RealtimeDialogClient()
await client.connect()
print(f"连接 #{index} 创建成功")
return client
except Exception as e:
print(f"连接 #{index} 创建失败: {e}")
return None
async def acquire(self, timeout=5) -> Optional:
"""获取一个可用连接"""
try:
# 从空闲队列获取连接,超时5秒
connection = await asyncio.wait_for(
self.idle_connections.get(),
timeout=timeout
)
# 移动到活跃集合
self.active_connections.add(connection)
# 更新健康状态
conn_id = id(connection)
if conn_id in self.connection_health:
self.connection_health[conn_id]['last_used'] = time.time()
self.connection_health[conn_id]['request_count'] += 1
return connection
except asyncio.TimeoutError:
print("警告: 连接池已耗尽,等待超时")
return None
async def release(self, connection):
"""归还连接到池中"""
if connection in self.active_connections:
self.active_connections.remove(connection)
# 检查连接是否健康
if await self._is_connection_healthy(connection):
await self.idle_connections.put(connection)
else:
# 连接不健康,重新创建
print(f"连接不健康,重新创建...")
new_conn = await self._create_connection(-1)
if new_conn:
await self.idle_connections.put(new_conn)
async def _is_connection_healthy(self, connection) -> bool:
"""检查连接健康状态"""
try:
# 检查连接是否还活着
if not connection or not connection.is_connected():
return False
# 检查空闲时间
conn_id = id(connection)
if conn_id in self.connection_health:
idle_time = time.time() - self.connection_health[conn_id]['last_used']
if idle_time > self.max_idle_time:
return False
return True
except:
return False
async def _health_check_loop(self):
"""定期健康检查"""
while True:
await asyncio.sleep(60) # 每分钟检查一次
# 检查空闲连接
print(f"健康检查: 空闲={self.idle_connections.qsize()}, "
f"活跃={len(self.active_connections)}")
# 全局连接池实例
connection_pool = DoubaoConnectionPool(pool_size=10)
class WSServerHandler:
"""WebSocket服务处理器"""
async def handle_client(self, websocket, path):
"""处理客户端连接"""
doubao_client = None
try:
# 从连接池获取豆包连接(几乎零延迟)
doubao_client = await connection_pool.acquire()
if not doubao_client:
await websocket.send(json.dumps({
'type': 'error',
'message': '服务繁忙,请稍后重试'
}))
return
# 创建会话
session = StreamingSession(websocket, doubao_client)
# 处理音频流
await session.start_streaming()
finally:
# 归还连接到池中
if doubao_client:
await connection_pool.release(doubao_client)
| 对比项 | 无连接池 | 有连接池 |
|---|---|---|
| 连接建立时间 | 100-200ms | 0ms(已建立) |
| 首次响应延迟 | 400-600ms | 200-300ms |
| 并发能力 | 受限于连接速度 | 池大小决定 |
| 资源利用 | 每次建立/销毁 | 复用 |
| 稳定性 | 受网络波动影响 | 健康检查保障 |
// 1. WebSocket配置优化
const ws = new WebSocket('wss://api.example.com', {
perMessageDeflate: false, // 禁用压缩,减少CPU时间
maxPayload: 1024 * 1024 // 限制消息大小
});
// 2. 使用二进制传输(不用JSON包装)
ws.binaryType = 'arraybuffer';
ws.send(pcmData.buffer); // 直接发送二进制
// 使用最小化的音频缓冲
const audioContext = new AudioContext({
latencyHint: 'interactive', // 交互模式,最低延迟
sampleRate: 16000
});
// AudioWorklet配置
await audioContext.audioWorklet.addModule('processor.js');
const workletNode = new AudioWorkletNode(audioContext, 'pcm-processor', {
processorOptions: {
bufferSize: 128 // 最小缓冲
}
});
# 避免不必要的数据拷贝
async def forward_audio(client_data):
# 直接转发,不做额外处理
await doubao_client.send(client_data) # 零拷贝
# 错误示例(增加延迟):
# decoded = decode(client_data) # 不必要的解码
# processed = process(decoded) # 不必要的处理
# await doubao_client.send(encode(processed))
上行转换(浏览器 → 豆包):
浏览器原始格式(Float32) → PCM Int16 Little-Endian
- 采样率: 保持16000Hz
- 声道: 单声道
- 位深: 32位浮点 → 16位整数
- 字节序: 转为小端序
下行处理(豆包 → 浏览器):
OGG/Opus格式 → 浏览器直接播放
- 无需转换,浏览器原生支持
- 直接使用MediaSource API
// pcm-processor.js - 在独立线程中处理
class PCMProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.bufferSize = 512;
this.buffer = new Float32Array(this.bufferSize);
this.bufferIndex = 0;
}
process(inputs, outputs, parameters) {
const input = inputs[0];
if (input.length > 0) {
const channelData = input[0]; // 取第一声道
for (let i = 0; i < channelData.length; i++) {
this.buffer[this.bufferIndex++] = channelData[i];
// 缓冲区满了,转换并发送
if (this.bufferIndex >= this.bufferSize) {
this.convertAndSend();
this.bufferIndex = 0;
}
}
}
return true;
}
convertAndSend() {
// Float32 → Int16 转换
const int16Array = new Int16Array(this.bufferSize);
for (let i = 0; i < this.bufferSize; i++) {
// 将 -1.0 到 1.0 的浮点数转换为 -32768 到 32767 的整数
let sample = this.buffer[i];
// 限幅
sample = Math.max(-1, Math.min(1, sample));
// 转换为16位整数
int16Array[i] = sample < 0
? sample * 0x8000 // -32768
: sample * 0x7FFF; // 32767
}
// 发送到主线程(已经是小端序)
this.port.postMessage({
type: 'pcm_data',
data: int16Array.buffer
}, [int16Array.buffer]); // 转移所有权,避免拷贝
}
}
registerProcessor('pcm-processor', PCMProcessor);
// main.js
class AudioStreamer {
constructor(websocket) {
this.ws = websocket;
this.workletNode = null;
}
async initialize() {
// 创建音频上下文
this.audioContext = new AudioContext({
sampleRate: 16000,
latencyHint: 'interactive'
});
// 加载AudioWorklet
await this.audioContext.audioWorklet.addModule('pcm-processor.js');
// 创建处理节点
this.workletNode = new AudioWorkletNode(
this.audioContext,
'pcm-processor'
);
// 监听PCM数据
this.workletNode.port.onmessage = (event) => {
if (event.data.type === 'pcm_data') {
// 直接通过WebSocket发送二进制数据
this.ws.send(event.data.data);
}
};
}
async startStreaming() {
// 获取麦克风
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1,
sampleRate: 16000,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
// 连接音频流
const source = this.audioContext.createMediaStreamSource(stream);
source.connect(this.workletNode);
// 不连接到destination,避免回声
}
}
// 检测浏览器能力
async function createAudioStreamer() {
if (window.AudioWorklet) {
// 现代浏览器:使用AudioWorklet
return new AudioWorkletStreamer();
} else if (window.ScriptProcessorNode) {
// 旧浏览器:使用ScriptProcessor(已废弃但兼容)
return new ScriptProcessorStreamer();
} else {
// 最低降级:使用MediaRecorder
return new MediaRecorderStreamer();
}
}
// ScriptProcessor降级方案(延迟稍高)
class ScriptProcessorStreamer {
startStreaming() {
const processor = audioContext.createScriptProcessor(2048, 1, 1);
processor.onaudioprocess = (e) => {
const inputData = e.inputBuffer.getChannelData(0);
const pcm = this.floatToPCM16(inputData);
this.ws.send(pcm.buffer);
};
}
}
[等待状态 IDLE]
↓ (用户按下按钮)
[倾听状态 LISTENING]
↓ (用户松开按钮)
[说话状态 SPEAKING]
↓ (音频播放完成)
[等待状态 IDLE]
// 使用状态机管理
class AnimationStateManager {
constructor(animationComponent) {
this.animation = animationComponent;
this.state = 'IDLE';
}
// 状态转换
transition(newState, reason) {
console.log(`状态转换: ${this.state} → ${newState} (${reason})`);
const oldState = this.state;
this.state = newState;
// 触发动画切换
this.animation.setState(newState);
// 触发事件
this.emit('stateChange', { oldState, newState, reason });
}
// 按钮按下
onButtonDown() {
if (this.state === 'IDLE') {
this.transition('LISTENING', '用户开始说话');
}
}
// 按钮松开
onButtonUp() {
if (this.state === 'LISTENING') {
this.transition('SPEAKING', '用户停止说话,等待AI回复');
}
}
// 音频播放完成
onAudioEnded() {
if (this.state === 'SPEAKING') {
this.transition('IDLE', 'AI回复完成');
}
}
// 错误处理
onError(error) {
this.transition('IDLE', `错误: ${error.message}`);
}
}
// 使用Lottie或Spine实现动画
class AnimatedIP {
constructor(container) {
this.animations = {
idle: lottie.loadAnimation({
container: container,
path: 'animations/idle.json',
loop: true,
autoplay: false
}),
listening: lottie.loadAnimation({
container: container,
path: 'animations/listening.json',
loop: true,
autoplay: false
}),
speaking: lottie.loadAnimation({
container: container,
path: 'animations/speaking.json',
loop: true,
autoplay: false
})
};
this.currentState = 'idle';
}
setState(state) {
// 停止当前动画
this.animations[this.currentState].stop();
this.animations[this.currentState].hide();
// 播放新动画
this.currentState = state.toLowerCase();
this.animations[this.currentState].show();
this.animations[this.currentState].play();
}
}
| 功能 | Chrome | Safari | Firefox | Edge | 移动Safari | 解决方案 |
|---|---|---|---|---|---|---|
| AudioWorklet | ✅ 66+ | ✅ 14.1+ | ✅ 76+ | ✅ 79+ | ✅ 14.5+ | ScriptProcessor降级 |
| MediaSource | ✅ | ✅ | ✅ | ✅ | ⚠️ 部分支持 | Audio元素降级 |
| WebSocket | ✅ | ✅ | ✅ | ✅ | ✅ | 无需降级 |
| HTTPS麦克风 | ✅ | ✅ | ✅ | ✅ | ✅ | 必需HTTPS |
// iOS需要用户手势触发
class iOSAudioFix {
static async unlock() {
if (this.unlocked) return;
// 在用户交互中创建音频上下文
const context = new (window.AudioContext || window.webkitAudioContext)();
// 播放静音,解锁音频
const buffer = context.createBuffer(1, 1, 22050);
const source = context.createBufferSource();
source.buffer = buffer;
source.connect(context.destination);
source.start(0);
await context.resume();
this.unlocked = true;
}
}
// 在按钮点击时调用
button.addEventListener('click', async () => {
await iOSAudioFix.unlock();
// 然后开始录音
});
// 检测Android浏览器
const isAndroid = /Android/i.test(navigator.userAgent);
if (isAndroid) {
// 某些Android浏览器不支持echoCancellation
const audioConstraints = {
channelCount: 1,
sampleRate: 16000
// 不设置echoCancellation等参数
};
}
class BrowserCapability {
static async detect() {
const result = {
audioWorklet: !!window.AudioWorklet,
mediaSource: !!window.MediaSource,
webSocket: !!window.WebSocket,
getUserMedia: !!(navigator.mediaDevices?.getUserMedia)
};
// 测试实际能力
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(track => track.stop());
result.microphone = true;
} catch {
result.microphone = false;
}
return result;
}
static getRecommendedImplementation(capabilities) {
if (capabilities.audioWorklet && capabilities.mediaSource) {
return 'premium'; // 最佳体验
} else if (capabilities.getUserMedia) {
return 'standard'; // 标准体验
} else {
return 'basic'; // 基础体验
}
}
}
| 错误场景 | 发生概率 | 影响 | 处理策略 |
|---|---|---|---|
| 网络中断 | 中 | 高 | 自动重连 |
| 麦克风权限拒绝 | 高 | 致命 | 友好提示 |
| 豆包API超时 | 低 | 高 | 超时重试 |
| 音频格式错误 | 低 | 中 | 降级处理 |
| 连接池耗尽 | 中 | 高 | 排队等待 |
class ResilientWebSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket连接成功');
this.reconnectAttempts = 0;
this.onopen?.();
};
this.ws.onclose = (event) => {
console.log('WebSocket连接关闭', event.code);
if (!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) {
// 指数退避重连
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
console.log(`${delay}ms后重连...`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
} else {
this.onerror?.('连接失败,请刷新页面');
}
};
this.ws.onerror = (error) => {
console.error('WebSocket错误', error);
};
}
}
class TimeoutController {
async withTimeout(promise, timeoutMs, errorMsg) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(errorMsg)), timeoutMs)
)
]);
}
}
// 使用示例
try {
const connection = await timeoutController.withTimeout(
connectionPool.acquire(),
5000,
'获取连接超时'
);
} catch (error) {
// 处理超时
}
class PerformanceMonitor {
constructor() {
this.metrics = {
// 延迟指标
captureLatency: [], // 采集延迟
networkLatency: [], // 网络延迟
totalLatency: [], // 总延迟
// 质量指标
audioQuality: [], // 音频质量
packetLoss: 0, // 丢包率
// 资源指标
cpuUsage: [], // CPU使用率
memoryUsage: [] // 内存使用
};
}
recordLatency(type, value) {
this.metrics[`${type}Latency`].push(value);
// 实时检测性能问题
if (value > 300) {
console.warn(`${type}延迟过高: ${value}ms`);
this.triggerOptimization(type);
}
}
getStats() {
return {
avgTotalLatency: this.average(this.metrics.totalLatency),
p95Latency: this.percentile(this.metrics.totalLatency, 95),
packetLoss: this.metrics.packetLoss
};
}
average(arr) {
return arr.reduce((a, b) => a + b, 0) / arr.length;
}
percentile(arr, p) {
const sorted = arr.sort((a, b) => a - b);
const index = Math.ceil(sorted.length * p / 100) - 1;
return sorted[index];
}
}
class LatencyTracker {
startRecording() {
this.startTime = performance.now();
}
endRecording() {
const endTime = performance.now();
const latency = endTime - this.startTime;
performanceMonitor.recordLatency('total', latency);
return latency;
}
}
// 使用
const tracker = new LatencyTracker();
// 用户松开按钮
button.onmouseup = () => {
tracker.startRecording();
};
// 音频开始播放
audio.onplay = () => {
const latency = tracker.endRecording();
console.log(`端到端延迟: ${latency}ms`);
};
Tolan/
├── frontend/ # 前端代码目录
│ ├── src/
│ │ ├── components/ # React组件
│ │ ├── utils/ # 工具函数
│ │ ├── hooks/ # 自定义Hooks
│ │ ├── stores/ # Zustand状态管理
│ │ └── assets/ # 静态资源
│ ├── public/
│ │ └── animations/ # Lottie动画文件
│ ├── package.json
│ └── vite.config.ts
│
├── backend/ # 后端代码目录
│ ├── main.py # FastAPI主程序
│ ├── connection_pool.py # 豆包API连接池
│ ├── websocket_handler.py # WebSocket处理器
│ ├── config.py # 配置管理
│ └── requirements.txt # Python依赖
│
├── doubao-api/ # 豆包API集成
│ ├── realtime_dialog_client.py
│ └── protocol.py
│
├── ssl/ # SSL证书
│ ├── cert.pem
│ └── key.pem
│
└── 项目文档/ # 项目文档
├── 用户需求分析书.md
├── 项目开发重点和难点分析.md
├── 开源组件推荐清单.md
└── API文档.md
前端:
后端:
前端环境:
# 进入前端目录
cd frontend/
# 安装依赖
npm install
# 开发环境运行
npm run dev
# 生产构建
npm run build
后端环境:
# 进入后端目录
cd backend/
# 安装依赖(使用本机Python环境,无需虚拟环境)
pip install -r requirements.txt
# 开发环境运行
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# 生产环境运行(使用SSL)
uvicorn main:app --host 0.0.0.0 --port 8443 \
--ssl-keyfile=../ssl/key.pem \
--ssl-certfile=../ssl/cert.pem
豆包API配置:
# 配置环境变量(backend/.env)
DOUBAO_API_KEY=your_api_key
DOUBAO_APP_ID=your_app_id
CONNECTION_POOL_SIZE=10
Week 1: 基础架构
Week 2: 音频流实现
Week 3: 连接池实现
Week 4: 延迟优化
Week 5: 稳定性优化
Week 6: UI/UX
Week 7: 兼容性
1. 克隆项目并配置:
cd /Users/wuzilong/work/00code/Tolan
# 配置SSL证书(开发环境)
mkdir -p ssl
# 生成自签名证书或使用mkcert
mkcert -install
mkcert -key-file ssl/key.pem -cert-file ssl/cert.pem localhost 127.0.0.1
2. 启动后端服务:
# 进入后端目录
cd backend/
# 安装依赖(使用本机Python)
pip install fastapi uvicorn websockets python-dotenv loguru
# 配置环境变量
cp .env.example .env
# 编辑.env填入豆包API密钥
# 启动服务
python main.py
3. 启动前端服务:
# 新开终端,进入前端目录
cd frontend/
# 安装依赖
npm install
# 启动开发服务器
npm run dev
4. 访问应用:
前端: https://localhost:5173
后端API: https://localhost:8443
WebSocket: wss://localhost:8443/ws/realtime
// 自动化测试关键指标
describe('实时语音交互测试', () => {
test('端到端延迟 < 300ms', async () => {
const latency = await measureLatency();
expect(latency).toBeLessThan(300);
});
test('音频流式传输', async () => {
const chunks = await recordAudioChunks();
expect(chunks.length).toBeGreaterThan(10); // 确保是流式的
});
test('连接池可用性', async () => {
const conn = await connectionPool.acquire();
expect(conn).toBeDefined();
expect(conn.isConnected()).toBe(true);
});
});
| 风险 | 缓解措施 | 应急预案 |
|---|---|---|
| 无法达到0.3秒延迟 | 充分优化各环节 | 放宽至0.5秒 |
| 连接池实现复杂 | 参考成熟方案 | 使用第三方库 |
| 移动端兼容性差 | 提前测试 | 提供PC优先版本 |
| 豆包API不稳定 | 重试+超时 | 友好错误提示 |
本项目的核心技术挑战集中在实时性和低延迟上。成功的关键在于:
流式音频传输
连接池预热
音频格式优化
全链路优化
P0(必须):
P1(重要):
P2(优化):
代码目录:
/Users/wuzilong/work/00code/Tolan/frontend//Users/wuzilong/work/00code/Tolan/backend//Users/wuzilong/work/00code/Tolan/doubao-api//Users/wuzilong/work/00code/Tolan/项目文档/Python环境:
pip install -r backend/requirements.txt文档版本: v1.1
创建日期: 2025-10-08
最后更新: 2025-10-08
更新内容: 添加项目结构、环境配置和快速开始说明