diff --git a/CLAUDE.md b/CLAUDE.md index 90e4ed4..f66681a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,106 +1,104 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +本文件为 Claude Code (claude.ai/code) 在本仓库中工作时提供指引。 -## Project Overview +## 项目概述 -CamTalk is a multimodal real-time AI visual dialogue assistant. Users interact via camera and microphone — the app captures visual scenes and voice input, sends them to AI services, and responds with both text and speech. The project is currently in the design-document phase; source code is being built incrementally. +CamTalk 是一款多模态实时 AI 视觉对话助手。用户通过摄像头和麦克风与 AI 交互,AI 理解视觉场景和语音输入后,以文字和语音形式给出自然回应。项目目前处于设计文档阶段,源代码正在逐步构建。 > **文档优先原则:** 执行任何开发任务前,先读取 `docs/` 下的相关设计文档(架构、接口、技术选型等),以文档为最高依据。代码实现应与文档一致;若有偏差,优先更新文档(尤其是接口文档)。 -**Design docs (Chinese):** `docs/` contains the full architecture, API contracts, user stories, cost control strategies, and technology selection rationale. +## 架构 -## Architecture +三层系统: -Three-layer system: +1. **浏览器客户端**(React 18 + TypeScript, Vite)—— 媒体采集、边缘预处理(VAD 通过 `@ricky0123/vad-web`、关键帧检测通过 ONNX Runtime Web)、UI 渲染。核心 Hook:`useVisionSession()` +2. **Go 网关**(gorilla/websocket, Redis, Viper, Zap)—— WebSocket 服务器、会话管理、模型路由、AI 编排、速率限制。每个 WebSocket 连接一个 goroutine。 +3. **云端 AI 服务** —— GPT-4o(LLM)、Deepgram(STT)、OpenAI TTS。仅通过 Go 网关访问,浏览器不直连。 -1. **Browser Client** (React 18 + TypeScript, Vite) — media capture, edge preprocessing (VAD via `@ricky0123/vad-web`, keyframe detection via ONNX Runtime Web), UI rendering. Core hook: `useVisionSession()`. -2. **Go Gateway** (gorilla/websocket, Redis, Viper, Zap) — WebSocket server, session management, model routing, AI orchestration, rate limiting. One goroutine per WebSocket connection. -3. **Cloud AI Services** — GPT-4o (LLM), Deepgram (STT), OpenAI TTS. Accessed only through the Go gateway, never directly from the browser. +**关键模式**:LLM 文本流和 TTS 音频流并行推送给客户端,以最小化感知延迟。 -**Key pattern:** LLM text chunks and TTS audio are streamed in parallel to the client to minimize perceived latency. +**存储**:冷热分离 —— Redis 存实时会话状态,PostgreSQL 存对话历史和用量统计(MVP 后引入)。Repository 接口模式(`HistoryRepository`、`UsageRepository`),MVP 用内存实现。 -**Storage:** Cold/hot separation — Redis for real-time session state, PostgreSQL for conversation history and usage stats (deferred past MVP). Repository interface pattern (`HistoryRepository`, `UsageRepository`) with in-memory MVP implementations. +## 技术栈 -## Tech Stack +| 层级 | 技术 | +|------|------| +| 前端 | React 18, TypeScript, Vite, ONNX Runtime Web, @ricky0123/vad-web | +| 后端 | Go, gorilla/websocket, Redis, Viper, Zap | +| LLM | GPT-4o(主), Claude Sonnet(备) | +| STT | Deepgram(主), FunASR(自部署备选) | +| TTS | OpenAI TTS(主), Edge TTS(免费替代) | +| 模型路由 | GPT-4o-mini 用于轻量分类 | -| Layer | Tech | -|-------|------| -| Frontend | React 18, TypeScript, Vite, ONNX Runtime Web, @ricky0123/vad-web | -| Backend | Go, gorilla/websocket, Redis, Viper, Zap | -| LLM | GPT-4o (primary), Claude Sonnet (backup) | -| STT | Deepgram (primary), FunASR (self-hosted backup) | -| TTS | OpenAI TTS (primary), Edge TTS (free alternative) | -| Model routing | GPT-4o-mini for lightweight classification | - -## Build & Run Commands +## 构建与运行命令 ```bash -# Frontend +# 前端 cd frontend && npm install -npm run dev # Vite dev server -npm run build # Production build -npm run lint # ESLint -npm run test # Vitest +npm run dev # Vite 开发服务器 +npm run build # 生产构建 +npm run lint # ESLint 检查 +npm run test # Vitest 测试 -# Backend +# 后端 cd backend && go mod download -go run ./cmd/server # Start gateway on :8080 +go run ./cmd/server # 启动网关,监听 :8080 go build -o bin/camtalk ./cmd/server -go test ./... # Run all tests -go test -run TestName ./path # Run single test -go vet ./... # Static analysis +go test ./... # 运行所有测试 +go test -run TestName ./path # 运行单个测试 +go vet ./... # 静态分析 ``` -Infrastructure: Redis required for session state. PostgreSQL optional for MVP (in-memory fallback). +基础设施:Redis 为会话状态必需。PostgreSQL 为 MVP 可选(内存回退)。 -## WebSocket Protocol +## WebSocket 协议 -Endpoint: `ws://localhost:8080/ws` +端点:`ws://localhost:8080/ws` -All messages are JSON text frames with `{type, request_id?, timestamp?}` envelope. See `docs/AI 视觉对话助手/项目实现/接口文档.md` for the full contract. +所有消息为 JSON 文本帧,统一信封格式 `{type, request_id?, timestamp?}`。完整契约见 `docs/03-接口文档.md`。 -**Client → Server:** `query` (image Base64 + audio Base64), `config`, `interrupt`, `ping` -**Server → Client:** `connected`, `stt_result`, `llm_chunk`, `llm_done`, `tts_audio`, `error`, `pong` +**客户端 → 服务端**:`query`(图像 Base64 + 音频 Base64)、`config`、`interrupt`、`ping` +**服务端 → 客户端**:`connected`、`stt_result`、`llm_chunk`、`llm_done`、`tts_audio`、`error`、`pong` -**Heartbeat:** Client pings every 30s. Server disconnects after 60s of silence. -**Reconnection:** Exponential backoff with jitter — 1s, 2s, 4s, 8s… max 30s. +**心跳**:客户端每 30 秒 ping,服务端 60 秒无 ping 断开连接。 +**重连**:指数退避 + 抖动 —— 1s, 2s, 4s, 8s… 最大 30s。 -## REST API (Auxiliary) +## REST API(辅助) -- `GET /api/health` — health check (version, uptime, active sessions) -- `POST /api/sessions` — create session (optional, MVP auto-creates on WS connect) -- `DELETE /api/sessions/{id}` — destroy session +- `GET /api/health` — 健康检查(版本、运行时间、活跃会话数) +- `POST /api/sessions` — 创建会话(可选,MVP 在 WS 连接时自动创建) +- `DELETE /api/sessions/{id}` — 销毁会话 -## Error Codes +## 错误码 -`INVALID_MESSAGE`, `SESSION_NOT_FOUND`, `RATE_LIMITED`, `IMAGE_TOO_LARGE`, `AUDIO_TOO_SHORT`, `LLM_TIMEOUT`, `LLM_ERROR`, `STT_ERROR`, `TTS_ERROR`, `INTERNAL_ERROR` +`INVALID_MESSAGE`、`SESSION_NOT_FOUND`、`RATE_LIMITED`、`IMAGE_TOO_LARGE`、`AUDIO_TOO_SHORT`、`LLM_TIMEOUT`、`LLM_ERROR`、`STT_ERROR`、`TTS_ERROR`、`INTERNAL_ERROR` -## Frontend Component Structure +## 前端组件结构 -| Component | Responsibility | -|-----------|---------------| -| `CameraManager` | Camera stream capture | -| `MicManager` | Microphone audio capture | -| `EdgeProcessor` | VAD + keyframe detection (ONNX Runtime) | -| `WebSocketManager` | WS connection lifecycle | -| `ChatPanel` | Message display | -| `VideoPreview` | Camera feed display | +| 组件 | 职责 | +|------|------| +| `CameraManager` | 摄像头流采集 | +| `MicManager` | 麦克风音频采集 | +| `EdgeProcessor` | VAD + 关键帧检测(ONNX Runtime) | +| `WebSocketManager` | WebSocket 连接生命周期管理 | +| `ChatPanel` | 消息展示 | +| `VideoPreview` | 摄像头画面预览 | -## Backend Module Structure +## 后端模块结构 -| Module | Responsibility | -|--------|---------------| -| WebSocket Hub | Connection management, broadcast/direct push | -| Session Manager | Session state, conversation history (Redis + TTL) | -| Model Router | Select AI model per request (rule engine + cost threshold) | -| AI Orchestrator | Parallel/sequential AI calls with context timeout | -| Rate Limiter | Per-user token bucket rate limiting | +| 模块 | 职责 | +|------|------| +| WebSocket Hub | 连接管理、广播/定向推送 | +| Session Manager | 会话状态、对话历史(Redis + TTL) | +| Model Router | 按请求选择 AI 模型(规则引擎 + 成本阈值) | +| AI Orchestrator | 并行/串行 AI 调用编排,context 超时控制 | +| Rate Limiter | 按用户的令牌桶速率限制 | -## Coding Conventions +## 编码规范 -- **Go:** Follow standard Go conventions. Use `context.Context` for cancellation/timeout in all AI calls. Use `sync.RWMutex` for concurrent map access. Struct tags use `json:"snake_case"`. -- **TypeScript:** Strict mode. Interfaces for all data models. WebSocket message types as discriminated unions (`type` field). -- **Commit messages:** Conventional commits 格式,描述用中文。示例:`feat: 添加 WebSocket 连接管理`, `fix: 修复心跳超时判断`, `docs: 更新接口文档` -- **No auto-push:** 禁止自动 push,除非用户明确要求。 -- **Docs-first:** 实现功能前先读取 `docs/` 下的相关设计文档,以文档为依据进行开发。实现与文档不一致时,优先更新 `docs/` 下的接口文档。 +- **Go**:遵循标准 Go 规范。所有 AI 调用使用 `context.Context` 做取消/超时。并发 map 访问使用 `sync.RWMutex`。结构体标签用 `json:"snake_case"`。 +- **TypeScript**:严格模式。所有数据模型用接口定义。WebSocket 消息类型用可辨识联合类型(`type` 字段)。 +- **提交信息**:Conventional Commits 格式,描述用中文。示例:`feat: 添加 WebSocket 连接管理`、`fix: 修复心跳超时判断`、`docs: 更新接口文档` +- **禁止自动 push**:除非用户明确要求。 +- **文档优先**:实现功能前先读取 `docs/` 下的相关设计文档。实现与文档不一致时,优先更新 `docs/` 下的接口文档。 diff --git a/docs/01-项目概述.md b/docs/01-项目概述.md new file mode 100644 index 0000000..8a5a2c9 --- /dev/null +++ b/docs/01-项目概述.md @@ -0,0 +1,28 @@ +# 项目概述 + +## 概述 + +开发一款**多模态实时对话应用**——通过摄像头与麦克风捕获用户的视觉场景与语音输入,由 AI 理解并给出自然、流畅的回应。 + +核心挑战在于三个维度之间的张力: + +| 维度 | 关键问题 | 详见 | +|------|---------|------| +| 视觉理解 | 如何准确理解摄像头画面中的人物、物体、场景? | `07-视觉理解.md` | +| 语音交互 | 如何让对话像真人交流一样自然、低延迟? | `06-语音交互.md` | +| 成本控制 | 实时视频流 + LLM 推理,如何避免账单爆炸? | `08-成本控制.md` | + +> 提升视觉精度意味着更高分辨率和更频繁的采样,但这会直接推高带宽和推理成本。架构设计需要在三者之间做好取舍。 + +## 项目目标 + +1. **用户故事规划**:明确"AI 能看、能听、能说"需要覆盖哪些场景 → `05-用户故事.md` +2. **成本控制策略**:从架构设计层面融入运营成本意识 → `08-成本控制.md` + +## 交付物 + +- 可运行的应用程序(摄像头 + 麦克风 → AI 回应) +- 设计文档,覆盖: + - 计划实现 vs 最终实现的用户故事 + - 成本控制技巧的构思 vs 实际采用的方案 + - 项目架构设计与技术选型 diff --git a/docs/02-系统架构.md b/docs/02-系统架构.md new file mode 100644 index 0000000..cd904f5 --- /dev/null +++ b/docs/02-系统架构.md @@ -0,0 +1,207 @@ +# 系统架构 + +## 概述 + +三层架构:**前端做轻量预处理,后端做智能编排,云端 AI 服务按需调用**。在保证交互体验的同时控制成本。 + +## 三层架构 + +| 层级 | 职责 | 关键约束 | +|------|------|---------| +| **客户端(浏览器)** | 媒体采集、边缘预处理、UI 渲染 | 浏览器资源有限,模型需轻量 | +| **Go 网关** | 会话管理、模型路由、AI 服务编排 | 高并发、低延迟、状态管理 | +| **AI 服务** | LLM 推理、语音识别、语音合成 | 按量计费,需控制调用频率 | + +> 为什么要单独加一层 Go 网关,而不是让前端直连 AI API?1)API Key 安全性;2)统一的速率限制和成本管控;3)多模型路由逻辑集中在一处便于维护。 + +## 技术栈 + +### 前端 + +| 技术 | 选型 | 选择理由 | +|------|------|---------| +| 框架 | React 18 + TypeScript | 组件化开发,类型安全,生态成熟 | +| 构建 | Vite | 开发热更新快,构建产物小 | +| 实时通信 | WebSocket(原生 API) | 浏览器原生支持,无需额外依赖 | +| 边缘推理 | ONNX Runtime Web | 浏览器端跑轻量模型(VAD、关键帧检测) | +| 语音检测 | @ricky0123/vad-web | 基于 WebRTC VAD,纯前端零延迟 | +| 媒体采集 | MediaDevices API | 浏览器原生摄像头/麦克风访问 | + +### 后端 + +| 技术 | 选型 | 选择理由 | +|------|------|---------| +| 语言 | Go | 高并发 goroutine 模型,适合长连接管理 | +| WebSocket | gorilla/websocket | Go 生态最成熟的 WebSocket 库 | +| 会话存储 | Redis | 高速 KV 存储,适合会话状态和上下文缓存 | +| 持久化存储 | PostgreSQL | 对话历史、用量统计、用户偏好(MVP 阶段可选) | +| 配置管理 | Viper | 支持多格式配置,环境变量覆盖 | +| 日志 | Zap | 高性能结构化日志 | + +### AI 服务 + +| 能力 | 主选方案 | 备选方案 | 选型考量 | +|------|---------|---------|---------| +| 多模态 LLM | GPT-4o | Claude Sonnet | 视觉理解能力强,API 成熟 | +| 语音识别 STT | Deepgram | FunASR 自部署 | 流式识别延迟低(<500ms) | +| 语音合成 TTS | OpenAI TTS | Edge TTS(免费) | 音质自然,支持流式 | +| 轻量分类 | GPT-4o-mini | Haiku | 模型路由时的复杂度判断 | + +> 不必绑定单一厂商。Go 网关的模型路由层统一封装不同 AI 服务的调用接口,按场景动态切换。 + +## 核心交互流程 + +一次完整的"用户提问 → AI 回答"流程: + +``` +Browser Go Gateway STT LLM TTS + | | | | | + |-- VAD 检测到语音结束 --->| | | | + | | | | | + |-- [音频+图像] -------->| | | | + | |--- 音频流 ------->| | | + | |<-- 流式文本 ------| | | + | | | | | + | |--- [图像+文本+上下文] -------->| | + | |<-- 流式回答文本 --------------| | + |<-- 推送回答文本 --------| | | | + | |--- 回答文本 ---------------------------->| + | |<-- 流式音频 --------------------------------| + |<-- 推送音频流 ----------| | | | + | | | | | + |-> 播放音频 + 渲染文字 | | | | +``` + +**关键优化**:LLM 文本流和 TTS 音频流是**并行推送**的——客户端先展示文字,同时开始播放语音,用户感知延迟大幅降低。 + +## 后端模块 + +| 模块 | 职责 | 关键实现 | +|------|------|---------| +| WebSocket Hub | 管理所有客户端连接,广播/定向推送 | goroutine per connection | +| Session Manager | 维护用户会话状态、对话历史 | Redis + TTL 过期策略 | +| Model Router | 根据请求类型选择 AI 模型 | 规则引擎 + 成本阈值 | +| AI Orchestrator | 编排多路 AI 调用(并行/串行) | context 取消 + 超时控制 | +| Rate Limiter | 防止单用户过度消耗 API 额度 | 令牌桶算法 | + +AI Orchestrator 核心代码: + +```go +func (o *Orchestrator) ProcessQuery(ctx context.Context, req *QueryRequest) (*QueryResponse, error) { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + // 并行:LLM 推理 + 准备 TTS + llmCh := make(chan string, 1) + go func() { + resp, _ := o.llm.Chat(ctx, req.Image, req.Text, req.History) + llmCh <- resp + }() + + llmText := <-llmCh + // LLM 返回后,流式推送给客户端,同时启动 TTS + ttsCh := make(chan []byte, 1) + go func() { + audio, _ := o.tts.Synthesize(ctx, llmText) + ttsCh <- audio + }() + + return &QueryResponse{Text: llmText, Audio: <-ttsCh}, nil +} +``` + +## 前端组件 + +| 组件 | 职责 | +|------|------| +| CameraManager | 摄像头流采集 | +| MicManager | 麦克风音频采集 | +| EdgeProcessor | VAD + 关键帧检测(ONNX Runtime) | +| WebSocketManager | WS 连接生命周期管理 | +| ChatPanel | 消息展示 | +| VideoPreview | 摄像头画面预览 | + +核心 Hook:`useVisionSession()` 封装一次完整的视觉对话会话(摄像头、VAD、WebSocket、消息状态)。 + +```typescript +function useVisionSession() { + const [messages, setMessages] = useState([]); + const wsRef = useWebSocket("ws://localhost:8080/ws"); + const videoRef = useRef(null); + const { captureFrame } = useCamera(videoRef); + + const { isSpeaking } = useVAD({ + onSpeechEnd: async (audio) => { + const frame = captureFrame(); + wsRef.current?.send(JSON.stringify({ + type: "query", + image: frame.toDataURL("image/jpeg", 0.7), + audio: encodeAudio(audio) + })); + } + }); + + useEffect(() => { + wsRef.current?.on("message", (data) => { + const { text, audio } = JSON.parse(data); + setMessages(prev => [...prev, { role: "assistant", text }]); + if (audio) playAudio(audio); + }); + }, []); + + return { messages, videoRef, isSpeaking }; +} +``` + +## 存储策略(分阶段) + +| 阶段 | 存储方案 | 持久化内容 | 理由 | +|------|---------|-----------|------| +| MVP | Redis only | 无 | 快速验证核心功能,重启丢数据可接受 | +| 上线 | Redis + PostgreSQL | 对话历史、用户偏好、用量统计 | 用户需要查看历史,运营需要成本数据 | +| 规模化 | Redis + PG + 对象存储 | 图像帧、音频片段归档 | 大文件不适合存关系库 | + +冷热分离:Redis 存"热数据"(当前对话上下文,微秒级读写),PostgreSQL 存"冷数据"(历史记录)。 + +### PostgreSQL 表设计 + +```sql +CREATE TABLE sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE TABLE messages ( + id BIGSERIAL PRIMARY KEY, + session_id UUID REFERENCES sessions(id), + role VARCHAR(16) NOT NULL, -- "user" | "assistant" + content TEXT NOT NULL, + image_url TEXT, + tokens_used INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE TABLE usage_daily ( + user_id UUID NOT NULL, + date DATE NOT NULL, + llm_tokens BIGINT DEFAULT 0, + stt_seconds REAL DEFAULT 0, + tts_chars INTEGER DEFAULT 0, + estimated_cost NUMERIC(10,4) DEFAULT 0, + PRIMARY KEY (user_id, date) +); +``` + +## 部署架构 + +``` +CDN(静态资源) ← 用户浏览器 +Nginx 负载均衡(sticky session for WebSocket) + ├── Gateway-1 ──→ Redis + ├── Gateway-2 ──→ Redis + └── Gateway-N ──→ AI Services(外部 API) +``` + +WebSocket 是长连接,Nginx 需要配置 `proxy_set_header Upgrade` 和 sticky session,确保同一用户的请求始终路由到同一个 Gateway 实例。 diff --git a/docs/03-接口文档.md b/docs/03-接口文档.md new file mode 100644 index 0000000..3aa530e --- /dev/null +++ b/docs/03-接口文档.md @@ -0,0 +1,433 @@ +# 接口文档 + +## 概述 + +前后端通信接口定义。以 WebSocket 承载实时对话,REST 端点支撑基础运维。**暂不实现持久化**,但通过 Repository 接口模式为后续扩展预留接入点。 + +**设计原则**: +- WebSocket 为主:所有对话数据走 WebSocket +- REST 为辅:仅用于健康检查、会话管理等低频操作 +- 接口先行:先定义契约,再填充实现——前后端可并行开发 + +## 接口全景 + +``` +浏览器 Go Gateway :8080 + WebSocket Client <--> /ws (实时对话) + HTTP Client --> GET /api/health + HTTP Client <--> POST/DELETE /api/sessions +``` + +--- + +## 一、WebSocket 协议 + +连接地址:`ws://localhost:8080/ws` + +### 消息格式约定 + +所有 WebSocket 消息均为 JSON 文本帧,统一结构: + +```typescript +interface WsMessage { + type: string; // 消息类型,必填 + request_id?: string; // 可选,用于请求-响应关联 + timestamp?: number; // 可选,毫秒时间戳 + [key: string]: any; // 类型特定字段 +} +``` + +### 客户端 → 服务端消息 + +#### `query` — 发起一次视觉对话 + +用户说完话后,客户端同时发送当前图像帧和语音片段: + +```typescript +interface QueryMessage { + type: "query"; + request_id: string; // 客户端生成的 UUID + image: string; // Base64 编码的 JPEG 图像(不含 data: 前缀) + audio: string; // Base64 编码的音频片段(PCM 16kHz) + mime_type?: string; // 音频格式,默认 "audio/pcm" +} +``` + +> 为什么图像和音频放在同一条消息里?因为 VAD 检测到用户说完话时,需要同时捕获"此刻的画面"和"说的话",拆成两条消息会增加时序同步的复杂度。 + +#### `config` — 更新会话配置 + +```typescript +interface ConfigMessage { + type: "config"; + payload: { + tts_enabled?: boolean; // 是否开启语音合成,默认 true + detail_level?: "low" | "high"; // 图像精度,默认 "low" + language?: string; // 交互语言,默认 "zh-CN" + }; +} +``` + +#### `interrupt` — 打断当前回复 + +```typescript +interface InterruptMessage { + type: "interrupt"; + request_id?: string; // 可选,指定打断哪次请求 +} +``` + +#### `ping` — 心跳保活 + +```typescript +interface PingMessage { + type: "ping"; +} +``` + +### 服务端 → 客户端消息 + +#### `connected` — 连接建立确认 + +```typescript +interface ConnectedMessage { + type: "connected"; + session_id: string; // 服务端生成的会话 ID + server_version: string; // 服务端版本号,如 "0.1.0" +} +``` + +#### `stt_result` — 语音识别结果 + +```typescript +interface STTResultMessage { + type: "stt_result"; + request_id: string; + text: string; // 识别出的用户语音文本 + is_final: boolean; // 是否为最终结果 +} +``` + +#### `llm_chunk` — LLM 流式输出片段 + +```typescript +interface LLMChunkMessage { + type: "llm_chunk"; + request_id: string; + delta: string; // 本次增量文本 + role: "assistant"; +} +``` + +#### `llm_done` — LLM 输出完成 + +```typescript +interface LLMDoneMessage { + type: "llm_done"; + request_id: string; + full_text: string; // 完整回复文本 + tokens_used: { + prompt: number; + completion: number; + total: number; + }; + model: string; // 实际使用的模型名 + latency_ms: number; // 端到端延迟(毫秒) +} +``` + +#### `tts_audio` — TTS 音频流片段 + +```typescript +interface TTSAudioMessage { + type: "tts_audio"; + request_id: string; + audio: string; // Base64 编码的音频片段 + mime_type: string; // "audio/mp3" 或 "audio/pcm" + is_last: boolean; // 是否为最后一片 +} +``` + +#### `error` — 错误通知 + +```typescript +interface ErrorMessage { + type: "error"; + request_id?: string; + code: string; // 错误码,见下方错误码表 + message: string; // 人类可读的错误描述 +} +``` + +#### `pong` — 心跳响应 + +```typescript +interface PongMessage { + type: "pong"; +} +``` + +### 消息流时序 + +一次完整交互: + +``` +Client Server + | | + |-- query {image, audio} ------>| + |<-- stt_result {text} ---------| + | | + |<-- llm_chunk {delta: "这"} ---| (LLM 流式输出) + |<-- llm_chunk {delta: "是一"} -| + |<-- llm_chunk {delta: "朵花"} -| + |<-- llm_done {full_text} ------| + | | + |<-- tts_audio {audio} ---------| (TTS 音频流) + |<-- tts_audio {is_last: true} -| +``` + +--- + +## 二、REST API + +### 健康检查 + +``` +GET /api/health +``` + +响应: + +```json +{ + "status": "ok", + "version": "0.1.0", + "uptime_seconds": 3600, + "active_sessions": 42 +} +``` + +### 创建会话(可选,MVP 自动创建) + +``` +POST /api/sessions +Content-Type: application/json + +{ + "user_id": "optional-user-id", + "config": { + "tts_enabled": true, + "detail_level": "low", + "language": "zh-CN" + } +} +``` + +响应: + +```json +{ + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "created_at": "2026-06-12T15:41:00Z" +} +``` + +### 销毁会话 + +``` +DELETE /api/sessions/{session_id} +``` + +响应:`204 No Content` + +### 预留端点(暂不实现) + +| 端点 | 方法 | 用途 | +|------|------|------| +| `/api/sessions/{id}/messages` | GET | 查询对话历史 | +| `/api/usage` | GET | 查询用量统计 | +| `/api/users/{id}/preferences` | GET/PUT | 用户偏好管理 | + +--- + +## 三、数据模型 + +### Go 后端模型 + +```go +// ---- 核心模型(MVP 实现)---- + +type Session struct { + ID string `json:"session_id"` + CreatedAt time.Time `json:"created_at"` + Config SessionConfig `json:"config"` +} + +type SessionConfig struct { + TTSEnabled bool `json:"tts_enabled"` + DetailLevel string `json:"detail_level"` // "low" | "high" + Language string `json:"language"` +} + +type QueryRequest struct { + RequestID string `json:"request_id"` + Image []byte `json:"-"` // Base64 解码后 + Audio []byte `json:"-"` // Base64 解码后 + MimeType string `json:"mime_type"` +} + +type Message struct { + Role string `json:"role"` // "user" | "assistant" + Content string `json:"content"` + ImageURL string `json:"image_url,omitempty"` + TokensUsed int `json:"tokens_used,omitempty"` +} +``` + +### TypeScript 前端模型 + +```typescript +interface Session { + sessionId: string; + createdAt: string; + config: SessionConfig; +} + +interface SessionConfig { + ttsEnabled: boolean; + detailLevel: "low" | "high"; + language: string; +} + +interface ChatMessage { + role: "user" | "assistant"; + content: string; + imageUrl?: string; + timestamp: number; + tokensUsed?: number; +} + +// WebSocket 消息联合类型 +type ServerMessage = + | ConnectedMessage + | STTResultMessage + | LLMChunkMessage + | LLMDoneMessage + | TTSAudioMessage + | ErrorMessage + | PongMessage; + +type ClientMessage = + | QueryMessage + | ConfigMessage + | InterruptMessage + | PingMessage; +``` + +--- + +## 四、扩展接口设计 + +通过 Repository 接口隔离存储层,MVP 用内存实现,后续替换为数据库——业务逻辑零改动。 + +```go +// HistoryRepository — 对话历史存储契约 +// MVP: 内存实现(session 内有效,断开即丢) +// 后续: PostgreSQL 实现 +type HistoryRepository interface { + SaveMessage(ctx context.Context, sessionID string, msg Message) error + GetMessages(ctx context.Context, sessionID string, limit int) ([]Message, error) +} + +// UsageRepository — 用量统计存储契约 +// MVP: 内存计数器 +// 后续: PostgreSQL 按天聚合 +type UsageRepository interface { + RecordUsage(ctx context.Context, sessionID string, usage UsageRecord) error + GetDailyUsage(ctx context.Context, userID string, days int) ([]UsageDaily, error) +} +``` + +MVP 内存实现: + +```go +type InMemoryHistory struct { + mu sync.RWMutex + sessions map[string][]Message +} + +func (h *InMemoryHistory) SaveMessage(ctx context.Context, sessionID string, msg Message) error { + h.mu.Lock() + defer h.mu.Unlock() + h.sessions[sessionID] = append(h.sessions[sessionID], msg) + return nil +} + +func (h *InMemoryHistory) GetMessages(ctx context.Context, sessionID string, limit int) ([]Message, error) { + h.mu.RLock() + defer h.mu.RUnlock() + msgs := h.sessions[sessionID] + if limit > 0 && len(msgs) > limit { + msgs = msgs[len(msgs)-limit:] + } + return msgs, nil +} +``` + +注入点(应用启动时根据配置选择实现): + +```go +func NewApp(cfg *Config) *App { + var history HistoryRepository + var usage UsageRepository + + switch cfg.Storage.Driver { + case "postgres": + pool, _ := pgxpool.New(ctx, cfg.Storage.DSN) + history = &PgHistory{pool: pool} + usage = &PgUsage{pool: pool} + default: // "memory" — MVP 默认 + history = &InMemoryHistory{sessions: make(map[string][]Message)} + usage = &InMemoryUsage{} + } + + return &App{ + orchestrator: NewOrchestrator(cfg.AI, history, usage), + sessionMgr: NewSessionManager(cfg.Session, history), + } +} +``` + +> 依赖倒置原则——业务层依赖接口,不依赖具体实现。MVP 注入 `InMemoryHistory`,上线时一行代码换成 `PgHistory`。 + +--- + +## 五、错误码 + +| 错误码 | 含义 | 客户端处理建议 | +|--------|------|--------------| +| `INVALID_MESSAGE` | 消息格式不合法 | 检查 JSON 结构,不重试 | +| `SESSION_NOT_FOUND` | 会话不存在或已过期 | 重新建立 WebSocket 连接 | +| `RATE_LIMITED` | 请求频率超限 | 延迟后重试,提示用户稍等 | +| `IMAGE_TOO_LARGE` | 图像超过 4MB 限制 | 降低分辨率或压缩质量 | +| `AUDIO_TOO_SHORT` | 音频片段 < 250ms | 忽略,等待下次语音输入 | +| `LLM_TIMEOUT` | LLM 推理超时(>10s) | 提示用户重试 | +| `LLM_ERROR` | LLM 服务异常 | 提示用户重试,服务端记录日志 | +| `STT_ERROR` | 语音识别失败 | 回退到纯文本输入模式 | +| `TTS_ERROR` | 语音合成失败 | 静默回退到纯文本回复 | +| `INTERNAL_ERROR` | 服务端内部错误 | 提示用户重试 | + +## 六、连接管理 + +**心跳机制**:客户端每 30 秒发送 `ping`,服务端回复 `pong`。超过 60 秒无 `ping`,服务端判定连接断开并清理会话资源。 + +**重连策略**(指数退避 + 抖动): + +```typescript +function reconnect(attempt: number) { + const delay = Math.min(1000 * Math.pow(2, attempt), 30000); // 最大 30s + const jitter = Math.random() * 1000; + setTimeout(() => connect(), delay + jitter); +} +// attempt: 0 → 1s, 1 → 2s, 2 → 4s, 3 → 8s, ... 最大 30s +``` diff --git a/docs/04-技术选型.md b/docs/04-技术选型.md new file mode 100644 index 0000000..1740ed3 --- /dev/null +++ b/docs/04-技术选型.md @@ -0,0 +1,178 @@ +# 技术选型 + +## 概述 + +本文档记录项目中各项技术的**选型过程、替代方案对比和决策理由**。技术选型没有"绝对正确",只有"更适合"。 + +**定位**:持久化部分是拓展选型,不阻塞 MVP(MVP 用 Redis 即可)。前端边缘处理部分是 MVP 阶段就需要确定的技术栈。 + +``` +技术选型 +├── 持久化层 → 数据库选型: PostgreSQL +└── 前端边缘处理层 + ├── 边缘推理: ONNX Runtime Web + ├── 语音检测: @ricky0123/vad-web + └── 媒体采集: MediaDevices API +``` + +--- + +## 一、持久化层选型 + +### 数据特征分析 + +| 数据 | 结构特征 | 读写模式 | 数据量级 | +|------|---------|---------|---------| +| 对话消息 | 强结构化 | 写多读少,按会话聚合读取 | 中(每用户日均 ~100 条) | +| 会话元信息 | 强结构化 | 写少读少 | 低 | +| 对话上下文 | 半结构化 JSON | 高频读写,TTL 过期 | 低(仅当前窗口) | +| 用量统计 | 强结构化 | 写多,定期聚合读 | 低(日粒度汇总后很小) | +| 用户偏好 | 强结构化 KV | 写极少读少 | 极低 | +| 关键帧图像 | 非结构化二进制 | 写少,按需读 | 大(单张 100KB~1MB) | + +核心数据(对话、会话、统计)都是**强结构化**的,关系型数据库天然适配。"对话上下文"是半结构化 JSON,需要数据库对 JSON 有良好支持。 + +### 候选方案对比 + +| 维度 | PostgreSQL | MySQL | SQLite | MongoDB | TiDB | +|------|-----------|-------|--------|---------|------| +| 数据模型 | 关系型 + JSONB | 关系型 | 关系型(嵌入式) | 文档型(BSON) | 关系型(分布式) | +| JSON 支持 | JSONB 原生索引 | JSON 类型,索引弱 | 无原生支持 | 天生擅长 | 兼容 MySQL JSON | +| 关联查询 | 强 | 强 | 强 | 弱(需 $lookup) | 强 | +| 聚合统计 | 窗口函数/CTE | 基础聚合 | 基础聚合 | 聚合管道 | 强 | +| 并发能力 | 高(MVCC) | 中 | 低(单写锁) | 高 | 极高(分布式) | +| Go 生态 | pgx / GORM | go-sql-driver | go-sqlite3 | mongo-go-driver | 兼容 MySQL 驱动 | + +### 淘汰理由 + +**SQLite** — 写锁是全局的,并发写入会频繁锁等待。多个 Go Gateway 实例无法共享同一 SQLite 文件。适合单机桌面应用,不适合 Web 服务。 + +**MySQL** — JSON 类型索引能力弱,无法对 JSON 内部字段高效查询。缺少 `gen_random_uuid()` 等原生函数。如果团队只熟悉 MySQL,MVP 阶段完全可用,后续复杂查询会比 PostgreSQL 麻烦。 + +**MongoDB** — `messages` 需按 `session_id` 关联 `sessions`,MongoDB 中要用 `$lookup`,写法复杂且性能不如 SQL JOIN。用量统计的"按天聚合"用 SQL 一句话搞定,MongoDB 聚合管道代码量多 3-5 倍。 + +**TiDB** — 部署复杂(至少 3 PD + 3 TiKV + 2 TiDB),单机 PostgreSQL 完全够用,过度设计。 + +### 选择 PostgreSQL 的理由 + +| 项目需求 | PostgreSQL 匹配点 | +|---------|------------------| +| 强结构化数据 | 原生关系型,SQL 标准完备 | +| JSON 半结构化 | JSONB 支持索引、路径查询、部分更新 | +| messages ↔ sessions 关联 | 完整的 FK 约束 + JOIN | +| 用量按天/周/月聚合 | 窗口函数、CTE、`DATE_TRUNC` | +| Go 后端对接 | pgx 驱动性能优秀,GORM/Ent 支持成熟 | +| 未来全文搜索 | 内置 `tsvector`,无需额外引入 ES | + +### Go 集成示例 + +```go +import "github.com/jackc/pgx/v5/pgxpool" + +pool, _ := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/vision_ai") + +func SaveMessage(ctx context.Context, pool *pgxpool.Pool, msg *Message) error { + _, err := pool.Exec(ctx, + `INSERT INTO messages (session_id, role, content, image_url, tokens_used) + VALUES ($1, $2, $3, $4, $5)`, + msg.SessionID, msg.Role, msg.Content, msg.ImageURL, msg.TokensUsed, + ) + return err +} + +func GetWeeklyUsage(ctx context.Context, pool *pgxpool.Pool, userID string) ([]UsageRow, error) { + rows, _ := pool.Query(ctx, + `SELECT date, llm_tokens, estimated_cost + FROM usage_daily + WHERE user_id = $1 AND date >= CURRENT_DATE - INTERVAL '7 days' + ORDER BY date`, userID) + defer rows.Close() + // ... scan rows +} +``` + +JSONB 包容查询: + +```sql +SELECT id, content, created_at +FROM messages +WHERE role = 'user' + AND content @> '{"text": "花"}' +ORDER BY created_at DESC +LIMIT 20; +``` + +### 冷热分离架构 + +``` +Go Gateway + ├── 写入路径 → Redis(实时会话状态) + │ → PostgreSQL(对话历史 + 用量) + └── 读取路径 → Redis(当前上下文,快) + → PostgreSQL(历史记录,慢) +``` + +建议异步写入——实时消息先写 Redis(快),异步批量刷入 PostgreSQL(慢),不影响对话体验。 + +### 决策流程 + +``` +需要持久化? + ├── 否 → 继续用 Redis + └── 是 → 数据强结构化? + ├── 否, 高度嵌套 → 考虑 MongoDB + └── 是 → 数据量级? + ├── < 100GB, 单机可扛 → PostgreSQL + ├── 海量, 需水平扩展 → TiDB / CockroachDB + └── 极小, 单文件即可 → SQLite +``` + +--- + +## 二、前端边缘处理层选型 + +### 总览 + +| 能力 | 当前选型 | 选择理由 | +|------|---------|---------| +| 边缘推理 | ONNX Runtime Web | 通用推理引擎,模型无关,WASM 加速 | +| 语音检测 | @ricky0123/vad-web | 包装原生 WebRTC VAD,零延迟,体积极小 | +| 媒体采集 | MediaDevices API | 浏览器原生接口,无中间层,零依赖 | + +### 边缘推理:ONNX Runtime Web + +| 方案 | 特点 | 适用场景 | +|------|------|---------| +| **ONNX Runtime Web** | 通用推理引擎,支持任意 ONNX 模型,WASM 加速 | 自定义模型 pipeline | +| TensorFlow.js | Google 生态,WebGL/WebGPU 加速 | 模型本身就是 TF 格式 | +| MediaPipe | 开箱即用 CV 任务 | 只需常见 CV 任务,不需自定义模型 | +| Transformers.js | HuggingFace 生态 | 快速集成预训练模型 | + +项目需要同时跑 VAD 和关键帧检测两种自定义模型。ONNX 是跨框架通用格式,核心优势是**模型无关**。 + +### 语音检测:@ricky0123/vad-web + +| 方案 | 特点 | 适用场景 | +|------|------|---------| +| **@ricky0123/vad-web** | 基于 WebRTC VAD,~100KB 含 WASM,纯前端零延迟 | "有没有人说话"二分类 | +| Web Audio API + 能量检测 | AnalyserNode 计算 RMS | 极简但不抗噪 | +| Silero VAD (ONNX) | 神经网络级 VAD | 嘈杂环境需更精准 | +| Picovoice Porcupine | 商业级唤醒词引擎 | 需要唤醒词功能 | + +vad-web 是"够用且最轻"的平衡点——直接包装浏览器原生 WebRTC VAD 算法。 + +### 媒体采集:MediaDevices API + +`navigator.mediaDevices.getUserMedia()` 是所有浏览器音视频采集的**唯一标准入口**。所有上层封装库底层都是调这个 API。项目需要原始 MediaStream,用封装库反而要多一层解包。 + +### 选型共同逻辑 + +三个技术选择的共同决策模式——**选择最薄的抽象层**: + +| 技术 | "最薄"体现在 | +|------|------------| +| ONNX Runtime Web | 不绑定特定框架,模型格式通用 | +| @ricky0123/vad-web | 包装原生 WebRTC VAD,没有多余的模型加载 | +| MediaDevices API | 直接用浏览器原生接口,不加封装层 | + +与"前端做轻量预处理"原则一致:前端层只需采集和判断"有没有值得发给后端的数据"。 diff --git a/docs/05-用户故事.md b/docs/05-用户故事.md new file mode 100644 index 0000000..f1c70aa --- /dev/null +++ b/docs/05-用户故事.md @@ -0,0 +1,58 @@ +# 用户故事 + +## 概述 + +用户故事按**优先级分层**,标注哪些属于 MVP 范围、哪些可后续迭代。 + +## 核心用户故事列表 + +### P0 - MVP 必做 + +| 编号 | 用户故事 | 验收标准 | +|------|---------|---------| +| US-01 | 对着摄像头提问"这是什么",AI 能识别画面中的物体并回答 | 准确识别常见物体,响应 < 3s | +| US-02 | 用语音与 AI 对话,无需打字 | VAD 准确检测语音,STT 准确率 > 95% | +| US-03 | AI 能"看到"摄像头拍到的画面 | 每次提问时自动捕获当前帧 | +| US-04 | AI 用语音回答,而不仅是文字 | TTS 自然流畅,延迟 < 1s | + +### P1 - 增强体验 + +| 编号 | 用户故事 | 验收标准 | +|------|---------|---------| +| US-05 | AI 能持续"看着"画面,主动提示重要变化 | 关键帧检测 + 主动推送 | +| US-06 | AI 能读出画面中的文字(OCR) | 中英文混合识别准确率 > 90% | +| US-07 | 连续对话时 AI 能记住上下文 | 支持多轮对话,上下文窗口 > 10 轮 | + +### P2 - 进阶探索 + +| 编号 | 用户故事 | 验收标准 | +|------|---------|---------| +| US-08 | 视障用户:AI 描述周围环境并提示障碍物 | 实时环境描述 + 安全警告 | +| US-09 | AI 翻译画面中的外语内容 | 支持主流语言实时翻译 | +| US-10 | 切换 AI 的"观察模式"和"对话模式" | 一键切换,模式状态清晰可见 | + +## 用户旅程示例(US-01) + +``` +用户打开应用, 授权摄像头 + → 启动摄像头预览 + → 对着花朵说 "这是什么花" + → VAD 检测语音结束 + → 捕获当前帧 + STT 识别 + → 发送图像 + "这是什么花" 到 LLM + → LLM 返回 "这是一朵红色的玫瑰..." + → TTS 合成语音 + → 播放语音回答 +``` + +> "响应 < 3s"这个验收标准约束了整条链路——帧采样、网络传输、LLM 推理、TTS 合成都必须在这个预算内完成。用户故事的价值:**用体验目标倒推技术方案**。 + +## 优先级决策依据 + +用两个维度交叉评估: +- **用户价值**:这个功能对用户有多大帮助? +- **实现成本**:需要多少开发工作量和 API 调用成本? + +P0 = 高价值 + 合理成本(MVP 必须有) +P1 = 高价值 + 较高成本(第二版加入) +P2 = 探索性(验证后再投入) diff --git a/docs/06-语音交互.md b/docs/06-语音交互.md new file mode 100644 index 0000000..6b39d6f --- /dev/null +++ b/docs/06-语音交互.md @@ -0,0 +1,77 @@ +# 语音交互 + +## 概述 + +语音交互全链路:**VAD(语音活动检测)** → **STT(语音转文字)** → **LLM 推理** → **TTS(文字转语音)**。 + +用户感知延迟 = VAD 响应 + STT 耗时 + LLM 首 token + TTS 首包。人类对话中停顿超过 300ms 就会感到"对方在想"。 + +**延迟目标**:端到端 **1.5~2 秒**(用户说完话到听到 AI 回应);流式 TTS 下,LLM 开始生成后 **0.5 秒**听到第一个词。 + +## 全链路 + +``` +麦克风 → VAD → STT → LLM → TTS → 扬声器 +``` + +## 环节一:VAD(语音活动检测) + +从持续音频流中检测"人什么时候在说话",避免将环境噪音当作有效输入。**浏览器端完成**,节省 ~70% 带宽。 + +```typescript +import { MicVAD } from "@ricky0123/vad-web"; + +const vad = await MicVAD.new({ + onSpeechStart: () => console.log("用户开始说话"), + onSpeechEnd: (audio) => { + // audio: Float32Array,送入 STT + sendToSTT(audio); + }, + positiveSpeechThreshold: 0.5, // 检测灵敏度 + minSpeechDuration: 250 // 最短语音时长 ms +}); + +vad.start(); +``` + +## 环节二:STT(语音转文字) + +| 方案 | 延迟 | 成本 | 特点 | +|------|------|------|------| +| Whisper API | 1-3s | 按分钟计费 | 准确率高,支持多语言 | +| **Deepgram** | <500ms | 按分钟计费 | 流式识别,延迟极低 | +| 浏览器原生 | ~1s | 免费 | 中文效果一般 | +| FunASR | <500ms | 自部署免费 | 阿里开源,中文优化 | + +流式 STT 是低延迟的关键——不必等用户说完,边说边识别: + +```typescript +// Deepgram 流式识别示例 +const ws = new WebSocket("wss://api.deepgram.com/v1/listen", { + headers: { Authorization: `Token ${API_KEY}` } +}); + +ws.onmessage = (event) => { + const { transcript, is_final } = JSON.parse(event.data).channel.alternatives[0]; + if (is_final) { + onFinalTranscript(transcript); // 一句完整语音,送入 LLM + } +}; +``` + +## 环节三:TTS(文字转语音) + +流式 TTS:检测 LLM 输出中的句子边界,每检测到一句就立即送入 TTS 合成并播放,不必等全部生成完。 + +方案选择: +- **OpenAI TTS**:音质好,延迟中等,按字符计费 +- **Edge TTS**:微软免费方案,音质不错,延迟略高 +- **Fish Speech / CosyVoice**:开源方案,支持声音克隆,可自部署 + +## 延迟优化要点 + +- VAD 浏览器端处理(减少无效传输) +- 流式 STT(边说边识别) +- LLM 流式输出 +- TTS 句子级流式合成 +- STT 与上下文准备并行处理 diff --git a/docs/07-视觉理解.md b/docs/07-视觉理解.md new file mode 100644 index 0000000..b39fae1 --- /dev/null +++ b/docs/07-视觉理解.md @@ -0,0 +1,73 @@ +# 视觉理解 + +## 概述 + +从摄像头视频流到 AI 语义理解的技术链路:**帧采样** → **图像编码** → **多模态 LLM** → **语义结果**。 + +摄像头每秒 30 帧,全部送入 LLM 不现实也不经济,帧采样是第一个需要解决的问题。 + +## 帧采样策略 + +| 策略 | 原理 | 适用场景 | +|------|------|---------| +| 固定间隔采样 | 每 N 秒取一帧 | 画面变化缓慢 | +| 关键帧检测 | 对比相邻帧差异,变化超阈值时触发 | 画面动态变化较多 | +| 事件驱动采样 | 用户主动触发(如拍照按钮) | 精确提问场景 | +| **混合策略** | 低频定时 + 高频事件触发 | **通用推荐方案** | + +关键帧检测核心逻辑: + +```python +import numpy as np + +def is_keyframe(prev_frame, curr_frame, threshold=30): + """通过帧间像素差异判断是否为关键帧""" + diff = np.mean(np.abs(prev_frame.astype(int) - curr_frame.astype(int))) + return diff > threshold +``` + +> 实际开发中,先降低分辨率(如 320x240)做关键帧检测,再对命中帧保留原始分辨率送入 LLM,兼顾速度与精度。 + +## 图像编码与多模态输入 + +主流多模态 LLM(GPT-4o、Claude)接受图片的两种方式: + +| 方式 | 适用场景 | +|------|---------| +| Base64 内联 | 本地/实时场景 | +| URL 引用 | 已有图床的场景 | + +OpenAI 兼容接口调用示例: + +```typescript +const response = await openai.chat.completions.create({ + model: "gpt-4o", + messages: [ + { + role: "user", + content: [ + { type: "text", text: "请描述画面中的内容" }, + { + type: "image_url", + image_url: { + url: `data:image/jpeg;base64,${base64Image}`, + detail: "low" // "low" | "high" | "auto" + } + } + ] + } + ] +}); +``` + +**detail 参数影响**: +- `low`:65x65 缩略图,约 85 tokens,适合快速识别 +- `high`:按 512px 方块切分,细节丰富但 token 数激增 +- 实时对话场景建议默认 `low`,仅在用户追问细节时切换 `high` + +## 视觉理解的局限性 + +- **运动模糊**:快速移动物体在低帧率下容易模糊 +- **光线变化**:逆光、暗光环境下识别率显著下降 +- **细小文字**:低分辨率下 OCR 能力受限 +- **空间推理**:精确的距离、尺寸判断仍是短板 diff --git a/docs/08-成本控制.md b/docs/08-成本控制.md new file mode 100644 index 0000000..a008e61 --- /dev/null +++ b/docs/08-成本控制.md @@ -0,0 +1,77 @@ +# 成本控制 + +## 概述 + +实时视频流 + 多模态 LLM 推理的成本极易失控。从**视觉链路**、**语音链路**、**推理链路**三个维度梳理成本控制策略,核心思想是**端云协同**——将适合的计算前置到客户端,降低对云端 API 的依赖。 + +**成本对比**:优化前(1fps 全量发送)vs 优化后(0.2fps + 端侧筛选 + 模型分级)→ 月成本从 **$5000 降至 $300~500**,降幅约 90%。 + +## 成本构成 + +``` +总成本 +├── 视觉链路:图像编码与传输、视觉 token 消耗 +├── 语音链路:STT 按分钟计费、TTS 按字符计费 +└── 推理链路:LLM 输入 tokens、LLM 输出 tokens +``` + +假设:10 分钟/天/用户,1fps,每次 1000 tokens → 一天 60 万 tokens。1000 用户时成本不可控。 + +## 策略一:智能采样——少发图,发好图 + +| 策略 | 降本幅度 | 实现复杂度 | 说明 | +|------|---------|-----------|------| +| 提高采样间隔 | 高 | 低 | 从 1fps 降到 0.2fps | +| 关键帧过滤 | 中 | 中 | 画面不变时不发送 | +| 用户触发 | 高 | 低 | 只在用户提问时拍照 | +| 本地预筛选 | 中 | 高 | 用轻量模型判断"是否值得问 LLM" | + +```typescript +// 混合策略:定时低频 + 事件高频 +const NORMAL_INTERVAL = 5000; // 正常 5 秒一帧 +const ACTIVE_INTERVAL = 1000; // 用户说话时 1 秒一帧 + +let isUserSpeaking = false; + +setInterval(() => { + captureAndSend(isUserSpeaking ? "low" : "high"); +}, isUserSpeaking ? ACTIVE_INTERVAL : NORMAL_INTERVAL); +``` + +## 策略二:端云协同——把计算推到边缘 + +不是所有计算都需要上云。可前置到客户端的计算: + +- **VAD 语音检测**:浏览器端完成,减少无效音频上传(节省 ~70% 带宽) +- **人脸/物体检测**:用 ONNX Runtime 跑轻量模型(如 YOLOv8-nano ~6MB,推理 ~30ms),只在检测到新物体时触发 LLM +- **重复画面过滤**:计算帧间相似度,相似度 > 90% 直接跳过 +- **敏感内容过滤**:NSFW 检测前置,避免无效 API 调用 + +## 策略三:模型分级——用对模型做对事 + +不是每个问题都需要最贵的模型: + +``` +用户提问 → 问题复杂度判断 + ├── 简单识别 → GPT-4o-mini ($0.15/1M tokens) + ├── 深度分析 → GPT-4o ($2.5/1M tokens) + └── 代码/推理 → o1 ($15/1M tokens) +``` + +```typescript +async function routeQuery(image: string, question: string) { + const complexity = await classifyComplexity(question); + const modelMap = { + simple: "gpt-4o-mini", // "这是什么?" + moderate: "gpt-4o", // "分析这张图" + complex: "o1" // "推理/规划" + }; + return callLLM(modelMap[complexity], image, question); +} +``` + +## 策略四:缓存与复用 + +- **语义缓存**:相似问题直接返回缓存结果(如反复问"这是什么") +- **上下文复用**:连续对话中,未变化的图像不必重复发送 +- **Prompt 压缩**:精简 system prompt,减少每轮的固定 token 开销 diff --git a/docs/09-技术名词解释.md b/docs/09-技术名词解释.md new file mode 100644 index 0000000..663baaa --- /dev/null +++ b/docs/09-技术名词解释.md @@ -0,0 +1,36 @@ +# 技术名词解释 + +对架构文档中技术选型表里出现的所有关键名词的简明解释。 + +--- + +## 前端相关 + +| 名词 | 一句话 | 展开 | +|------|--------|------| +| **React 18** | 组件化 UI 框架 | Facebook 开源,把页面拆成组件搭积木拼装。18 版本支持并发渲染。 | +| **TypeScript** | 带类型的 JavaScript | 在 JS 基础上增加类型声明,编译阶段就能发现类型错误。 | +| **Vite** | 前端构建工具 | 利用浏览器原生 ES Module,开发时毫秒级热更新(HMR),构建产物小。 | +| **WebSocket** | 浏览器与服务器的双向通道 | HTTP 是"一问一答",WebSocket 像打电话——接通后双方随时互发消息,适合实时对话场景。 | +| **ONNX Runtime Web** | 浏览器端 AI 推理引擎 | 微软定义的通用模型格式 ONNX 的运行引擎,可在浏览器中用 WASM 加速跑轻量模型(如 VAD、关键帧检测),零延迟、不耗服务器资源。 | +| **VAD** | 语音活动检测 | Voice Activity Detection,检测"人有没有在说话"。WebRTC 内置了高效的 VAD 算法,本项目用 @ricky0123/vad-web 包装。 | +| **MediaDevices API** | 浏览器摄像头/麦克风接口 | `navigator.mediaDevices.getUserMedia()` 是浏览器音视频采集的唯一标准入口,无需插件。 | + +## 后端相关 + +| 名词 | 一句话 | 展开 | +|------|--------|------| +| **Go (Golang)** | 高并发后端语言 | Google 开发,杀手锏是 goroutine——极轻量协程,一个程序可轻松开几万个,每个只占几 KB 内存,适合管理大量 WebSocket 长连接。 | +| **gorilla/websocket** | Go WebSocket 库 | Go 标准库无内置 WebSocket 支持,此库是社区最成熟的选择,处理了协议握手、帧解析等底层细节。 | +| **Redis** | 内存 KV 数据库 | 数据放在内存里,读写微秒级。本项目用于会话状态和对话上下文缓存,支持 TTL 过期自动清理。多 Gateway 实例通过 Redis 共享状态。 | +| **Viper** | Go 配置管理 | 读取 JSON/YAML/TOML 配置,支持环境变量覆盖,方便开发/测试/生产环境用不同配置。 | +| **Zap** | Go 结构化日志 | Uber 开源,输出 JSON 格式日志,方便工具搜索分析,性能远超标准库 log。 | + +## AI 服务相关 + +| 名词 | 一句话 | 展开 | +|------|--------|------| +| **多模态 LLM** | 能读文字又能看图片的大语言模型 | GPT-4o(OpenAI)/ Claude Sonnet(Anthropic),给照片+问题能"看懂"照片再回答。 | +| **STT** | 语音转文字 | Speech-to-Text。Deepgram 流式识别延迟 <500ms。备选 FunASR(阿里开源,可自部署)。 | +| **TTS** | 文字转语音 | Text-to-Speech。OpenAI TTS 音质接近真人。Edge TTS 免费。支持流式——边生成边读,不必等全部生成完。 | +| **GPT-4o-mini** | 轻量分类模型 | 又快又便宜的小模型,用于模型路由——先用小模型判断问题复杂度,简单问题走小模型省 API 费用。 | diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..0903535 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,27 @@ +# CamTalk 设计文档 + +CamTalk 是一款多模态实时 AI 视觉对话助手。用户通过摄像头和麦克风与 AI 交互,AI 理解视觉场景和语音输入后给出自然回应。 + +## 文档索引 + +| 文档 | 说明 | +|------|------| +| [01-项目概述](01-项目概述.md) | 项目目标、核心挑战、交付物 | +| [02-系统架构](02-系统架构.md) | 三层架构、技术栈、核心交互流程、前后端模块、存储策略、部署架构 | +| [03-接口文档](03-接口文档.md) | WebSocket 协议、REST API、数据模型、错误码、连接管理(**实现时首先阅读**) | +| [04-技术选型](04-技术选型.md) | 持久化层(PostgreSQL)和前端边缘处理层的选型对比与决策理由 | +| [05-用户故事](05-用户故事.md) | P0/P1/P2 用户故事、验收标准、优先级决策依据 | +| [06-语音交互](06-语音交互.md) | VAD → STT → LLM → TTS 全链路、延迟优化 | +| [07-视觉理解](07-视觉理解.md) | 帧采样策略、图像编码、多模态 LLM 输入机制 | +| [08-成本控制](08-成本控制.md) | 智能采样、端云协同、模型分级、缓存复用 | +| [09-技术名词解释](09-技术名词解释.md) | 前端/后端/AI 服务技术名词简明解释 | + +## 推荐阅读顺序 + +1. **01-项目概述** — 了解项目目标 +2. **02-系统架构** — 理解三层架构和技术栈全貌 +3. **03-接口文档** — 前后端通信契约,实现时的最高依据 +4. **04-技术选型** — 了解为什么选这些技术 +5. **05-用户故事** — 明确功能优先级 +6. **06~08** — 各技术领域的详细设计 +7. **09-技术名词解释** — 遇到不熟悉的名词时查阅