Skip to content

轻量级口播数字人系统 (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 创建(预处理,一次)

  1. 管理端上传形象图片 + 选择音色 + 分类,POST /api/avatars 直传对象存储,状态 initializing
  2. Worker 消费 avatar_init 队列 → LivePortrait 生成静音 24fps base 视频(默认 去眨眼、保留耸肩,约 3s 一次)→ 上传 S3 → Webhook 回写 base_video_s3_key 置为 ready

3.2 使用(离线播报)

  1. 提交脚本,POST /api/tasks 入 Redis 队列。
  2. Worker:Edge-TTS 合成语音 → Wav2Lip(ONNX) 按 base 视频逐帧对口型 → mux 音频成 MP4 → 上传 S3 → Webhook 更新状态。
  3. 管理端轮询 GET /api/tasks/:id,完成后用 xgplayer 播放。

3.3 使用(实时直播)

  1. POST /api/live/:id/start:登记 LiveSession,通知 Worker 打开常驻 FFmpeg 双管道(视频 BGR24 stdin + 音频 /dev/fd/3)推流 SRS;控制消息携带 idleVideos(所选场景的全部视频 S3 Key)+ idleSwitchMode/idleSwitchSeconds, 闲置态按定时 N 秒顺序或随机(5-30s)在这些视频间切换 + 静音。
  2. 观众端/直播台发文字:POST /api/live/:id/message → Go 按 avatar_idavatar_knowledge 中该数字人 enabled=true 的集合,按 collection_ids 从 service-rag 检索 Top-3 + 取最近 10 条房间消息 → 调 OpenAI SDK → DeepSeek Responsesdeepseek-v4-flash)生成回复 → 按句切块入 live_queue:{id};用户消息与机器人回复同时写入 live_messages
  3. Worker 逐句弹出:Edge-TTS → Wav2Lip 内存出帧(口型基于当前正在显示的那段 场景视频切片,不再固定默认视频)→ 按配置叠加字幕 → 口型帧 + TTS 音频替换推流; 句子播完从该视频衔接处回闲置,管道不关闭。TTS 飞行期间不丢队列句子(预取下一句)。
  4. 直播按 Avatar 的 live_settings(JSON)决定字幕开关/字体/位置/边框/字号,以及 闲置推流场景(idleSceneId)与切换方式。

3.4 知识库(RAG)数据流转

  1. 管理端 POST /api/knowledge-collections 创建全局知识库(不归属单个 数字人,多个数字人可共用);在数字人编辑页 GET/POST /api/avatars/:id/knowledge-selection 勾选绑定(avatar_knowledge 表, enabled 开关)。
  2. 在知识库内 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
  3. 直播/检索:/v1/knowledge/searchcollection_id(管理端测试)或 collection_ids(直播聊天按该数字人绑定的全部集合)做 BM25 Top-3。
  4. 删除:删文档/知识库时同步调 /v1/knowledge/delete 清理对应索引; 管理端可 POST .../documents/:did/chunks 查看文档实际分块。
  5. 数据持久化在 volume rag-zvec-data:/app/zvec_data;集合 avatar_knowledge 字段:avatar_id/collection_id(倒排索引)
    • chunk_text(STRING, FTS jieba)。

3.5 TTS 合成(S3 共享存储)

  1. POST /v1/tts/synthesize{"text": "...", "voiceId": "zh-CN-XiaoxiaoNeural"}
  2. edge_tts.Communicate(async API,不用 os.system/subprocess)合成到本地 临时文件 → ffmpeg(async subprocess)转 16kHz / 16-bit / mono PCM WAV (Wav2Lip 唯一可处理的格式)。
  3. boto3asyncio.to_thread 上传 RustFS(不阻塞事件循环),S3 配置全部来自 环境变量(S3_ENDPOINT/S3_BUCKET/S3_ACCESS_KEY/S3_SECRET_KEY)。
  4. 响应只返回 s3_key + 元数据(sample_rate/channels/bits/duration_sec); 临时文件在 finally 中删除,容器磁盘不堆积。

3.6 Telegram Mini App(登录 + Webhook 自动注册)

  1. frontend/telegram(Vite + React + Tailwind + @twa-dev/sdk)在 main.tsx 执行 WebApp.ready()App.tsxWebApp.initData POST 到 POST /api/auth/telegramVITE_API_ORIGIN 构建期注入,可指向 ngrok api-gateway 的 https 地址)。
  2. api-telegram 用 TG_BOT_TOKEN 校验 initData(HMAC-SHA-256:secret 由 HMAC("WebAppData", token) 派生,除 hash 外字典序 key=value 串签名, 24h 时效)→ 按 telegram_id upsert live_users/telegram_users(非游客,HttpOnly tg_uid cookie)→ 返回 {userId, username, ...}
  3. 启动注册(后台 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)。
  4. 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(场景视频)

  • scenesid / avatar_id(FK) / title / description / cover_s3_key / is_default(默认场景,创建时生成、不可删)
  • scene_videosid / scene_id(FK) / avatar_id / s3_key / description / is_default(默认视频,直播/播报兜底)
  • base_video_s3_keyavatar_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) / content
  • created_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/scenesPUT|DELETE /api/scenes/:idPOST /api/scenes/:id/videosDELETE /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/retryPOST /api/tasks/:id/status(Worker Webhook)

直播

  • POST /api/live/:id/start|stop|push|messageGET /api/live/:id/statusGET /api/live(开播列表,含分类/字幕/闲置推流配置与 idleVideos

知识库(全局集合 + N:N 绑定 → 文档)

  • POST /api/knowledge-collections:创建全局知识库
  • GET /api/knowledge-collectionsq 筛选)、 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(在线检索测试,avatarIdcollectionId

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/ 按文件名解析,缺失回退系统默认;开关/样式来自 Avatar live_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 回显原文。

灵播 LingCast · 端到端口播数字人平台