轻量级口播数字人系统 (Talking Avatar Platform)
技术需求与架构文档 (V2.0)
1. 项目概述与业务目标
端到端 AI 口播数字人平台:管理后台上传形象图片并选择 Edge-TTS 音色创建数字人, 系统先预处理生成基础驱动视频(LivePortrait,一次性),之后离线播报与实时直播都只跑 轻量推理:Edge-TTS 语音合成 → Wav2Lip(ONNX) 口型,输出带声音的视频/直播流。
观众端为独立客户端项目(Next.js + TailwindCSS):查看开播数字人、进入直播间观看、 以游客/账号身份发消息,消息经 LLM(DeepSeek Responses) 生成回复后由数字人实时 说出,聊天记录持久化。
2. 系统架构设计
AI 渲染任务消耗算力,系统采用微服务分离 + 容器化编排 + 宿主机原生 Worker 的混合 部署:基础设施、API、前端、SRS 在 Docker 内;真实 AI Worker 在宿主机原生运行 (macOS MPS/CoreML、Linux CUDA、Linux AMD ROCm)。
| 模块 | 职责与功能 | 核心技术栈 |
|---|---|---|
| 管理端前端 | 数字人创建/列表、播报制作、直播台、任务中心、用户列表。 | React + TypeScript + Vite + Tailwind + shadcn/ui(shadcn-admin) |
| 观众端前端 | 独立项目:开播列表(分类筛选)、直播间(xgplayer 拉流 + 聊天)。 | Next.js 16 + TailwindCSS 4 + xgplayer |
| API 控制面 | 拆分为三个微服务:管理端 CRUD/登录、观众端直播/聊天/LLM 编排、Worker 回调调度。 | Go + Gin + GORM + AWS S3 SDK v2(cmd/api-admin / cmd/api-live / cmd/api-telegram / cmd/api-web / cmd/api-scheduler) |
| AI Worker | 监听队列,下载素材,LivePortrait/Edge-TTS/Wav2Lip 推理,离线 MP4 / 直播推流。 | Python 3.11 + uv,boto3 + Redis |
| 知识库微服务 | 数字人私有知识:入库切块、Jieba 中文全文检索、按知识库/文档删除、分块查看。 | service-rag:FastAPI + uv + zvec(进程内全文索引),零模型依赖,:8001 |
| TTS 微服务 | Edge-TTS 语音合成 → 16kHz/16bit/单声道 PCM WAV → 上传 S3,只返回 key。 | service-tts:FastAPI + uv + edge_tts.Communicate(async)+ ffmpeg,:8002 |
| 直播网关 | RTMP 接收与 HTTP-FLV 分发。 | SRS v5(Docker) |
| 对象存储 | 统一管理图片、base 视频、成品 MP4。 | S3 兼容(RustFS) |
| 中间件与数据 | 任务/直播队列、会话与聊天记录持久化、知识库元数据。 | MariaDB 11 + Redis 8.2.2-alpine + RustFS + volume rag-zvec-data |
3. 数据流转管线
3.1 创建(预处理,一次)
- 管理端上传形象图片 + 选择音色 + 分类,
POST /api/avatars直传对象存储,状态initializing。 - Worker 消费
avatar_init队列 → LivePortrait 生成静音 24fps base 视频(默认 去眨眼、保留耸肩,约 3s 一次)→ 上传 S3 → Webhook 回写base_video_s3_key置为ready。
3.2 使用(离线播报)
- 提交脚本,
POST /api/tasks入 Redis 队列。 - Worker:Edge-TTS 合成语音 → Wav2Lip(ONNX) 按 base 视频逐帧对口型 → mux 音频成 MP4 → 上传 S3 → Webhook 更新状态。
- 管理端轮询
GET /api/tasks/:id,完成后用 xgplayer 播放。
3.3 使用(实时直播)
POST /api/live/:id/start:登记 LiveSession,通知 Worker 打开常驻 FFmpeg 双管道(视频 BGR24 stdin + 音频 /dev/fd/3)推流 SRS;控制消息携带idleVideos(所选场景的全部视频 S3 Key)+idleSwitchMode/idleSwitchSeconds, 闲置态按定时 N 秒顺序或随机(5-30s)在这些视频间切换 + 静音。- 观众端/直播台发文字:
POST /api/live/:id/message→ Go 按avatar_id查avatar_knowledge中该数字人enabled=true的集合,按collection_ids从 service-rag 检索 Top-3 + 取最近 10 条房间消息 → 调 OpenAI SDK → DeepSeek Responses(deepseek-v4-flash)生成回复 → 按句切块入live_queue:{id};用户消息与机器人回复同时写入live_messages。 - Worker 逐句弹出:Edge-TTS → Wav2Lip 内存出帧(口型基于当前正在显示的那段 场景视频切片,不再固定默认视频)→ 按配置叠加字幕 → 口型帧 + TTS 音频替换推流; 句子播完从该视频衔接处回闲置,管道不关闭。TTS 飞行期间不丢队列句子(预取下一句)。
- 直播按 Avatar 的
live_settings(JSON)决定字幕开关/字体/位置/边框/字号,以及 闲置推流场景(idleSceneId)与切换方式。
3.4 知识库(RAG)数据流转
- 管理端
POST /api/knowledge-collections创建全局知识库(不归属单个 数字人,多个数字人可共用);在数字人编辑页GET/POST /api/avatars/:id/knowledge-selection勾选绑定(avatar_knowledge表,enabled开关)。 - 在知识库内
POST /api/knowledge-collections/:id/documents添加文档 (粘贴文本或上传 .txt/.pdf):源文件入 S3,Go 提取文本(PDF 用ledongthuc/pdf)→ 同步 POST service-rag/v1/knowledge/ingest(携带collection_id/source_id,avatar_id 恒为 0)→ zvec 按 ~300 字/50 字重叠 切块建立 Jieba 全文索引 → 状态置indexed。 - 直播/检索:
/v1/knowledge/search按collection_id(管理端测试)或collection_ids(直播聊天按该数字人绑定的全部集合)做 BM25 Top-3。 - 删除:删文档/知识库时同步调
/v1/knowledge/delete清理对应索引; 管理端可POST .../documents/:did/chunks查看文档实际分块。 - 数据持久化在 volume
rag-zvec-data:/app/zvec_data;集合avatar_knowledge字段:avatar_id/collection_id(倒排索引)chunk_text(STRING, FTS jieba)。
3.5 TTS 合成(S3 共享存储)
POST /v1/tts/synthesize:{"text": "...", "voiceId": "zh-CN-XiaoxiaoNeural"}。edge_tts.Communicate(async API,不用 os.system/subprocess)合成到本地 临时文件 →ffmpeg(async subprocess)转 16kHz / 16-bit / mono PCM WAV (Wav2Lip 唯一可处理的格式)。boto3经asyncio.to_thread上传 RustFS(不阻塞事件循环),S3 配置全部来自 环境变量(S3_ENDPOINT/S3_BUCKET/S3_ACCESS_KEY/S3_SECRET_KEY)。- 响应只返回
s3_key+ 元数据(sample_rate/channels/bits/duration_sec); 临时文件在finally中删除,容器磁盘不堆积。
3.6 Telegram Mini App(登录 + Webhook 自动注册)
frontend/telegram(Vite + React + Tailwind +@twa-dev/sdk)在main.tsx执行WebApp.ready(),App.tsx取WebApp.initDataPOST 到POST /api/auth/telegram(VITE_API_ORIGIN构建期注入,可指向 ngrok api-gateway 的 https 地址)。- api-telegram 用
TG_BOT_TOKEN校验 initData(HMAC-SHA-256:secret 由HMAC("WebAppData", token)派生,除hash外字典序key=value串签名, 24h 时效)→ 按telegram_idupsertlive_users/telegram_users(非游客,HttpOnlytg_uidcookie)→ 返回{userId, username, ...}。 - 启动注册(后台 goroutine,不阻塞 Gin):webhook 地址优先
TG_WEBHOOK_URL, Mini App 地址优先TG_MINIAPP_URL;缺失项轮询NGROK_API_URL(默认http://ngrok:4040/api/tunnels,2s × 10 重试)取 对应隧道公网地址(webhook→api-gateway+/api/telegram/webhook, 菜单按钮→tg-app),再setWebhook/setChatMenuButton;两者都有则 跳过 ngrok。日志标注每个 URL 的来源(Environment / ngrok)。 POST /api/telegram/webhook为占位端点(返回 200);管理端 nginx 将/api/telegram/webhook与/api/auth/telegram单独路由到 api-telegram。
4. 核心数据字典 (Database Schema)
Table: avatars(数字人)
id/name/image_s3_key/category(分类)/voice_id(Edge-TTS 音色)persona(JSON:年龄/身高/体重/族裔/感情状态/性格,注入 LLM 提示词)status(initializing/ready/failed/skipped)live_settings(JSON:字幕开关、字体文件名、位置、边框、字号 + 默认推流idleSceneId/idleSwitchMode(interval|random)/idleSwitchSeconds)created_at
Table: scenes(场景)+ scene_videos(场景视频)
scenes:id/avatar_id(FK) /title/description/cover_s3_key/is_default(默认场景,创建时生成、不可删)scene_videos:id/scene_id(FK) /avatar_id/s3_key/description/is_default(默认视频,直播/播报兜底)- 旧
base_video_s3_key与avatar_videos表已由启动迁移并入默认场景。
Table: broadcast_tasks(离线播报任务)
id/avatar_id(FK) /script_text/status(pending/processing/completed/failed)progress(0-100) /stage(tts/lipsync/mux) /scene_id+scene_video_id+video_s3_key(记录用到的场景/视频,重试不混参数)/tts_s3_key(TTS 缓存复用)output_video_s3_url/error_message/created_at/updated_at
Table: live_sessions(直播会话)
id/avatar_id(unique) /stream_id(avatar_<id>) /status(idle/active)created_at/updated_at
Table: live_users/telegram_users(聊天身份)
id/username(unique) /password_hash(bcrypt,游客为空)/is_guest- 游客行注册时原地升级(同一 id 保历史);登录时游客消息合并进账号。
Table: live_messages(持久化聊天记录)
id/avatar_id/user_id/username(快照) /role(user|bot) /contentcreated_at
Table: knowledge_collections(知识库)
id/name(全局共享)/created_at/updated_at
Table: avatar_knowledge(数字人 ↔ 知识库 N:N 绑定)
avatar_id+collection_id(复合主键)/enabled(是否参与该数字人直播检索)
Table: knowledge_documents(知识库文档)
id/collection_id(FK) /content(提取后的文本)/status(pending/indexed/failed)source_key(S3 源文件)/filename/created_at
5. API 接口定义(RESTful)
数字人
POST /api/avatars:multipart(name + image + voice_id + category + persona)GET /api/avatars/GET /api/avatars/:id:列表 / 详情(含 liveSettings)PUT /api/avatars/:id:编辑(name/category/voice/persona)PUT /api/avatars/:id/live-settings:保存直播配置(字幕 + 默认推流视频)POST /api/avatars/:id/retry|skip:重新生成 / 跳过基础视频DELETE /api/avatars/:id:删除(级联任务/会话/文件)GET|POST /api/avatars/:id/scenes、PUT|DELETE /api/scenes/:id、POST /api/scenes/:id/videos、DELETE /api/scenes/:id/videos/:vid:场景/视频管理
播报任务
POST /api/tasks/GET /api/tasks/GET /api/tasks/:id/DELETE /api/tasks/:id(创建支持sceneId + videoId,列表含场景/视频/进度/阶段)POST /api/tasks/:id/retry、POST /api/tasks/:id/status(Worker Webhook)
直播
POST /api/live/:id/start|stop|push|message、GET /api/live/:id/status、GET /api/live(开播列表,含分类/字幕/闲置推流配置与idleVideos)
知识库(全局集合 + N:N 绑定 → 文档)
POST /api/knowledge-collections:创建全局知识库GET /api/knowledge-collections(q筛选)、PUT /api/knowledge-collections/:id(重命名)、DELETE /api/knowledge-collections/:id(级联删除文档与索引)GET|POST /api/avatars/:id/knowledge-selection:读取/保存该数字人绑定集合GET|POST /api/knowledge-collections/:id/documents(列表/添加文档:text或.txt/.pdf)DELETE /api/knowledge-collections/:id/documents/:did(删文档含索引)POST /api/knowledge-collections/:id/documents/:did/chunks(查看分块)POST /api/knowledge/search(在线检索测试,avatarId或collectionId)
TTS(service-tts,内网)
POST /v1/tts/synthesize:{text, voiceId}→ 16kHz PCM WAV 上传 S3, 返回{status, s3_key, metadata}(媒体文件不过 HTTP)
聊天身份与记录
POST /api/chat/guest:临时游客身份POST /api/chat/register:注册(升级当前游客行,保留历史)POST /api/chat/login:登录(合并游客消息进账号)GET /api/chat/history?avatarId=:房间持久化聊天记录GET /api/users:用户列表(游客/账号 + 消息数)
6. 直播流式架构要点
- 常驻管道:每个数字人一条 ffmpeg 子进程管道,视频带
-re实时节流,音频每 0.5s 切片交错写入(先写首片再写帧,避免双管道死锁)。 - 音画同步:口型帧与 TTS 音频来自同一 chunk,闲置/说话两态帧率分辨率一致。
- 字幕:Pillow 渲染(brew ffmpeg 无 drawtext 滤镜),字体从
worker/fonts/按文件名解析,缺失回退系统默认;开关/样式来自 Avatarlive_settings。 - LLM:OpenAI Go SDK 调 DeepSeek Responses API(base_url
https://api.deepseek.com,模型deepseek-v4-flash),无 key 时原样回读(测试); System Prompt 注入人物设定 + 按数字人聚合的 Top-3 知识 + 最近 10 条消息, 观众只发关键词时视为「想了解该主题」主动讲解。
7. 工程化与部署要求
- S3 协议唯一文件通道:Go 用 aws-sdk-go-v2、Python 用 boto3,服务间只传 S3 Key,不用本地路径。
- 容器编排:
docker-compose.yml编排 MariaDB 11 / Redis 8.2 / RustFS / api-admin / api-live / api-scheduler / 管理端 / 观众端 / SRS / service-rag / service-tts;宿主机仅暴露 3000(观众端)、8080(管理端+API)、1935(RTMP 推流)、6379(Redis)、9000(RustFS);三个 api 微服务与 service-rag/service-tts 只在内网,不发布宿主端口。nginx 将/api代理到 api-admin,Worker 回调(/api/tasks/:id/status、/api/avatars/:id/base-video) 精确代理到 api-scheduler;观众端 Next.js 服务端代理指向 api-live。 - Worker 环境:Python 3.11 + uv(
pyproject.toml+uv.lock,禁 requirements.txt);依赖组 models/cuda/rocm 互斥,ROCm 容器uv sync --inexact。 - 模型/外部代码/字体不入库:
worker/models/、worker/external/、worker/fonts/均已 gitignore,新设备用download_models.py或整目录拷贝。 - 优雅降级:
AI_MODE=mock轻量演示;无OPENAI_API_KEY时 LLM 回显原文。