本文档提供项目开发所需的开源组件推荐,所有组件均经过技术评估,符合项目的实时性、低延迟和兼容性要求。
/Users/wuzilong/work/00code/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
│
└── 项目文档/ # 项目文档
重要:本项目使用本机Python环境,不需要创建虚拟环境。
# 检查Python版本(需要 >= 3.9)
python --version
# 直接使用 pip 安装依赖
pip install -r backend/requirements.txt
# 运行命令使用 python(不是 python3)
python -m uvicorn main:app --reload
| 组件 | 推荐版本 | 用途 | 选择理由 |
|---|---|---|---|
| React | 18.2+ | 前端框架 | Concurrent模式支持、优秀的性能、生态成熟 |
| Vite | 5.0+ | 构建工具 | 极速开发体验、原生ESM、优化的生产构建 |
| TypeScript | 5.0+ | 类型系统 | 类型安全、更好的IDE支持、减少运行时错误 |
# 前端项目初始化(在 frontend/ 目录)
cd /Users/wuzilong/work/00code/Tolan/frontend
npm create vite@latest . -- --template react-ts
npm install
Recorder - xiangyuecn/Recorder
核心优势:
安装方式:
npm install recorder-core
基础使用:
import Recorder from 'recorder-core';
import 'recorder-core/src/engine/pcm'; // 引入PCM格式
// 创建录音实例
const rec = Recorder({
type: "pcm", // 输出PCM格式
sampleRate: 16000, // 采样率16kHz
bitRate: 16, // 位深16位
onProcess: function(buffers, powerLevel, duration, sampleRate) {
// 实时获取PCM数据
const pcmData = Recorder.SampleData(buffers, sampleRate, 16000);
// 转换为Int16格式
const int16Data = new Int16Array(pcmData.data.length);
for(let i = 0; i < pcmData.data.length; i++) {
int16Data[i] = pcmData.data[i];
}
// 实时发送到后端
websocket.send(int16Data.buffer);
}
});
// 打开录音
rec.open(function() {
// 开始录音
rec.start();
}, function(msg, isUserNotAllow) {
console.error('无法录音:', msg);
});
// 停止录音
rec.stop(function(blob, duration) {
console.log('录音完成');
}, function(msg) {
console.error('停止失败:', msg);
});
iOS兼容方案:
// iOS需要在用户交互中初始化
import Recorder from 'recorder-core';
import 'recorder-core/src/extensions/ios-weixin-config';
// iOS微信兼容
Recorder.IsOpen(function() {
// 已准备好
}, function(msg, isUserNotAllow) {
// 处理错误
});
核心优势:
npm install howler
import { Howl } from 'howler';
// 流式播放音频
const sound = new Howl({
src: ['audio.ogg'], // 支持OGG格式
format: ['ogg'],
html5: true, // 使用HTML5 Audio(支持流式)
onload: function() {
console.log('音频加载完成');
},
onplay: function() {
console.log('开始播放');
}
});
sound.play();
流式播放方案(实时音频):
由于Howler.js不直接支持流式追加,建议使用原生方案:
// 使用MediaSource API实现流式播放
class StreamAudioPlayer {
constructor() {
this.mediaSource = new MediaSource();
this.audio = document.createElement('audio');
this.audio.src = URL.createObjectURL(this.mediaSource);
this.sourceBuffer = null;
this.queue = [];
}
async init() {
return new Promise((resolve) => {
this.mediaSource.addEventListener('sourceopen', () => {
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();
});
});
}
appendAudio(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();
}
}
}
Socket.IO Client (可选)
npm install socket.io-client
或使用原生WebSocket(推荐,更低延迟):
class RealtimeWebSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.binaryType = 'arraybuffer'; // 二进制模式
this.ws.onopen = () => {
console.log('WebSocket连接成功');
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event) => {
// 接收音频数据
if (event.data instanceof ArrayBuffer) {
this.onAudioData?.(event.data);
}
};
this.ws.onclose = () => {
this.handleReconnect();
};
}
send(data) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(data);
}
}
handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
const delay = 1000 * Math.pow(2, this.reconnectAttempts);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
}
}
选择理由:
npm install zustand
import { create } from 'zustand';
type AnimationState = 'IDLE' | 'LISTENING' | 'SPEAKING';
interface AppState {
animationState: AnimationState;
isRecording: boolean;
isPlaying: boolean;
setAnimationState: (state: AnimationState) => void;
startRecording: () => void;
stopRecording: () => void;
startPlaying: () => void;
stopPlaying: () => void;
}
const useAppStore = create<AppState>((set) => ({
animationState: 'IDLE',
isRecording: false,
isPlaying: false,
setAnimationState: (state) => set({ animationState: state }),
startRecording: () => set({
isRecording: true,
animationState: 'LISTENING'
}),
stopRecording: () => set({
isRecording: false
}),
startPlaying: () => set({
isPlaying: true,
animationState: 'SPEAKING'
}),
stopPlaying: () => set({
isPlaying: false,
animationState: 'IDLE'
})
}));
export default useAppStore;
npm install lottie-web
import lottie from 'lottie-web';
class AnimationController {
private animations: Record<string, any> = {};
private currentState: string = 'idle';
constructor(container: HTMLElement) {
// 加载三种状态的动画
this.animations.idle = lottie.loadAnimation({
container: container,
renderer: 'svg',
loop: true,
autoplay: false,
path: '/animations/idle.json'
});
this.animations.listening = lottie.loadAnimation({
container: container,
renderer: 'svg',
loop: true,
autoplay: false,
path: '/animations/listening.json'
});
this.animations.speaking = lottie.loadAnimation({
container: container,
renderer: 'svg',
loop: true,
autoplay: false,
path: '/animations/speaking.json'
});
}
setState(state: 'idle' | 'listening' | 'speaking') {
this.animations[this.currentState].stop();
this.animations[this.currentState].hide();
this.currentState = state;
this.animations[state].show();
this.animations[state].play();
}
}
@esotericsoftware/spine-player
npm install @esotericsoftware/spine-player
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm install antd
lodash-es - 工具函数库
npm install lodash-es
npm install -D @types/lodash-es
dayjs - 日期处理(轻量级)
npm install dayjs
axios - HTTP客户端(如果需要REST API)
npm install axios
核心优势:
# 后端环境配置(使用本机Python环境)
cd /Users/wuzilong/work/00code/Tolan/backend
# 安装依赖(使用本机Python)
pip install fastapi[all]
pip install uvicorn[standard]
from fastapi import FastAPI, WebSocket
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# CORS配置
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
# 接收音频数据
audio_data = await websocket.receive_bytes()
# 处理音频...
# 发送响应
await websocket.send_bytes(response_data)
except Exception as e:
print(f"WebSocket错误: {e}")
finally:
await websocket.close()
pip install websockets
或直接使用FastAPI的WebSocket支持(推荐)
内置 asyncio(Python 3.9+自带)
import asyncio
from asyncio import Queue
class StreamingSession:
def __init__(self):
self.audio_queue = Queue()
self.response_queue = Queue()
async def process_stream(self):
# 并发处理多个任务
await asyncio.gather(
self.receive_audio(),
self.send_to_doubao(),
self.receive_from_doubao(),
self.send_to_client()
)
pip install pydub
# 需要安装ffmpeg
# macOS: brew install ffmpeg
# Ubuntu: sudo apt-get install ffmpeg
注意: 由于项目要求实时流式传输,音频转换应该在前端完成(Recorder已支持PCM输出),后端直接透传,无需pydub。
pip install python-dotenv
from dotenv import load_dotenv
import os
load_dotenv()
DOUBAO_API_KEY = os.getenv("DOUBAO_API_KEY")
DOUBAO_APP_ID = os.getenv("DOUBAO_APP_ID")
pip install loguru
from loguru import logger
# 配置日志
logger.add("logs/app_{time}.log", rotation="500 MB", retention="10 days")
logger.info("WebSocket连接建立")
logger.error("豆包API调用失败: {error}", error=str(e))
logger.debug("延迟测量: {latency}ms", latency=latency)
pip install httpx
import httpx
async def call_api():
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.example.com/endpoint",
json={"key": "value"}
)
return response.json()
前端:
# ESLint + Prettier
npm install -D eslint prettier eslint-config-prettier
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
后端:
# Black(代码格式化)
pip install black
# Flake8(代码检查)
pip install flake8
# MyPy(类型检查)
pip install mypy
前端:
# Vitest(与Vite完美集成)
npm install -D vitest @vitest/ui
npm install -D @testing-library/react @testing-library/jest-dom
后端:
# Pytest
pip install pytest pytest-asyncio pytest-cov
前端:
npm install web-vitals
import { onCLS, onFID, onFCP, onLCP, onTTFB } from 'web-vitals';
function sendToAnalytics(metric: any) {
console.log(metric);
}
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onFCP(sendToAnalytics);
onLCP(sendToAnalytics);
onTTFB(sendToAnalytics);
自定义延迟监控:
class PerformanceTracker {
private startTime: number = 0;
start() {
this.startTime = performance.now();
}
end(label: string) {
const duration = performance.now() - this.startTime;
console.log(`${label}: ${duration.toFixed(2)}ms`);
// 发送到监控服务
this.sendMetric(label, duration);
}
sendMetric(label: string, duration: number) {
// 发送到后端或第三方监控服务
}
}
前端Dockerfile:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 443
CMD ["nginx", "-g", "daemon off;"]
后端Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Let's Encrypt - 免费SSL证书
Certbot - 自动化SSL证书管理
# 安装Certbot
sudo apt-get install certbot python3-certbot-nginx
# 获取证书
sudo certbot --nginx -d yourdomain.com
或使用:
mkcert - 本地开发用证书
# 安装
brew install mkcert # macOS
# 或
choco install mkcert # Windows
# 创建本地证书
mkcert -install
mkcert localhost 127.0.0.1 ::1
配置示例(nginx.conf):
upstream backend {
server localhost:8000;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 前端静态文件
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# WebSocket代理
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 86400;
}
}
查阅豆包官方文档,使用官方Python SDK
基于项目中的 doubao-api/realtime_dialog_client.py
参考文件结构:
doubao-api/
├── realtime_dialog_client.py # 豆包客户端
├── protocol.py # 协议定义
├── config.py # 配置管理
└── audio_manager.py # 音频管理
{
"name": "tolan-frontend",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"recorder-core": "^1.2.23102400",
"howler": "^2.2.3",
"zustand": "^4.5.0",
"lottie-web": "^5.12.2",
"dayjs": "^1.11.10",
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/howler": "^2.2.11",
"@types/lodash-es": "^4.17.12",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.1.0",
"vitest": "^1.2.2"
}
}
# Web框架
fastapi==0.110.0
uvicorn[standard]==0.27.1
# WebSocket
websockets==12.0
# 异步HTTP客户端
httpx==0.27.0
# 配置管理
python-dotenv==1.0.1
# 日志
loguru==0.7.2
# 音频处理(可选,按需使用)
# pydub==0.25.1
# 开发工具
black==24.2.0
flake8==7.0.0
mypy==1.8.0
# 测试
pytest==8.0.1
pytest-asyncio==0.23.5
pytest-cov==4.1.0
# 类型提示
types-requests==2.31.0
| 浏览器 | 支持版本 | 说明 |
|---|---|---|
| Chrome | 53+ | 完美支持 |
| Firefox | 52+ | 完美支持 |
| Safari | 11+ | 完美支持 |
| Edge | 79+ | 完美支持(Chromium) |
| iOS Safari | 11+ | 需要用户交互激活,支持良好 |
| Android Chrome | 53+ | 完美支持 |
| 微信浏览器 | - | 需要额外配置,支持良好 |
import Recorder from 'recorder-core';
import 'recorder-core/src/engine/pcm';
class VoiceRecorder {
private recorder: any = null;
private ws: WebSocket;
constructor(websocketUrl: string) {
this.ws = new WebSocket(websocketUrl);
this.ws.binaryType = 'arraybuffer';
}
async init() {
return new Promise<void>((resolve, reject) => {
this.recorder = Recorder({
type: 'pcm',
sampleRate: 16000,
bitRate: 16,
// 实时回调,每隔一段时间触发
onProcess: (buffers: any[], powerLevel: number, duration: number, sampleRate: number) => {
// 获取PCM数据
const pcmData = Recorder.SampleData(buffers, sampleRate, 16000);
// 转换为Int16Array(小端序)
const int16Data = new Int16Array(pcmData.data.length);
for (let i = 0; i < pcmData.data.length; i++) {
// Recorder内部是Int16,直接复制
int16Data[i] = pcmData.data[i];
}
// 实时发送到WebSocket
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(int16Data.buffer);
}
}
});
// 打开录音
this.recorder.open(() => {
console.log('录音已准备');
resolve();
}, (msg: string, isUserNotAllow: boolean) => {
console.error('打开录音失败:', msg);
reject(new Error(msg));
});
});
}
start() {
if (this.recorder) {
this.recorder.start();
console.log('开始录音');
}
}
stop() {
return new Promise<void>((resolve) => {
if (this.recorder) {
this.recorder.stop(
(blob: Blob, duration: number) => {
console.log('录音完成,时长:', duration, 'ms');
resolve();
},
(msg: string) => {
console.error('停止录音失败:', msg);
resolve();
}
);
} else {
resolve();
}
});
}
close() {
if (this.recorder) {
this.recorder.close();
}
if (this.ws) {
this.ws.close();
}
}
}
// 使用示例
const recorder = new VoiceRecorder('wss://your-backend.com/ws');
// 初始化
await recorder.init();
// 按钮按下
button.addEventListener('mousedown', () => {
recorder.start();
});
// 按钮松开
button.addEventListener('mouseup', async () => {
await recorder.stop();
});
import Recorder from 'recorder-core';
import 'recorder-core/src/extensions/ios-weixin-config';
// iOS需要在用户手势中初始化
class IOSCompatibleRecorder {
private static isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
private static isReady = false;
static async prepare() {
if (!this.isIOS || this.isReady) return;
return new Promise<void>((resolve, reject) => {
// iOS必须在用户交互中调用
Recorder.IsOpen(() => {
this.isReady = true;
console.log('iOS录音已准备');
resolve();
}, (msg: string) => {
console.error('iOS录音准备失败:', msg);
reject(new Error(msg));
});
});
}
}
// 在页面加载时显示提示
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
// 显示"点击任意位置激活录音"的提示
document.addEventListener('click', async () => {
await IOSCompatibleRecorder.prepare();
// 隐藏提示
}, { once: true });
}
代码分割:
// 使用React.lazy进行路由级别的代码分割
import { lazy, Suspense } from 'react';
const HomePage = lazy(() => import('./pages/Home'));
const ChatPage = lazy(() => import('./pages/Chat'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/chat" element={<ChatPage />} />
</Routes>
</Suspense>
);
}
组件级优化:
import { memo, useCallback, useMemo } from 'react';
// 使用memo避免不必要的重渲染
const AnimationComponent = memo(({ state }: { state: string }) => {
return <div>{/* 动画内容 */}</div>;
});
// 使用useCallback缓存函数
const handleRecordStart = useCallback(() => {
recorder.start();
}, []);
连接池配置:
# 根据服务器资源调整连接池大小
CPU_COUNT = os.cpu_count() or 4
POOL_SIZE = CPU_COUNT * 2 # 经验值:CPU核心数的2倍
connection_pool = DoubaoConnectionPool(pool_size=POOL_SIZE)
异步优化:
# 使用asyncio.create_task而不是await,提高并发
task1 = asyncio.create_task(send_to_doubao(data))
task2 = asyncio.create_task(log_metrics(data))
# 不等待task完成,继续处理其他逻辑
| 层级 | 组件 | 推荐方案 | 优先级 |
|---|---|---|---|
| 前端录音 | 音频采集 | Recorder (xiangyuecn) | ⭐⭐⭐⭐⭐ |
| 前端播放 | 音频播放 | MediaSource API / Howler.js | ⭐⭐⭐⭐⭐ |
| 前端框架 | UI框架 | React 18+ | ⭐⭐⭐⭐⭐ |
| 前端状态 | 状态管理 | Zustand | ⭐⭐⭐⭐ |
| 前端动画 | 动画播放 | Lottie | ⭐⭐⭐⭐ |
| 前端构建 | 构建工具 | Vite | ⭐⭐⭐⭐⭐ |
| 后端框架 | Web服务 | FastAPI | ⭐⭐⭐⭐⭐ |
| 后端异步 | 并发处理 | asyncio | ⭐⭐⭐⭐⭐ |
| 通信协议 | WebSocket | 原生WebSocket | ⭐⭐⭐⭐⭐ |
| 部署 | 服务器 | Nginx + Uvicorn | ⭐⭐⭐⭐⭐ |
# 1. 前端项目(在 frontend/ 目录)
cd /Users/wuzilong/work/00code/Tolan/frontend
npm install recorder-core howler zustand lottie-web
npm install -D tailwindcss postcss autoprefixer
npm run dev
# 2. 后端项目(在 backend/ 目录,使用本机Python)
cd /Users/wuzilong/work/00code/Tolan/backend
# 安装依赖(无需虚拟环境)
pip install fastapi uvicorn[standard] websockets python-dotenv loguru
# 启动开发服务器
python -m uvicorn main:app --reload
# 生产环境(使用SSL)
python -m uvicorn main:app --host 0.0.0.0 --port 8443 \
--ssl-keyfile=../ssl/key.pem --ssl-certfile=../ssl/cert.pem
# 进入前端目录
cd /Users/wuzilong/work/00code/Tolan/frontend
# 安装依赖
npm install
# 开发环境
npm run dev
# 生产构建
npm run build
# 进入后端目录
cd /Users/wuzilong/work/00code/Tolan/backend
# 安装依赖(无需虚拟环境)
pip install -r requirements.txt
# 开发环境
python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000
# 生产环境(使用SSL)
python -m uvicorn main:app --host 0.0.0.0 --port 8443 \
--ssl-keyfile=../ssl/key.pem \
--ssl-certfile=../ssl/cert.pem
文档版本: v1.1
创建日期: 2025-10-08
最后更新: 2025-10-08
更新内容: 添加项目结构说明,更新为本机Python环境配置