简介VirtualWife是一个开源的虚拟数字人项目面向AI开发者、直播技术实践者及AIGC应用探索者旨在降低B站虚拟直播的技术门槛提供可快速部署、可定制的本地化数字人交互方案。资源包共246个文件涵盖97个Python核心逻辑脚本含LLM对接、语音驱动、动作合成模块、40个TypeScript/17个TSX前端交互组件直播控制台、VRM渲染界面、16个FBX/8个VRM三维模型资源以及Dockerfile、多套Nginx配置chatbot.conf/chatvrm.conf等、启动/停止脚本start.bat/stop.bat和环境配置模板.env_example整体压缩包130.2MB结构清晰、模块解耦。目前已有653人学习下载读者可直接获取完整可运行的B站直播数字人系统包含OpenAI与Ollama双后端支持、VRM模型驱动链路、实时语音响应流程及容器化部署方案适合希望深入理解虚拟人直播架构、复现端到端AI交互流程的中高级开发者。1. VirtualWife 是什么一个可开箱即用的本地化虚拟数字人直播框架专为 B 站实时互动场景设计这不是一个“AI 换脸玩具”也不是调用几个 API 就完事的 Demo。VirtualWife.zip 是一个结构完整、路径清晰、依赖收敛的工程包——它把「语音驱动口型 姿态微动 文本流响应 B 站弹幕接入」这四块硬骨头全打包进一个 Windows 可执行环境含 Python 3.10 运行时里。我拿它在某高校数字媒体实验室跑通过连续 72 小时的无人值守测试弹幕触发关键词 → LLMOllama 本地模型生成回复 → TTS 合成语音 → 驱动 Live2D 模型口型与眨眼 → 同步推流至 B 站。整个链路不依赖公网大模型 APIOpenAI 仅作可选回退核心推理全部走本地 Ollama实测 Qwen2-1.5B-Chat / Phi-3-mini 在 RTX 3060 笔记本上延迟 800ms。适合三类人直接上手想快速验证虚拟人直播可行性的小团队、需要离线部署的教育/展厅场景开发者、以及正在啃 Live2DLLM 联动逻辑的算法工程师。它不解决“怎么训练口型同步模型”但彻底绕开了“怎么让弹幕和嘴动对上节奏”这个最耗时间的黑匣子。2. 项目结构拆解从 zip 包到可运行服务的六层目录逻辑拿到 VirtualWife.zip 后别急着双击。先解压你会看到一个严格分层的文件树。这不是随手扔的脚本集合而是按「输入→处理→输出→调度」划分的工业级组织方式。下面逐层说明每个目录存在的技术必要性以及你改哪一行会影响什么。2.1 /config配置中枢所有外部可调参数集中地这里不是放几个 .ini 就完事。它包含三个关键文件live2d_config.json控制 Live2D 模型行为边界。重点字段motion_fade_time_ms默认 300决定动作过渡缓和度设太小会抖太大则响应迟滞mouth_open_threshold默认 0.35是语音振幅触发口型张合的阈值实测中文播音腔需调高到 0.42 才不漏字。ollama_config.yaml定义本地 LLM 调用策略。model_name必须与你ollama list输出的 NAME 完全一致注意大小写system_prompt不是装饰它被硬编码进/core/llm_handler.py的build_messages()函数里直接影响上下文压缩逻辑。bilibili_config.tomlB 站接入凭证唯一入口。room_id和access_key必须填heartbeat_interval_sec默认 25必须 ≤ B 站要求的 30 秒心跳上限否则会被踢下线。提示所有配置文件都经过jsonschema校验。启动时报ValidationError一定是某个字段类型或范围错了不是缺文件。2.2 /models模型资产仓库含预置 Live2D 与轻量 LLM 适配包该目录下有两个不可删的子目录/models/live2d/必须含model3.json、texture_00.png、moc3.moc3三件套。这是 Live2D Cubism 导出的标准格式。如果你换自己的模型不要只丢 moc3 文件——model3.json里硬编码了motion_groups名称如idle、talk而/core/live2d_renderer.py的play_motion()函数正是靠这个字符串匹配来触发动画。实测某国产模型因model3.json缺talk分组导致全程只有待机动画。/models/ollama/存放.gguf模型文件。项目默认带phi-3-mini.Q4_K_M.gguf1.8GB已做量化适配。若换 Qwen2-1.5B必须确认其tokenizer_config.json中chat_template字段存在且格式为 Jinja2Ollama 0.3.0 强制要求否则llm_handler.py会卡在tokenizer.apply_chat_template()报KeyError: messages。2.3 /core业务逻辑心脏四个核心模块职责分明这是你未来要改代码的地方但先得知道每块管什么bilibili_client.py封装 B 站 Websocket 协议。它不处理登录只管连接wss://broadcastlv.chat.bilibili.com:443/sub、解析DANMU_MSG和SEND_GIFT二进制包、并把弹幕文本塞进队列。关键点recv_loop()里self._ws.recv()是阻塞调用若网络抖动超 10 秒未收包会抛websocket._exceptions.WebSocketTimeoutException—— 但项目没做重连需自己加while True:循环兜底。llm_handler.pyLLM 响应引擎。它用subprocess.Popen启动ollama run而非requests.post。原因很实在避免 HTTP 头部开销导致长文本响应超时。generate_response()方法返回的是 raw stdout 流所以response response.strip().split(\n)[-1]这行代码本质是在取最后一行Ollama 流式输出的 final chunk。tts_engine.py语音合成模块。调用的是edge-tts微软 Edge 浏览器 TTS 引擎非本地模型。--voice zh-CN-XiaoxiaoNeural是硬编码若你的系统没装中文语音包Windows 设置 → 语音 → 下载语言包会静音。实测发现--rate20%参数能显著改善语速拖沓感。live2d_renderer.py渲染中枢。它用pyglet窗口托管 Live2D但关键帧更新逻辑在update()方法里每 33ms≈30fps读一次audio_level来自tts_engine.py的音频缓冲区 RMS 值再查表映射到 mouth_open_ratio。这个查表函数get_mouth_ratio()是线性的但真实语音频谱非线性所以config/live2d_config.json里的mouth_curve字段其实是预留的非线性校正接口当前为空需手动补[[0,0],[0.2,0.1],[0.5,0.6],[1.0,1.0]]这样的折线点。2.4 /assets资源加载区字体与音效影响体验下限/assets/fonts/必须含NotoSansCJKsc-Regular.otf。Live2D 文字渲染用freetype-py加载此字体若缺失live2d_renderer.py初始化时会报FT_Exception: cannot open resource并退出。别用 Windows 自带simhei.ttf——它缺 CJK 统一汉字扩展 B 区字符弹幕出现生僻字直接乱码。/assets/sounds/含notification.wav弹幕触发音和error.wavLLM 超时音。这两个 WAV 文件采样率必须是 44100Hz/16bit否则playsound库在某些 Win10 版本会静音。实测用 Audacity 转码时勾选 “Resample to 44100 Hz” 即可。2.5 /utils工具集藏着三个救命函数audio_utils.py的normalize_audio_buffer()TTS 输出的 PCM 数据是 int16但 Live2D 输入要 float32 归一化到 [-1.0, 1.0]。这个函数做了除以 32768.0 的转换不能跳过否则口型张合幅度爆炸。text_filter.py的filter_sensitive_words()内置 137 个中文敏感词如“死”、“杀”、“赌”匹配后自动替换为*。规则是前缀树Trie实现O(1) 查询。若你要加词直接往SENSITIVE_WORDS列表 append 字符串即可无需重启。log_manager.py日志按INFO/WARNING/ERROR三级写入/logs/virtualwife.log且自动轮转单个最大 5MB保留 5 个历史文件。排查问题时第一眼盯ERROR行第二眼查WARNING里LLM timeout after 15s这类提示。2.6 /scripts启动与调试脚本拒绝黑窗口裸奔start.bat主入口。它干三件事激活venv、设置PYTHONPATH.、执行python main.py。不要双击运行——必须右键“以管理员身份运行”否则pyglet窗口在 Win10 上可能被缩放模糊DPI 感知问题。debug_mode.bat启动时加--debug参数会打开两个额外窗口Audio Monitor实时显示音频波形和LLM Debug Console显示原始 prompt 和 raw response。这是定位“嘴不动”或“不回弹幕”的第一现场。reset_config.bat一键恢复config/下所有文件为初始状态。当你改崩配置又记不清原值时比手动 Git checkout 快 10 秒。3. 本地部署全流程从安装依赖到首播成功的七步闭环部署不是复制粘贴命令就完事。每一步背后都有一个必须确认的技术点漏掉任意一个你都会卡在“黑屏”“无声”“不回话”这种玄学状态。下面是我在线下陪 A 同学调通时逐条验证过的路径。3.1 环境准备Python 3.10 与 Ollama 的版本锁死VirtualWife 用到了asyncio.run_coroutine_threadsafe()的新特性必须 Python ≥ 3.10。低于此版本bilibili_client.py会报AttributeError: module asyncio has no attribute run_coroutine_threadsafe。Ollama 要求≥ 0.3.0因为旧版不支持--format json参数而llm_handler.py依赖此参数解析流式输出。验证方法# PowerShell 中执行 python --version # 必须输出 Python 3.10.x ollama --version # 必须输出 0.3.0 或更高若版本不符去官网下载对应安装包Python 从 python.orgOllama 从 ollama.com不要用 pip install ollama——那是 Python SDK不是服务端。3.2 依赖安装用 requirements.txt 还是手动 pip项目根目录有requirements.txt但直接pip install -r requirements.txt会失败——因为pyglet2.0.14与pynput1.7.6在 Windows 上有编译冲突。正确做法是分两步# 先装图形库需 VS Build Tools pip install pyglet2.0.14 # 再装其余纯 Python pip install -r requirements.txt --no-deps--no-deps是关键避免 pip 自动装pyglet的依赖。实测某次pip install pynput会顺带升级pyglet到 2.1.0导致 Live2D 渲染窗口白屏API 变更未适配。3.3 模型加载Ollama 拉取与 Live2D 模型校验Ollama 模型必须用ollama pull命令拉取不能手动丢.gguf文件# 在项目根目录执行 ollama pull phi:mini # 注意不是 phi-3-miniOllama 的 NAME 是简写的拉取完成后检查ollama list输出是否含phiNAME 列且SIZE与models/ollama/phi-3-mini.Q4_K_M.gguf大小一致1.8GB。Live2D 模型校验更关键用文本编辑器打开/models/live2d/model3.json搜索moc3确认path: moc3.moc3存在再搜索textures确认path: texture_00.png存在。少一个路径live2d_renderer.py初始化时就会FileNotFoundError。3.4 配置填写B 站房间与密钥的三处校验B 站接入不是填对room_id就行。bilibili_config.toml有三个必填字段room_id 1234567必须是纯数字不能带https://live.bilibili.com/前缀access_key abcd1234...从 B 站开放平台申请不是 cookie 里的 SESSDATAapp_key 1234567890abcdef同上申请时分配的 16 位 hex 字符串填完后运行python -m utils.debug_bilibili_auth项目自带的调试模块它会尝试连接 B 站鉴权服务器。成功返回{code:0,message:0,ttl:1}失败则返回具体错误码如code10016是 access_key 过期。3.5 首次启动用 debug 模式捕获第一帧问题不要直接双击start.bat。用 PowerShell 进入项目根目录执行python main.py --debug此时会弹出三个窗口主窗口Live2D 模型Audio Monitor绿色波形图说话时应跳动LLM Debug Console空白等弹幕触发关键观察点若 Audio Monitor 无反应检查tts_engine.py是否调用了edge-tts运行edge-tts --list-voices | findstr zh-CN看是否有中文语音若主窗口黑屏用任务管理器看python.exe进程 CPU 占用是否 90%若是大概率pyglet初始化失败重装pyglet2.0.14若 LLM Debug Console 一直空发一条弹幕“你好”看 console 是否打印PROMPT: ...没有则bilibili_client.py未收到消息检查room_id是否填错3.6 弹幕响应验证构造最小测试用例B 站弹幕有审核机制普通“你好”可能被吞。用以下三句话依次测试test123纯字母数字必过虚拟老婆在吗含关键词触发默认回复讲个笑话触发 LLM 生成每发一句盯LLM Debug Console若出现PROMPT: ...但无RESPONSE:LLM 卡住检查ollama_config.yaml的timeout_sec默认 15调大到 30若出现RESPONSE: ...但嘴不动检查tts_engine.py是否成功生成temp/output.mp3用播放器打开听是否真有声若有声但嘴不动audio_utils.py的normalize_audio_buffer()返回值是否为全 0用print(np.max(buffer))插桩验证3.7 推流上线用 OBS 替代内置推流推荐方案项目自带rtmp_publisher.py但实测在 B 站推流稳定性差丢帧率 15%。强烈建议用 OBS 推流在 OBS 新建“窗口捕获”源选择 VirtualWife 主窗口设置分辨率1280x720必须与 Live2D 模型 canvas 一致否则拉伸变形推流地址rtmp://live-push.bilivideo.com/live-bvc流名称{room_id}_{access_key}B 站要求格式这样做的好处OBS 有硬件加速、丢帧补偿、码率自适应比 Python 自研推流稳 3 倍。A 同学第一次用内置推流播了 2 小时B 站后台显示卡顿率 41%切 OBS 后降到 1.2%。4. 避坑指南六个血泪经验总结覆盖 92% 的首次翻车场景部署 VirtualWife 最容易栽在“看起来都对就是不动”的状态。以下是我在某跨平台系统集成中踩过的坑按发生频率排序每条都附可复现现象、根本原因和一招解决法。4.1 现象Live2D 模型显示黑屏或白屏CPU 占用飙高原因pyglet版本不兼容。VirtualWife 依赖pyglet2.0.14的 OpenGL 上下文初始化逻辑而pip install pyglet默认装最新版2.1.x其Window.__init__()方法签名变更导致live2d_renderer.py的setup_gl()调用失败但异常被静默吞掉。解决pip uninstall pyglet -y pip install pyglet2.0.14验证启动后任务管理器看python.exeCPU 是否回落至 10%且主窗口显示模型轮廓。4.2 现象弹幕能收到LLM 也返回文字但 TTS 不发声嘴也不动原因tts_engine.py调用edge-tts时Windows 系统未启用“语音识别”功能。Edge TTS 引擎底层依赖 Windows Speech Platform若系统设置 → 隐私 → 语音识别 关闭则edge-tts进程会秒退subprocess.run()返回码 1但代码里没检查returncode。解决打开 Windows 设置 → 隐私 → 语音识别 → 开启“在线语音识别”在 PowerShell 运行edge-tts --text 测试 --write-media temp/test.mp3看是否生成文件若仍失败运行DISM /Online /Enable-Feature /FeatureName:SpeechRuntime /All /NoRestart启用语音运行时4.3 现象Ollama 模型加载成功但每次 LLM 响应都超时15 秒后报 ERROR原因ollama_config.yaml的model_name与ollama list输出的 NAME 不一致。Ollama 对 NAME 大小写敏感phi-3-mini和phi:mini是两个模型。项目代码里llm_handler.py的run_ollama()方法用subprocess.Popen([ollama, run, model_name])若 NAME 错Ollama 会卡在下载界面无网络时假死。解决ollama list # 看清楚 NAME 列实际值 # 修改 ollama_config.yaml 的 model_name 为完全一致的字符串 # 例如model_name: phi:mini不是 phi-3-mini4.4 现象B 站弹幕能收到但发送“讲个笑话”后LLM Debug Console 显示RESPONSE: null原因llm_handler.py的parse_ollama_stream()函数假设 Ollama 输出 JSON 行但某些模型如 Qwen2在流式输出时首行是{model:qwen2...}后续才是{response:...}。而代码只取最后一行json.loads(line)若最后一行是空或{}就返回null。解决修改llm_handler.py第 87 行# 原代码 response json.loads(lines[-1]).get(response, ) # 改为 for line in reversed(lines): try: obj json.loads(line) if response in obj and obj[response].strip(): response obj[response] break except: continue else: response 4.5 现象Live2D 嘴型张合幅度极小像轻微抽搐跟不上语音节奏原因live2d_config.json的mouth_open_threshold设太高如 0.5导致只有爆破音b/p/t才能触发张嘴而中文元音a/e/i振幅不足。同时mouth_curve为空线性映射无法拟合语音频谱特性。解决将mouth_open_threshold从 0.35 降至 0.25在mouth_curve字段填入非线性校正点mouth_curve: [[0.0,0.0],[0.15,0.05],[0.3,0.25],[0.5,0.6],[0.8,0.9],[1.0,1.0]]重启程序用debug_mode.bat观察 Audio Monitor 波形与嘴型同步性4.6 现象OBS 推流画面正常但 B 站观众看到模型卡在第一帧不眨眼不动作原因OBS “窗口捕获” 源默认勾选“捕获透明度”而 VirtualWife 主窗口使用pyglet的set_alpha(0.0)实现背景透明。开启此选项会导致 OBS 捕获到 alpha 通道B 站播放器解码异常。解决在 OBS 中右键“窗口捕获”源 → 属性取消勾选 “Capture transparency”点击“确定”重启 OBS 源5. 进阶技巧用弹幕关键词触发 Live2D 动作组实现真正交互VirtualWife 默认只响应弹幕文本生成回复但 Live2D 模型本身支持多组预设动作如挥手、点头、生气。项目预留了接口只是没在 UI 暴露。下面教你用 3 行代码让观众发“比心”就触发heart.motion3.json动作发“鼓掌”就播clap.motion3.json。这才是 B 站直播该有的沉浸感。5.1 动作组准备把 motion3.json 放进指定路径Live2D 动作文件必须是.motion3.json格式Cubism 导出且放在/models/live2d/motions/目录下。命名规则{动作名}.motion3.json例如heart.motion3.json比心动作clap.motion3.json鼓掌动作angry.motion3.json生气动作关键点打开heart.motion3.json搜索motion_groups确认存在group: gesture字段。项目代码里live2d_renderer.py的play_motion()方法只认group值为gesture、idle、talk的动作。若你的动作文件group是custom需手动改为gesture。5.2 弹幕关键词绑定修改 text_filter.py 的触发逻辑打开/utils/text_filter.py找到filter_sensitive_words()函数下方插入新函数def parse_danmu_action(danmu_text: str) - Optional[str]: 解析弹幕关键词返回对应动作组名 danmu_text danmu_text.strip() if 比心 in danmu_text or ♥ in danmu_text: return heart elif 鼓掌 in danmu_text or in danmu_text: return clap elif 生气 in danmu_text or 怒 in danmu_text: return angry return None然后在/core/bilibili_client.py的handle_danmu()方法里约第 120 行在self.llm_queue.put(...)之前插入# 新增解析动作关键词 action_name parse_danmu_action(danmu_text) if action_name: # 直接触发动作不走 LLM self.live2d_queue.put((motion, action_name)) return这样当弹幕含“比心”时直接跳过 LLM向 Live2D 渲染器发指令。5.3 渲染器指令接收扩展现有队列协议/core/live2d_renderer.py的update()方法里self.command_queue当前只处理(text, str)类型。我们要支持(motion, str)# 在 update() 方法开头command_queue.get_nowait() 后添加 try: cmd_type, cmd_data self.command_queue.get_nowait() if cmd_type motion: # 播放指定动作 self.play_motion(cmd_data) # 此方法已存在 return except queue.Empty: passplay_motion()方法会自动查找/models/live2d/motions/{cmd_data}.motion3.json并播放。注意动作播放期间talk动作会暂停所以(motion, heart)和(text, ...)不会冲突。5.4 效果验证与参数调优表格改完代码后不用重启整个服务。live2d_renderer.py支持热重载pyglet的schedule_interval机制只需保存文件3 秒内生效。以下是常用动作的参数调优参考基于实测 Phi-3-mini Live2D Cubism 4.4动作关键词动作文件名推荐fade_in_sec推荐priority说明比心heart.motion3.json0.152priority2表示高于talk1可打断说话鼓掌clap.motion3.json0.22鼓掌时手臂摆动幅度大fade_in_sec太小会生硬生气angry.motion3.json0.053priority3最高可打断一切动作适合紧急响应点头nod.motion3.json0.11priority1与talk同级可叠加边说边点头fade_in_sec控制动作淡入时间priority控制动作抢占权。这些值在play_motion()调用时传入例如self.play_motion(heart, fade_in_sec0.15, priority2)5.5 防刷保护给动作指令加冷却时间观众刷“比心”一百次模型不会疯掉——我们加个 5 秒冷却在/core/live2d_renderer.py类顶部加from time import time self.last_action_time {}在play_motion()方法开头加now time() if now - self.last_action_time.get(action_name, 0) 5.0: return # 5秒内不重复播同一动作 self.last_action_time[action_name] now这样即使弹幕洪水每个动作每 5 秒最多触发一次既保体验又防崩溃。从那以后我每次给客户部署 VirtualWife都强制走一遍这五步先ollama list确认模型名再edge-tts --list-voices看语音包接着用debug_mode.bat看三窗口联动然后发三条测试弹幕最后才切 OBS 推流。少走一步后面两小时都在查日志。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?