ESC
输入关键词搜索文章
目录

数字人工程解读(一):CyberVerse:把论文拼成一个能对话的实时数字人

3服务进程
6+1gRPC 服务+消息契约
230+90Python+Go 文件
GPLv3开源协议
本篇定位:这是数字人系列的第六篇,也是从“论文”转向“工程”的一篇。前五篇我们拆解了实时数字人的生成算法(Wav2Lip、SadTalker、VASA-1、流式蒸馏)和算力选型。但一个真正能用的数字人,光有生成模型远远不够——你还需要把语音识别、大模型、语音合成、口型驱动、WebRTC 推流、记忆、工具调用全部串起来。CyberVerse 就是这样一套“把论文拼成产品”的开源框架。本文不只看架构,还会给出一条实用阅读路径:先用纯语音模式跑通,再理解三进程边界,最后再打开 Avatar 视频后端。#CyberVerse-GitHub
CyberVerse digital human agent
图 1:CyberVerse 的核心理念——"一张照片,一个活的数字人"。它不是单个生成模型,而是把语音对话、实时通话、可选数字人形象打包成可自托管的 Agent 平台。(来源:CyberVerse 官方 README, docs/assets/digital-human-agent.jpeg)
第一章
它是什么:不是模型,是"系统编排层"

CyberVerse 在 README 里的自我定位是一句话:"一个开源的实时数字人 Agent 框架,用 WebRTC、人设记忆、工具、RAG 和可选的数字人视频能力,帮你构建以语音交互为中心的 AI Agent。"#CyberVerse-GitHub

这句话每个词都有分量,但最关键的是它不是一个生成模型。前五篇里 Wav2Lip 解决"对口型"、VASA-1 解决"整脸动力学"——它们都是回答"如何从音频生成一张会动的脸"。而 CyberVerse 回答的是另一个问题:当你已经有了这些模型,如何把它们和大模型、语音识别、推流、记忆拼成一个能实时对话的完整系统?

一句话区分:论文给你"会动的脸",CyberVerse 给你"能对话的人"。前者是组件,后者是系统。

谁适合读这个项目

如果你的目标是落地一个能看、能听、能实时对话的数字人客服/数字分身/虚拟主播,而不是研究生成算法本身,那么 CyberVerse 是一份极好的工程参考。它把"实时数字人系统"该有的模块边界划得很清楚,而且每个模块都做成可替换的插件——这正是工程落地最关心的东西。

CyberVerse character gallery
图 2:CyberVerse 的角色选择界面。每个角色可以有独立的音色、欢迎语、人设配置,并支持声音克隆。(来源:CyberVerse 官方 README, docs/assets/character1.png;注:演示角色仅为示例,不随项目分发,不提供商用)
第二章
架构全景:三个进程,六个 gRPC 服务

先补齐几个基础词:WebRTC、LiveKit、gRPC、TURN

在进入源码之前,先把后面反复出现的几个工程词对齐。它们不是数字人模型本身,而是让数字人能“实时听、实时说、实时出现在浏览器里”的通信基础设施

术语全称在 CyberVerse 里的意思
WebRTCWeb Real-Time Communication浏览器原生支持的实时音视频通信技术。用户麦克风音频、数字人返回的视频/音频,最终都要靠 WebRTC 这类低延迟通道传输;它解决的是“浏览器和服务端/房间之间怎么实时通话”。
LiveKit产品名,不是缩写一套开源实时音视频基础设施,可以把 WebRTC 房间、音视频轨道、发布订阅和服务器转发封装起来。CyberVerse 里的 server/internal/livekit 就是把数字人的媒体流接进 LiveKit 房间。
SFUSelective Forwarding Unit选择性转发单元。它像一个音视频“中转站”:客户端把音视频发给 SFU,SFU 再转发给需要订阅的人。相比每两端都互相直连,SFU 更适合多人、复杂网络和生产部署。
P2PPeer-to-Peer点对点连接。两个端尽量直接建立 WebRTC 连接,链路短、结构简单,但在 NAT、防火墙或企业内网下更容易连不上。
NATNetwork Address Translation网络地址转换。家用路由器、公司内网、运营商网络经常会把内网地址映射成公网出口地址,这会让外部机器很难直接找到你的设备,因此 WebRTC 需要额外的穿透机制。
STUNSession Traversal Utilities for NATNAT 穿透的“探路服务”。它帮助客户端发现自己在公网侧暴露出来的地址和端口,优先尝试让两端直连。
TURNTraversal Using Relays around NATWebRTC 的“兜底中继”。如果 STUN 探路后仍然无法直连,双方就把音视频发到 TURN 服务器,由 TURN 代为转发;它能显著提高连通率,但会增加带宽成本和一点延迟。
gRPCgRPC Remote Procedure Calls;早期也常被理解为 Google Remote Procedure Call后端服务之间的高性能远程调用框架,通常配合 Protocol Buffers 定义接口。CyberVerse 用它连接 Go API Server 和 Python Inference Server,让 Go 像调用本地函数一样调用 Python 里的 ASR、LLM、TTS、Avatar 等推理能力。
Protocol Buffers / protoProtocol BuffersGoogle 设计的结构化数据描述和序列化格式。proto/*.proto 文件就是 CyberVerse 的跨进程接口契约:有哪些服务、方法、请求字段、返回字段,都先写在 proto 里,再生成 Go/Python 代码。

这几个词可以按链路分成两类:WebRTC / LiveKit / SFU / STUN / TURN 负责音视频怎么连通gRPC / proto 负责 Go 编排服务怎么调用 Python 推理服务。也就是说,前者是“实时通话管道”,后者是“AI 服务调用管道”。

理解 CyberVerse 的第一把钥匙,是看清它的进程边界。从 Makefile 可以看到,它要在三个终端分别启动三个服务:#CyberVerse-GitHub

进程语言启动命令职责端口
Inference ServerPythonmake inferenceAI 推理:ASR/LLM/TTS/Avatar/RAG 全部跑在这里50051 (gRPC)
API ServerGomake server会话编排、WebRTC 信令、媒体管线、任务状态8080 (HTTP/WS)
FrontendVue 3make frontendWeb UI、推流接收、设置面板5173

为什么要拆成三个进程、还用两种后端语言?这是一个非常清醒的工程决策,在它的设计文档(docs/zh-CN/features/2026-05-11-persona-agent-task-mvp.md)里说得很直白:#CyberVerse-Persona-Design

「Go Orchestrator 适合管理 session、WebSocket、Avatar 播放和任务状态,但不适合承载 Python LangGraph 生态。」

CyberVerse 设计文档

也就是说:Go 擅长高并发的连接管理和实时媒体调度,Python 擅长 AI 生态(PyTorch、LangChain、各家模型 SDK)。CyberVerse 没有勉强用一种语言通吃,而是让两边各做擅长的事,中间用 gRPC 连接。

七张 proto 文件:系统的“骨架”

进程之间怎么通信?答案在 proto/ 目录。这里有七个 .proto 文件,其中六个定义 gRPC 服务,另一个 common.proto 定义跨服务复用的 AudioChunkVideoChunkImageFrame 等消息类型。更准确地说:实时主链路依赖 streaming 接口,RAG 与部分管理接口是一元调用。这比笼统说“全部流式”更接近源码本身。#CyberVerse-GitHub

proto / gRPC 服务关键方法调用形态作用
ASRServiceTranscribeStreamstream → stream音频流 → 转录事件流
LLMServiceGenerateStreamreq → stream请求 → token 流
TTSServiceSynthesizeStream / ListVoicesstream → stream;管理接口一元文本流 → 音频流,兼顾音色列表查询
VoiceLLMServiceConverse / Interrupt / CheckVoice双向流;管理接口一元语音输入 ↔ 语音输出,支持打断与音色检查
AvatarServiceGenerateStream / SetAvatar / Reset / GetInfostream → stream;管理接口一元音频流 → 视频帧流,兼顾 Avatar 状态管理
RAGServiceSearch / IndexSource一元知识库索引与检索
common共享消息:AudioChunk / VideoChunk / ImageFrame

注意 VoiceLLMService 里有一个 Interrupt 方法——这是实时语音体验的灵魂:用户可以在数字人说话时打断它。同时,RAGService 保持一元调用也很合理:知识库索引和检索不是逐帧媒体链路,不必为了“流式”而流式。能不能优雅打断,是“玩具 demo”和“可用产品”的分水岭。

下面这张图把三个进程、六个 gRPC 服务和共享消息契约的关系画出来:

graph TD
    subgraph Browser["浏览器 (Vue 3 Frontend :5173)"]
        UI["麦克风/摄像头
视频渲染/设置面板"] end subgraph GoServer["Go API Server :8080"] WS["ws.Hub
WebSocket 信令"] ORCH["orchestrator
会话编排核心"] MEDIA["livekit / mediapeer
WebRTC 两模式"] TASK["agenttask
任务状态 (SQLite)"] ICLIENT["inference.Client
gRPC 客户端"] end subgraph PyServer["Python Inference :50051"] ASRS["ASRService"] VLLMS["VoiceLLMService"] LLMS["LLMService"] TTSS["TTSService"] AVS["AvatarService"] RAGS["RAGService"] end subgraph Models["数字人后端 models/"] FH["FlashHead"] LA["LiveAct"] end UI -->|"WebRTC 音视频"| MEDIA UI -->|"控制信令"| WS WS --> ORCH MEDIA --> ORCH ORCH --> TASK ORCH --> ICLIENT ICLIENT -->|"gRPC stream"| ASRS ICLIENT -->|"gRPC stream"| VLLMS ICLIENT -->|"gRPC stream"| TTSS ICLIENT -->|"gRPC stream"| AVS ICLIENT -->|"gRPC"| RAGS AVS --> FH AVS --> LA

从代码上可以验证这套装配关系。Go 主入口 server/cmd/cyberverse-server/main.go 几乎就是一份"组件清单":

// Create WebSocket hub
wsHub := ws.NewHub()
// Create inference gRPC client(连 Python :50051)
inferenceClient, err := inference.NewClient(cfg.Inference.Addr)
// Create LiveKit room manager(SFU 模式)
roomMgr := livekit.NewRoomManager(cfg.LiveKit.URL, ...)
// Create character store(一角色一目录)
charStore, err := character.NewStore(...)
// Agent task store(SQLite 持久化任务 + artifact)
taskStore, err := agenttask.OpenStore(taskDBPath, artifactDir)
// 把全部组件装配进 orchestrator
orch := orchestrator.New(inferenceClient, wsHub, sessionMgr, recorder, charStore, cfg.Pipeline)

可以看到,orchestrator 是 Go 侧的中枢,它持有 gRPC 客户端、WebSocket hub、会话管理器、录制器、角色存储和任务服务的引用,负责把一次对话的所有环节串起来。

尝试解读 orchestrator:它不是业务模块,而是会话总控台

进入 server/internal/orchestrator/orchestrator.go 后,最直观的观感是“文件体量很大”。但它大不是因为它在实现某一个复杂算法,而是因为它站在多个系统边界的交叉点上:浏览器的 WebSocket/WebRTC、Go 侧的 Session、Python 侧的 inference gRPC、角色存储、RAG、录制、任务事件、Avatar 视频流,都要在这里被调度。更准确地说,Orchestrator 不是 ASR、LLM、TTS 或 Avatar 本身,而是决定这些能力什么时候启动、怎么串联、何时打断、结果发给谁的会话总控台

阅读 orchestrator 的关键姿势:不要按文件从上到下一行行读。先抓入口函数,再顺着“创建会话 → 接收输入 → 选择 pipeline → 调 inference → 推音视频 → 保存状态 → 清理资源”这条主链路读。

一个常见误区是:orchestrator.go 没有一个像 main() 那样唯一的业务入口。它更像一个被 API handler、WebSocket handler 和媒体层共同调用的“调度对象”。因此,入口需要按调用层级理解:

入口层级对应函数阅读重点
整个 Go 后端启动入口server/cmd/cyberverse-server/main.go 里的 main()加载配置、创建依赖,然后调用 orchestrator.New(...) 完成总控台装配。
Orchestrator 对象创建入口New(...)关注它保存了哪些依赖:inferencewsHubsessionMgrcharStorerecorderpipelineCfg
一次会话建立入口SetupSession(...)关注它如何选择 LiveKit 或 Direct WebRTC,如何设置角色头像,如何启动 AV pipeline。
文本输入入口HandleTextInput(...)这是阅读对话逻辑的优先入口,因为它清楚地把 ModeStandardModeOmni 分开。
语音输入入口HandleAudioStream(...)关注麦克风音频如何进入 ASR 或 VoiceLLM。
WebRTC 信令入口HandleSignaling(...)处理 webrtc_readywebrtc_answerice_candidate 等浏览器信令,不是模型主链路入口。
会话清理入口TeardownSession(...)关注它如何取消 pipeline、关闭 peer、停止 silent runtime,并释放 session 资源。

因此第一遍读 orchestrator.go,可以只按这条路径走:New → SetupSession → HandleTextInput / HandleAudioStream → runStandardPipeline / runVoiceLLMPipelineWithConfig → TeardownSession。其他函数先当作工具函数或边缘能力:例如 RAG、视觉帧、待机视频、任务事件、录制持久化,等主链路通了再回头看。

可以把 Orchestrator 结构体里的字段分成五组:

字段类型代表字段它说明了什么
会话与状态sessionMgrpipelineCfgstreamingMode它要知道当前 session 是 standard 还是 omni,媒体走 direct 还是 LiveKit,以及当前 pipeline 是否应该继续。
前端通信wsHubpeersdirectPeers它负责把状态、错误、任务事件、WebRTC 信令和音视频段发回浏览器。
AI 能力调用inferenceGo 不直接跑模型,而是通过 inference client 调 Python 的 ASR、LLM、TTS、VoiceLLM、Avatar、RAG。
业务数据charStoretaskServicerecorder角色人设、头像、知识库、后台任务、录制文件和对话历史都要和会话生命周期绑定。
实时数字人细节avatarMusilentRuntimeswebrtcAPIturnServer这些字段处理的是工程落地里的硬问题:Avatar 状态互斥、待机推理、NAT 穿透、WebRTC 拥塞控制。

再看它的主入口,基本可以分成四条线:

入口函数阅读重点对应用户体验
SetupSession创建 LiveKit Bot 或 DirectPeer,设置角色头像,启动 AV pipeline。用户进入会话页后,浏览器和后端建立实时音视频连接。
HandleTextInput根据 session mode 分流到 standard 文本链路或 omni 文本链路。用户在聊天框打字,数字人生成回答并可能开口说话。
HandleAudioStream处理麦克风音频;standard 模式走 ASR,omni 模式直接走 VoiceLLM。用户直接说话,系统实时听、实时响应,并支持打断。
HandleVisualFrame校验 camera/screen JPEG 帧,并存入 session 最近视觉上下文。用户打开摄像头或共享屏幕后,模型可以“看见”最近画面。
TeardownSession取消 pipeline、关闭 peer、停止 silent runtime、释放 session 资源。用户退出房间后,后端不要继续占用 WebRTC、GPU 或任务资源。

其中最值得注意的是它同时维护了两套对话 pipeline。Standard pipeline 是传统串联:ASR → LLM → TTS → AvatarOmni / VoiceLLM pipeline 则把音频、文本甚至视觉帧一起送进实时语音模型,由模型直接流式吐出文本和音频,再按需驱动 Avatar。源码里的 handleStandardTextInputrunStandardPipelinerunStandardASRLoop 对应前者;handleVoiceLLMTextInputrunVoiceLLMPipelinewrapVoiceMultimodalInput 对应后者。

flowchart TD
    A[用户输入] --> B{Session Mode}
    B -->|Standard| C[ASR / 文本输入]
    C --> D[LLM 生成文本]
    D --> E[TTS 合成音频]
    E --> F[Avatar 生成视频]
    F --> G[MediaPeer 推送音视频]

    B -->|Omni| H[VoiceLLM / PersonaAgent]
    H --> I[流式文本 + 音频]
    I --> J{Avatar Enabled?}
    J -->|是| K[音频驱动 Avatar]
    J -->|否| L[纯语音输出]
    K --> G
    L --> G

    G --> M[浏览器播放]

这个文件里还有一个非常工程化的细节:voiceAVSyncBuffer。它不是模型能力,而是为了让语音模型输出的 PCM 音频和 Avatar 返回的视频帧按 segment 对齐。它会根据视频帧数、FPS 和采样率计算“这一段视频应该配多少音频采样”,音频不够就补静音,最后一段清尾,避免长对话中音画逐渐漂移。这类代码通常不会出现在论文 demo 里,但会决定产品体验是否稳定。

最后,orchestrator.go 也承担了很多“用户看不见但必须正确”的收尾工作:PersonaAgent 的任务事件要落库并广播,RAG 检索结果要拼进 prompt,角色头像过大要回退默认头像,session 结束要保存对话历史,用户打断时要取消旧 pipeline 并推进 playback epoch。也就是说,它真正管理的是实时交互的生命周期,而不只是“调用模型生成一句话”。

一句话总结 orchestrator:如果 inference/server.py 是“模型能力工厂”,那么 server/internal/orchestrator/orchestrator.go 就是“实时数字人导演”——它决定谁先上场、谁被打断、音视频怎么对齐、结果如何送回用户,以及会话结束后如何收尾。

继续下钻 AvatarService:音频如何变成数字人视频帧

理解 Avatar 视频链路时,不能只看 inference/services/avatar_service.py。这个文件是 Python 侧的 gRPC 适配层,真正的模型实现藏在 inference/plugins/avatar/ 下面。完整调用链路是:Go 侧 Orchestrator 决定什么时候需要 Avatar;Go inference client 打 gRPC;Python AvatarGRPCService 接住请求;最后把请求转交给当前注册的 AvatarPlugin,例如 FlashHead 或 LiveAct。

flowchart TD
    A[Orchestrator
会话编排] --> B[Go Avatar Client
gRPC 客户端] B --> C[AvatarService Proto
接口契约] C --> D[Python gRPC Server
服务注册] D --> E[AvatarGRPCService
gRPC 适配层] E --> F[PluginRegistry
获取 avatar 插件] F --> G[AvatarPlugin
插件抽象] G --> H[FlashHead / LiveAct
真实视频生成]

接口契约定义在 proto/avatar.proto,只有四个方法:SetAvatarGenerateStreamResetGetInfo。其中 GenerateStream 是双向流:Go 持续发送 AudioChunk,Python 持续返回 VideoChunk。这就是 Avatar 能被接入实时语音链路的关键——它不是等整段音频结束后再一次性返回视频,而是按 chunk 边生成边返回。

层级关键文件 / 函数职责
协议层proto/avatar.proto定义 SetAvatarGenerateStreamResetGetInfo 四个 gRPC 接口。
Go 客户端server/internal/inference/avatar_client.go把头像图片、音频 chunk 和 trace metadata 发给 Python inference server,并把返回的视频 chunk 转成 Go channel。
Go 编排层server/internal/orchestrator/orchestrator.go在设置角色头像、生成 idle video、语音回复驱动 Avatar 时调用 SetAvatarGenerateAvatarAvatarInfo
Python 服务注册inference/server.py通过 avatar_pb2_grpc.add_AvatarServiceServicer_to_server(...)AvatarGRPCService 挂到 gRPC server。
Python 适配层inference/services/avatar_service.py校验 Avatar 是否启用,读取 gRPC metadata,转换 protobuf 消息,并调用 Avatar 插件。
模型插件层inference/plugins/avatar/base.py规定插件必须实现 set_avatargenerate_streamresetget_fpsget_output_dimensions
AvatarGRPCService 本身没有直接跑 FlashHead 或 LiveAct 模型。它的核心方法是 _get_plugin():先检查配置里的 Avatar 开关,如果 inference.avatar.enabled 为 false,就返回 FAILED_PRECONDITION;如果已开启,则从 PluginRegistry 里取出当前 category 为 avatar 的插件。也就是说,这个服务层只负责“接 gRPC、找插件、做格式转换”,不负责具体模型推理

四个接口可以这样理解:

gRPC 方法avatar_service.py 里的实现工程含义
SetAvatar把请求里的 image_data 写入临时图片文件,然后调用 plugin.set_avatar(image_path, use_face_crop)为当前 Avatar 后端设置驱动图。Go 侧通常从角色图片目录读取图片,再通过这个接口传给 Python。
GenerateStream把 gRPC 输入流包装成内部 AudioChunk 异步迭代器,再调用 plugin.generate_stream(audio_stream()),最后把插件返回的 numpy 视频帧序列转成 common_pb2.VideoChunk这是“音频驱动视频”的主链路:TTS 或 VoiceLLM 产出的音频 chunk 进入 Avatar,返回 RGB 视频帧、fps、chunk index 和 final 标记。
Reset调用 plugin.reset()清理 Avatar 插件内部状态,避免上一轮会话的状态污染下一轮。
GetInfo读取插件名、输出尺寸、fps,并通过 _avatar_frames_per_chunk(...) 估算每个视频 chunk 的帧数和时长。Go 侧用它判断 idle video 输出尺寸、Avatar 帧率和音视频同步参数。
GenerateStream 还有一个很重要的细节:Go 侧会把 session_idquestion_idreply_idturn_sequser_final_unix_ms 放进 gRPC metadata;Python 侧再把这些 trace 字段塞进内部 AudioChunk。如果 Avatar 插件返回了 trace_generation_started_since_user_final_ms,服务会通过 x-cyberverse-trace-flashhead-generation-started-since-user-final-ms initial metadata 回传给 Go。这个设计不是模型必需品,而是工程观测点:它可以记录从用户说完话到 FlashHead 真正开始生成视频之间的耗时。

从实现边界看,avatar_service.py 的价值在于把三种世界隔离开:外部世界看到的是 protobuf 定义的 gRPC 接口;Go 编排层看到的是 SetAvatarGenerateAvatar 这种稳定 client 方法;模型插件只需要实现 AvatarPlugin 抽象。这样一来,FlashHead 换成 LiveAct,或者未来接入另一个数字人后端时,理论上不需要改 Orchestrator 主链路,只要实现同一套插件接口即可。

一句话总结 AvatarServiceinference/services/avatar_service.py 不是 Avatar 模型本体,而是 Avatar 的“协议翻译层”——它把 Go 发来的头像图片和音频流翻译成插件接口调用,再把插件生成的视频帧翻译回 gRPC 流。

源码路径地图:从入口一路追到能力模块

如果按源码阅读顺序下钻,CyberVerse 不是从模型文件开始读,而是先从运行入口和接口边界开始。推荐路径如下:#CyberVerse-GitHub

阅读目标关键路径读到什么
启动方式Makefile / scripts/三个服务如何分别启动,哪些环境变量和依赖是必需的
Go 编排入口server/cmd/cyberverse-server/main.goWebSocket、LiveKit、inference client、character store、task store 如何装配进 orchestrator
跨进程接口proto/*.protoASR、LLM、TTS、Avatar、VoiceLLM、RAG 六个 gRPC 服务,以及 common 共享消息类型
实时媒体层server/internal/livekit / server/internal/mediapeerSFU 与 P2P 两套 WebRTC 模式,以及 NAT/TURN 处理
Python 推理服务inference/server.py插件注册、配置加载、gRPC 服务挂载和 Avatar 开关
插件抽象inference/plugins/base.py不同模型/供应商如何被统一成可初始化、可关闭、可替换的插件
PersonaAgentinference/plugins/persona 与相关设计文档前台实时对话如何触发后台任务,而不阻塞语音链路
前端体验frontend/角色选择、设置页、WebRTC 接收与用户交互入口

先跑起来:推荐从纯语音模式开始

如果你是第一次读这个仓库,不建议一上来就下载 Avatar 权重、配 CUDA、调 FlashHead 或 LiveAct。更稳的方式是先把纯语音 Agent 跑通:它能验证三进程、WebRTC、gRPC、模型 API key、角色配置和设置页是否工作;等语音链路稳定后,再打开数字人视频后端。#CyberVerse-GitHub

git clone https://github.com/dsd2077/CyberVerse.git
cd CyberVerse
conda create -n cyberverse python=3.10
conda activate cyberverse
cp infra/.env.example .env
cp infra/cyberverse_config.example.yaml cyberverse_config.yaml

然后先在 .env 里填模型供应商 key,例如 DASHSCOPE_API_KEY 或豆包相关的 DOUBAO_ACCESS_TOKEN / DOUBAO_APP_ID。第一次阅读源码时,建议把 cyberverse_config.yaml 改成:

inference:
  avatar:
    enabled: false

这样系统会退化成纯语音 Agent,不需要本地 Avatar GPU。接下来不要直接启动服务,而是先进入专门的依赖安装章节。

第4章
环境准备与依赖安装

CyberVerse 同时包含 Python gRPC 推理服务、Go API/WebRTC 编排服务和 Vue 前端,任意一侧依赖缺失,后面的源码阅读都会被 ModuleNotFoundErrorprotocpkg-config 之类错误打断。因此必须在阅读源码前完成完整的依赖安装与 smoke test。

系统级依赖

这里不要用包管理器的最新版 protobuf 顶替 protoc:当前仓库的 Go proto 生成脚本锁定 libprotoc 29.3,所以安装阶段就应当把 protoc 29.3 放到 PATH 最前面。另一个必须前置的依赖是 npmmake setup 会安装/构建前端依赖,执行前必须确认 nodenpm 都可用。macOS Apple Silicon 使用 osx-aarch_64 包;Intel Mac 把文件名换成 protoc-29.3-osx-x86_64.zip;Linux x86_64 使用 linux-x86_64.zip

brew install pkg-config opus opusfile libsoxr ffmpeg node

PROTOC_VERSION=29.3
PROTOC_ZIP=protoc-${PROTOC_VERSION}-osx-aarch_64.zip
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${PROTOC_ZIP}
mkdir -p "$HOME/.local/protoc-${PROTOC_VERSION}"
unzip -o "$PROTOC_ZIP" -d "$HOME/.local/protoc-${PROTOC_VERSION}"
export PATH="$HOME/.local/protoc-${PROTOC_VERSION}/bin:$PATH"
export PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig:/opt/homebrew/share/pkgconfig"

node --version
npm --version
go version
protoc --version
ffmpeg -version
pkg-config --modversion opus opusfile soxr
# 1. 安装系统原生依赖
sudo apt-get update
sudo apt-get install -y build-essential pkg-config libopus-dev libopusfile-dev libsoxr-dev ffmpeg unzip curl

# 2. 安装 Node.js LTS(自动附带 npm)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs

# 3. 配置 Go 国内镜像(服务器在国内时必做,否则 go mod download 会超时)
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOSUMDB=sum.golang.org

# 4. 安装 protoc 29.3(GitHub release 在国内可能较慢,可配合代理或镜像)
# Ubuntu/Debian 的 protobuf-compiler 会提供 /usr/bin/protoc,版本通常是 3.x;先移除它,避免 PATH 命中旧版本。
sudo apt-get remove -y protobuf-compiler || true
hash -r

PROTOC_VERSION=29.3
PROTOC_ZIP=protoc-${PROTOC_VERSION}-linux-x86_64.zip
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${PROTOC_ZIP}
mkdir -p "$HOME/.local/protoc-${PROTOC_VERSION}"
unzip -o "$PROTOC_ZIP" -d "$HOME/.local/protoc-${PROTOC_VERSION}"
export PATH="$HOME/.local/protoc-${PROTOC_VERSION}/bin:$PATH"
echo 'export PATH="$HOME/.local/protoc-29.3/bin:$PATH"' >> "$HOME/.bashrc"
hash -r
which -a protoc

# 5. smoke test
node --version
npm --version
go version
protoc --version
ffmpeg -version
pkg-config --modversion opus opusfile soxr

项目依赖安装

确认 protoc --version 输出 libprotoc 29.3,并且 node --versionnpm --version 都可用后,再安装项目依赖。make setup 会触发前端依赖安装,因此 npm 不是可选项;Python 依赖可以走 uv,也可以走已经创建好的 conda 环境。纯语音链路不只是 LLM 和 TTS:如果配置里启用了 Whisper ASR 插件,还必须安装 asr extra,否则启动 inference/server.py 时会在 import whisper 处报 No module named 'whisper'。因此这里一次性安装 inferencellmttsvoice_llmagentasr

make setup
uv pip install -e ".[inference,llm,tts,voice_llm,agent,asr]"
uv run python -c "import sys; print(sys.executable); print(sys.version)"
uv run python -c "from openai import AsyncOpenAI; import langgraph, aiosqlite, whisper; print('voice deps ok')"
conda activate cyberverse
python -m pip install -U pip setuptools wheel
make setup
python -m pip install -e ".[inference,llm,tts,voice_llm,agent,asr]"
python -c "import sys; print(sys.executable); print(sys.version)"
python -c "from openai import AsyncOpenAI; import langgraph, aiosqlite, whisper; print('voice deps ok')"

如果要开启 Avatar 视频后端,再单独在 Linux + NVIDIA CUDA 环境安装 flash_head extra;macOS 本地阅读和调试不要把 FlashHead 当成必选依赖,否则会被 nvidia-nccl-cu12xformers 或 CUDA wheel 阻塞。还要注意,FlashHead 源码会直接导入 torchtorchvision.transforms;官方 README 对完整数字人视频模式的要求是支持 CUDA 12.8+ 的 GPU,并安装 torch2.8.0torchvision0.23.0torchaudio2.8.0cu128 wheel。因此不要随手装一个不匹配的 torchvision,否则很容易在导入阶段遇到 RuntimeError: operator torchvision::nms does not exist

# macOS 不安装 flash_head;确认 cyberverse_config.yaml 中 inference.avatar.enabled=false
python -c "import platform; print(platform.platform())"
# Linux + NVIDIA CUDA 机器上才执行;官方 README 要求 CUDA 12.8+ 与 PyTorch 2.8
uv pip uninstall -y torch torchvision torchaudio
uv pip install --no-cache-dir torch==2.8.0 torchvision==0.23.0 torchaudio==2.8.0 --index-url https://download.pytorch.org/whl/cu128
uv pip install -e ".[flash_head]"
uv run python -c "import torch, torchvision; print(torch.__version__, torchvision.__version__, torch.cuda.is_available())"
uv run python -c "from loguru import logger; import torchvision.transforms as transforms; print('flash_head deps ok')"
# Linux + NVIDIA CUDA 机器上才执行;官方 README 要求 CUDA 12.8+ 与 PyTorch 2.8
conda activate cyberverse
python -m pip uninstall -y torch torchvision torchaudio
python -m pip install --no-cache-dir torch==2.8.0 torchvision==0.23.0 torchaudio==2.8.0 --index-url https://download.pytorch.org/whl/cu128
python -m pip install -e ".[flash_head]"
python -c "import torch, torchvision; print(torch.__version__, torchvision.__version__, torch.cuda.is_available())"
python -c "from loguru import logger; import torchvision.transforms as transforms; print('flash_head deps ok')"

Avatar 权重下载与路径校验

依赖安装成功只代表 FlashHead 代码能导入;真正启动 Avatar 时还必须准备完整权重目录。官方 README 要求下载 SoulX-FlashHead-1_3Bwav2vec2-base-960h,并在 cyberverse_config.yaml 中把 inference.avatar.flash_head.checkpoint_dirwav2vec_dir 指向本地路径。如果只下载了部分文件,启动时就会出现 FileNotFoundError: ./checkpoints/SoulX-FlashHead-1_3B/VAE_Wan/Wan2.1_VAE.pth 这类错误。

python -m pip install -U "huggingface_hub[cli]"

# 国内服务器可先启用 HF 镜像
export HF_ENDPOINT=https://hf-mirror.com

hf download Soul-AILab/SoulX-FlashHead-1_3B --local-dir ./checkpoints/SoulX-FlashHead-1_3B
hf download facebook/wav2vec2-base-960h --local-dir ./checkpoints/wav2vec2-base-960h

test -f ./checkpoints/SoulX-FlashHead-1_3B/VAE_Wan/Wan2.1_VAE.pth
test -d ./checkpoints/wav2vec2-base-960h
python -m pip install -U modelscope

modelscope download --model Soul-AILab/SoulX-FlashHead-1_3B --local_dir ./checkpoints/SoulX-FlashHead-1_3B
modelscope download --model facebook/wav2vec2-base-960h --local_dir ./checkpoints/wav2vec2-base-960h

test -f ./checkpoints/SoulX-FlashHead-1_3B/VAE_Wan/Wan2.1_VAE.pth
test -d ./checkpoints/wav2vec2-base-960h
inference:
  avatar:
    enabled: true
    default: "flash_head"
    flash_head:
      checkpoint_dir: "./checkpoints/SoulX-FlashHead-1_3B"
      wav2vec_dir: "./checkpoints/wav2vec2-base-960h"

这里的 test -f 是硬校验:如果 Wan2.1_VAE.pth 不存在,不要继续启动 make inference;先重新下载完整的 FlashHead 权重目录,或修正 checkpoint_dir 到真实路径。

如果后续仍然提示 protoc-gen-goprotoc-gen-go-grpc 不存在,按 server/go.mod 的 tool 配置安装 Go 插件,或确认 go tool 能找到对应插件。只有依赖安装、权重下载和 smoke test 都通过,后面对 server/internal/orchestratorinference/pluginsproto/ 的阅读才不是“看到了文件名但没验证入口能不能跑”。

启动服务与基础验证

依赖阶段通过后,再按 Makefile 的入口分别启动三个进程:

# 终端 1:Python gRPC 推理服务
make inference

# 终端 2:Go API / WebRTC 编排服务
make server

# 终端 3:Vue 前端
make frontend

验证时先访问 http://localhost:5173,再用健康检查确认 Go 服务能连上 Python 推理服务:

curl -s http://localhost:8080/api/v1/health

常见报错:protoc not found / version mismatch

如果 make setupmake inference 输出 Python proto generation complete 后失败,并提示 ERROR: protoc not found in PATHERROR: protoc version mismatch for reproducible Go codegen,不要继续往下启动服务,也不要尝试 apt uninstall libprotoc。真正参与代码生成的是当前 shell 里 which protoc 命中的 protoc 二进制;Ubuntu/Debian 上旧版本通常来自 protobuf-compiler 包提供的 /usr/bin/protoc。正确做法是移除这个包、清掉 shell 命令缓存,并把 protoc 29.3 放到 PATH 最前面。

sudo apt-get remove -y protobuf-compiler || true
hash -r

export PATH="$HOME/.local/protoc-29.3/bin:$PATH"
echo 'export PATH="$HOME/.local/protoc-29.3/bin:$PATH"' >> "$HOME/.bashrc"
hash -r

which -a protoc
protoc --version
make setup

which -a protoc 的第一行必须是 $HOME/.local/protoc-29.3/bin/protoc,并且 protoc --version 必须输出 libprotoc 29.3。如果第一行仍然是 /usr/bin/protoc,说明当前 shell 的 PATH 顺序还不对;如果只有新终端不生效,执行 source ~/.bashrc 后再检查。当前脚本会拒绝 libprotoc 3.21.12libprotoc 35.0 这类非 29.3 版本,以避免生成出来的 Go 代码头部和内容不可复现。

常见报错:No module named 'openai'

如果 make inference 启动时报 ModuleNotFoundError: No module named 'openai',不要单独 pip install openai。这说明当前终端没有使用安装阶段配置好的同一个 uv 环境,或前面的 editable extras 没有安装成功。应回到安装阶段,用同一个终端和同一个项目环境重新安装完整语音链路依赖:

uv run python -c "import sys; print(sys.executable)"
uv pip install -e ".[inference,llm,tts,voice_llm,agent,asr]"
uv run python -c "from openai import AsyncOpenAI; import langgraph, aiosqlite; print('voice deps ok')"

关键原则是让 uv run make inference、导入检查和 editable 安装使用同一个 Python 环境。不要一边用 uv run 启动服务,一边用裸 pip 往另一个 Python 环境里装包。

常见报错:No module named 'loguru'

如果打开数字人后,make inference 在初始化 avatar.flash_head 时抛出 ModuleNotFoundError: No module named 'loguru',根因不是 WebSocket、Go Server 或前端问题,而是当前 Python 环境没有安装 FlashHead 后端依赖。loguru 属于项目的 flash_head extra;但要注意,当前 flash_head extra 还硬依赖 nvidia-nccl-cu122.27.3,这个包面向 Linux/CUDA 平台,在 Apple Silicon / macOS 上没有匹配 wheel。因此 macOS 上继续执行 uv pip install -e ".[flash_head]" 会进入依赖求解失败,而不是简单缺一个 loguru

# 先确认当前平台和 Python 环境
uv run python -c "import platform, sys; print(platform.platform()); print(sys.executable)"

# Linux + NVIDIA CUDA 机器上再安装 FlashHead 后端依赖;官方 README 要求 CUDA 12.8+ 与 PyTorch 2.8
uv pip uninstall -y torch torchvision torchaudio
uv pip install --no-cache-dir torch==2.8.0 torchvision==0.23.0 torchaudio==2.8.0 --index-url https://download.pytorch.org/whl/cu128
uv pip install -e ".[flash_head]"
uv run python -c "import torch, torchvision; print(torch.__version__, torchvision.__version__, torch.cuda.is_available())"
uv run python -c "from loguru import logger; import torchvision.transforms as transforms; print('flash_head deps ok')"
make inference

如果你当前是在 Mac 本地调通 CyberVerse,正确做法是在安装阶段就关闭数字人视频后端,只跑纯语音/Omni/PersonaAgent 链路:把 cyberverse_config.yaml 里的 inference.avatar.enabled 设为 false,并安装 .[inference,llm,tts,voice_llm,agent,asr]。等三进程、DashScope key、WebRTC、LLM/TTS 都稳定后,再把 FlashHead 部署到 Linux + NVIDIA CUDA 的机器上。

uv pip install -e ".[inference,llm,tts,voice_llm,agent,asr]"
uv run python -c "from openai import AsyncOpenAI; import langgraph, aiosqlite; print('voice deps ok')"
make inference

不要在 macOS 上用单独安装 loguru 来“绕过”这个问题,因为后面还会继续遇到 diffusersxfuserxformersnvidia-nccl-cu12 或 CUDA/GPU 相关依赖。这个错误的边界结论是:Mac 适合跑前端、Go Server 和语音链路;FlashHead 实时数字人后端应放到支持 CUDA 的 Linux GPU 环境。

常见报错:operator torchvision::nms does not exist

如果导入 torchvision 时抛出 RuntimeError: operator torchvision::nms does not exist,通常不是 CyberVerse 的业务代码问题,而是当前环境里的 torchtorchvision wheel 不匹配,或者混装了 CPU wheel、CUDA wheel、conda 包和 pip 包。torchvision._meta_registrations 在注册 nms 的 fake/meta kernel 时,发现底层 C++/CUDA 扩展里没有对应算子,于是导入阶段直接失败。

# uv 环境:先移除旧 wheel,再按官方 README 的 CUDA 12.8 / PyTorch 2.8 组合成对安装
uv pip uninstall -y torch torchvision torchaudio
uv pip install --no-cache-dir torch==2.8.0 torchvision==0.23.0 torchaudio==2.8.0 --index-url https://download.pytorch.org/whl/cu128
uv run python -c "import torch, torchvision; print(torch.__version__, torchvision.__version__, torch.cuda.is_available())"

# conda + pip 环境:不要混用另一个 Python 的 pip
conda activate cyberverse
python -m pip uninstall -y torch torchvision torchaudio
python -m pip install --no-cache-dir torch==2.8.0 torchvision==0.23.0 torchaudio==2.8.0 --index-url https://download.pytorch.org/whl/cu128
python -c "import torch, torchvision; print(torch.__version__, torchvision.__version__, torch.cuda.is_available())"

官方 README 对完整数字人视频模式写的是 PyTorch 2.8(CUDA 12.8),因此这里固定使用 torch2.8.0torchvision0.23.0torchaudio==2.8.0cu128 wheel。修复后再安装 .[flash_head],并运行 import torchvision.transforms 的 smoke test;如果这一步仍失败,先解决 PyTorch / torchvision 二进制匹配问题,不要继续排查 WebRTC、Go Server 或 Avatar 插件配置。

常见报错:No omni model plugin initialized for provider 'persona'

如果 Go 服务日志里出现 No omni model plugin initialized for provider 'persona',不要把 persona 当成错误供应商。Go 编排层在 Omni 会话里启用 PersonaAgent 时,会故意把 provider 传成 persona;真正的问题通常是 Python 推理服务没有成功初始化 persona.persona 插件。使用 uv 时,先检查 PersonaAgent 依赖是否可导入:

uv run python -c "import langgraph, langgraph.checkpoint.sqlite, aiosqlite; from inference.plugins.voice_llm.persona_agent import PersonaAgentPlugin; print('persona deps ok')"

如果这里报 No module named 'langgraph',说明安装阶段没有安装 agent extra,或当前终端没有使用同一个 uv 环境。不要只补一个包;直接重新执行完整语音链路安装和 smoke test:

uv pip install -e ".[inference,llm,tts,voice_llm,agent,asr]"
uv run python -c "import langgraph, langgraph.checkpoint.sqlite, aiosqlite; from inference.plugins.voice_llm.persona_agent import PersonaAgentPlugin; print('persona deps ok')"
make inference

重启后确认 Python 推理服务日志里出现 Initialized plugin: persona.persona。如果仍然失败,再往上翻 make inference 启动日志,通常会有更早的插件初始化异常,例如 DASHSCOPE_API_KEY 缺失、qwen_omni provider 配置缺失,或 PersonaAgent 的底层 model_provider 没有指向具体 Omni provider。

常见报错:AuthenticationError 401 invalid_api_key

如果日志里出现 AuthenticationErrorError code: 401Incorrect API key providedinvalid_api_key,或 TTS/Omni 日志里出现 server rejected WebSocket connection: HTTP 401,这通常不是源码问题,而是 DashScope / 阿里云百炼 Model Studio 的 API Key 无效、过期、填错,当前进程没有读到正确的环境变量,或者请求打到了不匹配的 DashScope endpoint。CyberVerse 的 qwenqwen_omniqwen_tts 和 PersonaAgent 的本地 Qwen LLM 都会读取 DASHSCOPE_API_KEY;如果使用国内百炼服务,还需要显式设置 DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1,否则代码默认会走国际站 endpoint。

# 不要把真实 key 打到公开日志里;只检查变量是否存在
uv run python -c "import os; print('DASHSCOPE_API_KEY set:', bool(os.getenv('DASHSCOPE_API_KEY'))); print('DASHSCOPE_BASE_URL:', os.getenv('DASHSCOPE_BASE_URL')); print('DASHSCOPE_WS_URL:', os.getenv('DASHSCOPE_WS_URL')); print('DASHSCOPE_TTS_WS_URL:', os.getenv('DASHSCOPE_TTS_WS_URL'))"

# 国内百炼 / DashScope OpenAI-compatible endpoint;TTS/Omni WebSocket 会从这个 base URL 派生
export DASHSCOPE_API_KEY="sk-..."
export DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
make inference

如果只想显式指定实时 WebSocket endpoint,也可以设置 DASHSCOPE_WS_URL 作为通用实时地址,或设置 DASHSCOPE_TTS_WS_URL 只覆盖 Qwen TTS。通常不需要手写它们,因为 DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 会自动派生到同域名下的 wss://dashscope.aliyuncs.com/api-ws/v1/realtime

如果你已经改了 .env、shell 环境变量或 cyberverse_config.yaml,必须重启 make inference,必要时也重启 make server,因为已经启动的 Python 推理服务不会自动读取新的环境变量。还要确认使用的是 Model Studio / DashScope 的有效 key,而不是 OpenAI、GitHub、通义 App Key 或其他平台的 token;如果 key 曾经贴到聊天、日志或公开页面里,应立即在控制台禁用并重新生成。

常见报错:发送失败:网络异常,请稍后重试

这个提示是前端兜底错误,不是根因。代码路径是:前端优先通过 WebSocket 发送 text_input;如果 WebSocket 未连接或发送失败,就 fallback 到 HTTP POST /api/v1/sessions/{id}/message。只有这个 HTTP fallback 也失败时,界面才显示 发送失败:网络异常,请稍后重试。

# 先确认 Go API Server 是否可达
curl -i http://localhost:8080/api/v1/health

# 再打开浏览器 DevTools → Network,查看失败的请求:
# POST /api/v1/sessions/<session_id>/message

如果 Network 里是 404,通常是会话已经失效或前端拿着旧 session_id,刷新页面重新创建会话即可;如果是 500 failed to process message,要看 make server 日志里 Failed to handle text input 后面的真实错误,常见仍然是上游 make inference 的 LLM/TTS/Omni 鉴权、endpoint 或插件初始化失败;如果请求根本发不出去,则优先检查 make server 是否在运行、前端 Vite proxy 是否仍指向 Go 服务,以及 /api/v1/health 是否返回正常。

常见报错:Package soxr / opus / opusfile not found

如果 make serverPackage soxr was not foundPackage 'opus' not foundPackage 'opusfile' not found,说明安装阶段的原生依赖或 PKG_CONFIG_PATH 没有在当前 shell 生效。回到安装阶段重新安装 Homebrew 依赖,并在启动 make server 的同一个终端导出 PKG_CONFIG_PATH

brew install pkg-config opus opusfile libsoxr
export PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig:/opt/homebrew/share/pkgconfig"
pkg-config --modversion opus opusfile soxr
make server

Apple Silicon 的 Homebrew 通常在 /opt/homebrew;Intel Mac 通常把上面的路径换成 /usr/local/lib/pkgconfig:/usr/local/share/pkgconfig。仓库的 Makefile 默认还会把 CONDA_ENV=$(HOME)/miniconda3/envs/cyberverse 下的 lib/pkgconfig 拼进 PKG_CONFIG_PATH。如果你不用 conda、只用 uv.venv,就不要指望 Python 虚拟环境提供这些 C 库;应使用 Homebrew 或手动设置 CONDA_ENV 指向真正包含 opus.pcopusfile.pcsoxr.pc 的环境。

阅读顺序建议

把 CyberVerse 当成一个“可运行的源码导览”来读:先跑纯语音模式,确认 make inferencemake servermake frontend 三个进程能连起来;再看 proto/*.proto 明确边界;最后才进入 Avatar 后端和 GPU 参数。否则很容易被 CUDA、权重路径和显存问题挡在架构理解之外。

按目标使用代码库:四条入口路径

你的目标先改哪里再看哪里判断是否成功
只是体验实时语音 Agent.env + cyberverse_config.yaml,关闭 inference.avatar.enabledfrontend/ 设置页与角色页浏览器能进房间、麦克风输入后有语音回复
替换 LLM / TTS / ASR 供应商cyberverse_config.yamlinference.llminference.ttsinference.asrinference/plugins/inference/core/registry.pymake inference 启动时插件能初始化,设置页能读到配置
接入新的数字人视频模型inference.plugins.avatar 下新增插件类,并实现统一 Avatar 接口proto/avatar.protoinference/services/avatar_service.pyAvatarService.GenerateStream 能把音频 chunk 转成视频帧流
做二次开发或嵌入业务系统server/internal/api/router.goserver/internal/orchestratorserver/internal/livekit / server/internal/mediapeer会话创建、消息发送、任务事件和 WebRTC 连接都能被业务侧稳定调用

这张路径表也说明了为什么本文把 CyberVerse 称为“系统编排层”:真正值得读的不是某个单点模型,而是入口、接口、媒体、推理、插件、任务状态这些边界如何被组织起来。

第三章
实时对话链路:音频如何变成会说话的脸

现在把镜头拉近,看一次完整的语音对话在系统里怎么流动。这是理解任何实时数字人系统的核心。

sequenceDiagram
    participant U as 用户(浏览器)
    participant M as WebRTC(Go)
    participant O as Orchestrator(Go)
    participant V as VoiceLLM/ASR(Py)
    participant L as LLM(Py)
    participant T as TTS(Py)
    participant A as Avatar(Py)
    U->>M: 麦克风音频流
    M->>O: 音频 chunk
    O->>V: TranscribeStream / Converse(stream)
    V-->>O: 识别文本 / 语音回复(stream)
    O->>L: GenerateStream(对话上下文)
    L-->>O: token 流
    O->>T: SynthesizeStream(文本流)
    T-->>O: 音频 chunk 流
    O->>A: GenerateStream(音频 chunk)
    A-->>O: 视频帧流
    O->>M: 音视频同步推流
    M->>U: 数字人开口说话
    Note over U,A: 任意环节用户都可触发 Interrupt 打断

这条链路里有两条值得注意的设计:

全链路 streaming,不等“整句”

传统的“录音→识别完整句→大模型生成完整答案→合成完整语音→生成完整视频”是串行阻塞的,延迟会累加到好几秒。CyberVerse 的实时主链路把 ASR、LLM、TTS、VoiceLLM、Avatar 都做成 streaming 接口,意味着上游还没说完,下游就能开始处理:LLM 吐出第一个 token,TTS 就能开始合成;TTS 吐出第一个音频 chunk,Avatar 就能开始生成对应的视频帧。这种“边生成边消费”的流水线,是把端到端延迟压到可对话区间的关键。

两种 WebRTC 模式应对不同网络

从 README 和 server/internal/livekitmediapeer 两个模块可以看到,CyberVerse 支持两种推流模式:

模式实现模块适用场景
P2P 直连mediapeer + 嵌入式 TURN单机/内网/SSH 隧道,延迟最低
LiveKit SFUlivekit复杂网络、多人、需要穿透 NAT
main.go 里能看到为 NAT 穿透专门内嵌了一个 TURN-over-TCP 服务器(direct.TURNServer),注释直说是为了 AutoDL、SSH 隧道这类场景——这是很务实的细节,说明作者真的在云 GPU 环境里部署过。#CyberVerse-GitHub
第四章
PersonaAgent:让数字人"边聊边干活"

如果只做到上一章,CyberVerse 也就是个"实时语音对话 + 数字人脸"的系统。它真正有意思的设计,是 PersonaAgent + SubAgent 的多 Agent 架构,解决了一个实际矛盾:用户既想要秒回的流畅对话,又想让数字人能做"搜索资料、写报告"这种需要几十秒的重活

核心洞察:前台对话不能被后台任务阻塞

设计文档把这个矛盾说得很清楚:实时语音链路不能被长任务阻塞。如果用户说"帮我查一下今天的新闻并整理成报告",系统不能卡住几十秒不说话。#CyberVerse-Persona-Design

CyberVerse 的解法是一套"前台 + 后台"分工:

角色位置职责
PersonaAgentPython 前台维持流畅对话、快速响应打断、触发任务
Go TaskServiceGo 服务端任务状态中心:SQLite 持久化、事件广播、断线恢复
SubAgent (LangGraph)Python 后台真正执行搜索、调研、写报告的重活

关键设计:PersonaAgent 是编排层,不是模型

这是最容易误解的一点。文档里有一句斩钉截铁的话:

「PersonaAgent 不是模型,而是编排层。它不是 omni 模型本身,而是包装真实 Omni provider 的编排层。」

CyberVerse 设计文档

具体来说,它包装的是阿里的 Qwen Omni Realtime 模型(qwen3.5-omni-flash-realtime),并给这个 Realtime session 注入了一组隐藏工具(hidden tools),通过模型原生的 function calling 来触发后台任务:#CyberVerse-Persona-Design

# PersonaAgent 注入给 Qwen Realtime 的隐藏工具
wait_for_more_input(partial_text, reason)   # 用户话没说完,继续等
create_task(user_request, title, kind)      # 创建后台任务
get_task_status()                           # 查询任务进度
cancel_task()                               # 取消任务

“先确认,后异步启动”的对话节奏

最精彩的是 create_task 的流程设计。它把后台任务同步阻塞在工具调用里,而是:

graph LR
    A["Qwen Omni
触发 create_task"] --> B["回灌 accepted
tool result"] B --> C["数字人先说
'好的,请稍等'"] C --> D["异步调 Go
TaskService 建任务"] D --> E["SubAgent 后台
跑 LangGraph"] E --> F["结果文本注入
同一 Omni session"] F --> G["Omni 生成
最终语音回复"]

这个"先用一句自然语音确认,再异步干活,干完把结果塞回对话"的节奏,让数字人显得既反应快(立刻回应)又有能力(能完成复杂任务)。这是把 Agent 能力嵌入实时语音对话的一个很值得借鉴的模式。

第五章
插件化:为什么每个能力都能换

README 里有一句话概括了 CyberVerse 的可扩展性哲学:"大脑、声音、听觉、工具、记忆、脸——全部是可替换的模块。"#CyberVerse-GitHub

这不是一句口号。打开 inference/plugins/base.py,可以看到所有插件都继承自同一个抽象基类:

class CyberVersePlugin(ABC):
    name: str = ""
    version: str = "0.1.0"

    @abstractmethod
    async def initialize(self, config: PluginConfig) -> None: ...

    @abstractmethod
    async def shutdown(self) -> None: ...

任何 ASR、LLM、TTS、Avatar 的具体实现,只要实现这个 initialize / shutdown 接口,就能被注册进系统。而 inference/server.py 里的 PluginRegistry 负责按配置把它们装配起来:

_PLUGIN_CATEGORIES = ("avatar", "llm", "tts", "asr", "omni", "persona", "voice_llm")
# 按 cyberverse_config.yaml 的配置,注册并初始化对应类别的插件
self.registry = PluginRegistry()

配置即架构

这套插件化的直接好处是:换供应商不用改代码,改 YAML 就行。在 cyberverse_config.yaml 里组合不同的 omni 模型、LLM、TTS、ASR、embedding、RAG、工具和 Avatar 后端,再在 Web UI 的 /settings 配各家的 API key。README 里明确支持阿里云 Qwen 系列(DASHSCOPE_API_KEY)和火山引擎豆包系列(DOUBAO_ACCESS_TOKEN)。

数字人视频是“可选后端”

特别值得说的是 Avatar——它在架构里是完全可选的。inference/server.py 里专门有一个 avatar_enabled 开关:

avatar_cfg = self.config.get("inference", {}).get("avatar", {})
self.avatar_enabled = _config_bool(avatar_cfg.get("enabled"), True)

inference.avatar.enabled: false 时,平台退化为纯语音 Agent,只推音频流,不需要本地 Avatar GPU,但同样的人设和角色配置继续生效。这对没有 GPU 或还不需要视频形象的场景非常友好——你可以先跑通语音对话,再决定要不要加上"脸"。

CyberVerse 目前内置支持两个数字人视频后端:FlashHead(SoulX-FlashHead-1.3B)和 LiveAct(SoulX-LiveAct 18B),都通过统一的 AvatarService.GenerateStream(音频流 → 视频帧流)接口接入。#CyberVerse-GitHub #SoulX-FlashHead #SoulX-LiveAct

CyberVerse demo Alice
图 3:CyberVerse 驱动的数字人 demo 之一。单张参考图即可驱动实时面部动画、唇形同步和缓存的待机视频。(来源:CyberVerse 官方 README, docs/assets,对应 YouTube demo;演示角色仅为示例,不提供商用)
第六章
实时性怎么量:RTP 因子与硬件门槛

数字人视频最大的工程挑战是实时性——生成一帧的速度必须跟得上播放的速度。CyberVerse 在 README 里给了一个非常实用的度量指标 RTP(real-time performance factor)#CyberVerse-GitHub

RTP 的定义

$$\text{RTP} = \frac{\text{elapsed}}{\text{frames} / \text{fps}}$$

其中 elapsed 是生成这批帧实际花的时间,frames / fps 是这批帧按目标帧率播放需要的时长。

判读规则非常直观:

RTP含义
< 1生成比播放快,有实时余量 ✅
= 1刚好实时
> 1生成比播放慢,视频会卡顿、落后 ❌

举个 README 里的真实例子:LiveAct 生成 32 帧、目标 20fps,播放时长是 32 / 20 = 1.6 秒;如果实际花了 1.870 秒,那么 RTP = 1.870 / 1.6 ≈ 1.17 > 1,说明在这块 GPU 上跑 320×480@20fps 已经追不上播放了。#CyberVerse-GitHub

硬件门槛:实时数字人视频不便宜

README 给的硬件 benchmark 很坦诚,也印证了我们在系列第五篇"算力选型"里的判断——实时数字人视频的算力门槛相当高#CyberVerse-GitHub #digital-human-compute-selection

模型质量档GPU卡数分辨率FPS实时?
FlashHead 1.3BLiteRTX 40901512×51225+
FlashHead 1.3BProRTX 40901512×512~10.8
FlashHead 1.3BProRTX 50902512×51225+
LiveAct 18BRTX PRO 60001256×41720

可以看到:Lite 档单卡 4090 能实时,但要 Pro 级画质就得上双 5090;18B 的 LiveAct 更是直接要 RTX PRO 6000 这种专业卡。这条 benchmark 给了一个非常具体的成本锚点——如果你想做"高画质 + 实时"的数字人,硬件预算要按多张高端卡来规划。

实战经验

当数字人视频卡顿时,先看 inference 日志里的 RTP。如果 RTP > 1,按优先级处理:

  • 降分辨率或从 Pro 切 Lite;
  • 加卡 / 开 FP8/FP4 GEMM / 编译优化;
  • 直接对照 benchmark 表,挑一行标了"实时✅"的配置。这套排查路径比盲目调参高效得多。
  • 第七章
    工程评价与 GPLv3 风险

    它做得好的地方

    • 边界划得清楚:三进程 + 六个 gRPC 服务 + common 共享消息契约,把“实时数字人系统”该有的模块拆得干净,每个模块职责单一。
    • 语言各尽其能:Go 管并发媒体、Python 管 AI 生态,不强行统一,是务实的选择。
    • 实时链路流式:ASR/LLM/TTS/VoiceLLM/Avatar 都围绕 streaming 设计,加上 Interrupt 打断,把实时对话体验做到了“可用产品”级别。
    • 插件化彻底:从抽象基类到配置驱动,换模型/换供应商只改 YAML,工程扩展性强。
    • 务实的部署细节:嵌入式 TURN、avatar 可关闭退化为纯语音、RTP 度量、多语言 README——能看出在真实云 GPU 环境里打磨过。

    局限与复现风险

    复现门槛不低

    完整跑起来需要 Node 18+、Go 1.25、Conda、Python 3.10+、FFmpeg、libopus/soxr 等系统库,外加三个终端协同。要带视频还得 CUDA 12.8+、PyTorch 2.8、Avatar 权重和高端 GPU。纯语音模式门槛低很多,但"完整数字人"对环境和算力都有硬要求。

    GPLv3:商用前必须做法务确认

    CyberVerse 使用 GPLv3 协议(仓库根目录 LICENSE,35KB 完整 GPLv3 文本)。GPLv3 是强 copyleft 协议——如果你基于它分发衍生产品,可能被要求以同样的协议开源你的修改和衍生代码。这对想把它直接集成进闭源商业产品的团队是关键约束。在系列第五篇里我们也强调过:不能因为它是好用的实时 Agent 框架就直接纳入闭源产品,商用前必须先做法务判断。相比之下,我们前面提到的 Ultralight-Digital-Human(Apache 2.0)、SadTalker(Apache 2.0)在协议上友好得多。#CyberVerse-GitHub #digital-human-compute-selection

    可迁移的工程经验

    即便你不直接用 CyberVerse,它的几个设计模式也很值得借鉴到自己的实时 AI 系统里:

    1. 用 gRPC streaming 契约切分"实时管线":把 ASR/LLM/TTS/Avatar 定义成统一的流式服务,天然支持"边生成边消费"。
    2. 前台 Agent + 后台 SubAgent 分工:用"先确认、后异步、再回灌"的节奏,让对话流畅性和复杂任务能力共存。
    3. 能力插件化 + 配置驱动:抽象统一的插件基类,把"用哪个供应商"从代码下沉到配置。
    4. 用 RTP 这类单一指标量化实时性:给运维一个可以直接判读、直接行动的健康度量。

    落地前检查表

    检查项为什么重要建议动作
    协议GPLv3 可能影响闭源商业分发先做法务评估,再决定 fork、隔离服务或仅作参考
    算力Avatar 视频实时性受 GPU、分辨率、质量档强约束用 RTP 指标压测,先确认目标分辨率和帧率能否实时
    链路延迟ASR、LLM、TTS、Avatar、WebRTC 任一环节都会拖慢首帧分别记录首 token、首音频 chunk、首视频帧和端到端延迟
    打断体验实时语音最怕“用户插话但系统继续说旧答案”重点测试 Interrupt、队列清理和 Avatar 状态重置
    降级路径没有 GPU 或视频后端不可用时,系统不能整体不可用保留纯语音 Agent 模式,把 Avatar 做成可关闭能力
    模型供应商ASR/LLM/TTS/Omni 都可能有成本、限流和稳定性差异把供应商选择放在配置层,避免写死在业务代码里
    结语
    从论文到产品的最后一公里

    数字人系列走到这里,我们从最底层的生成算法(Wav2Lip 的唇形同步、VASA-1 的面部动力学),到训练资源、推理成本、benchmark 与产品选型,再到这一篇——一个把所有这些拼成完整系统的开源工程。

    CyberVerse 的价值恰恰在于它展示了"最后一公里"长什么样:一个能对话的实时数字人,70% 的工程量不在生成模型本身,而在 WebRTC 推流、流式管线编排、打断处理、Agent 任务调度、插件化、实时性度量这些"系统层"的活。它用一套清晰的 gRPC 微服务架构把这些活组织了起来,是研究者理解"工程落地差距"、工程师参考"系统该怎么搭"的好样本。

    唯一需要时刻记在心里的,是那行 GPLv3——技术上它很优秀,但能不能进你的商业产品,先问法务。

    参考来源

    • CyberVerse. 开源实时数字人 Agent 框架. GitHub: dsd2077/CyberVerse(本文所有架构、代码、配置与 benchmark 均基于对该仓库源码与 README/docs 的实际阅读)
    • CyberVerse. PersonaAgent 与后台任务 MVP 设计文档. docs/zh-CN/features/2026-05-11-persona-agent-task-mvp.md(仓库内)
    • Soul-AILab. SoulX-FlashHead-1_3B. Hugging Face
    • Soul-AILab. SoulX-LiveAct. Hugging Face
    • 本系列第八篇. 数字人系列(八):算力、训练资源与 Benchmark 总结. 站内文章