简介:这是一套面向二次元爱好者与AI应用开发者的互动聊天模型设计源码,融合Live2d、VITS与ChatGPT三项技术,目标是复刻类似哔哩哔哩的虚拟角色实时交流体验。项目以Java为主要开发语言,适合具备一定Android与深度学习基础、希望研究多技术栈整合的开发者参考学习。压缩包共200个文件,约74.59MB,其中95个XML配置文件承担界面布局与参数设置,52个Java源文件实现消息处理、互动逻辑与语音合成,另有PNG、JPG图片资源用于角色形象展示,以及gradle构建脚本、json与properties配置等辅助文件。目前已有445人学习下载。通过研读源码,读者可掌握Live2d角色动画驱动、VITS音色合成与ChatGPT对话生成的协同方式,理解项目目录组织与构建流程,为二次元AI应用开发积累可复用的实践思路。
1. 从一张会眨眼的表情到能对话的虚拟角色:这套源码到底在解决什么
很多人第一次看到「基于 Live2d+VITS+ChatGPT 的二次元互动聊天模型」这类源码,脑子里浮现的是 B 站上那种会眨眼、会跟着鼠标转头、还能开口说话的虚拟主播。但真把源码拉下来跑,十有八九卡在第一步:模型加载不出来,或者语音合成报错,或者 ChatGPT 接口连不上。这套方案的本质,是把三个原本独立的模块串成一条链路——Live2d 负责「脸」,VITS 负责「嘴」,ChatGPT 负责「脑子」。三者之间靠一层薄薄的调度逻辑粘起来,谁掉链子整条链路就哑火。
它适合两类人:一类是想给自己的桌面助手、直播工具、游戏 NPC 加一层「能看能说」外壳的开发者;另一类是手里已经有 Live2d 模型资源,想让它从「只会做预设动作」变成「能根据对话内容实时反应」的折腾党。源码本身不复杂,复杂的是三个模块各自的依赖环境和版本兼容性。下面按「先跑通再优化」的顺序拆,每一步都给出可复现的命令和参数。
2. 三件套怎么串起来:Live2d 渲染、VITS 合成、ChatGPT 调度的最小闭环
2.1 为什么是这三个,而不是别的组合
Live2d 在二次元角色渲染上是事实标准,模型资源多、社区工具链成熟,Web 端有 pixi-live2d-display,桌面端有 live2d-py 这类绑定。VITS 是目前开源 TTS 里音质和推理速度平衡得比较好的方案,支持少样本微调,想换成特定角色的声线只需要几十条音频。ChatGPT 负责对话生成,接口稳定、上下文管理简单。三者组合的合理性在于:Live2d 输出的是视觉帧,VITS 输出的是音频流,ChatGPT 输出的是文本,三者通过一个事件循环解耦,互不阻塞。
常见做法是用 Python 做主调度,WebSocket 做前后端通信。前端只负责渲染 Live2d 和播放音频,后端跑 VITS 推理和 ChatGPT 请求。这样拆的好处是:换模型不用动后端,换对话模型不用动前端。
2.2 最小可运行链路的搭建步骤
先装依赖。VITS 推荐用官方推理脚本或 MoeGoe 这类封装,ChatGPT 用 openai 官方 SDK,Live2d 前端用 pixi-live2d-display。
# 后端依赖 pip install openai websockets numpy torch # VITS 推理(以 MoeGoe 为例,需单独下载模型和配置文件) # 前端依赖 npm install pixi.js pixi-live2d-display后端调度核心逻辑如下:
import openai import asyncio import websockets import json openai.api_key = "你的key" openai.api_base = "https://api.openai.com/v1" # 如用兼容接口改这里 async def chat_handler(websocket, path): async for message in websocket: user_text = json.loads(message)["text"] # 1. 调 ChatGPT 拿回复 resp = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": user_text}], temperature=0.8, max_tokens=200 ) reply = resp.choices[0].message.content # 2. 把回复文本发给 VITS 合成音频 audio_path = await vits_tts(reply) # 自定义函数,调 VITS 推理 # 3. 把文本和音频路径一起推回前端 await websocket.send(json.dumps({ "reply": reply, "audio": audio_path })) async def vits_tts(text): # 这里调 VITS 推理脚本,输出 wav 文件 # 实际项目中建议用子进程或独立服务,避免阻塞事件循环 import subprocess out = "output.wav" subprocess.run(["python", "vits_infer.py", "--text", text, "--out", out]) return out asyncio.get_event_loop().run_until_complete( websockets.serve(chat_handler, "localhost", 8765) ) asyncio.get_event_loop().run_forever()这段代码的关键点:ChatGPT 的temperature控制回复随机性,二次元角色建议 0.7 到 0.9,太低会像客服,太高会胡言乱语。max_tokens限制在 200 以内,因为 VITS 合成长文本会明显变慢,而且 Live2d 口型同步对长句不友好。VITS 推理放在子进程里是血泪经验——直接在主事件循环里跑会卡住 WebSocket 心跳,前端表现为「说着说着突然不动了」。
前端加载 Live2d 模型的核心代码:
import * as PIXI from 'pixi.js'; import { Live2DModel } from 'pixi-live2d-display'; const app = new PIXI.Application({ view: document.getElementById('canvas'), autoStart: true, resizeTo: window }); const model = await Live2DModel.from('path/to/model.model3.json'); app.stage.addChild(model); model.x = app.screen.width / 2; model.y = app.screen.height / 2; // WebSocket 接收后端消息 const ws = new WebSocket('ws://localhost:8765'); ws.onmessage = (event) => { const data = JSON.parse(event.data); // 播放音频 const audio = new Audio(data.audio); audio.play(); // 触发口型动作(需模型支持口型参数) model.motion('tap_body'); // 示例动作名,按模型实际动作改 };model.motion里的动作名必须和模型 JSON 里定义的一致,不同模型差异很大。口型同步如果模型没有内置口型参数,需要额外做音频振幅分析,这部分后面章节展开。
2.3 参数怎么调才不翻车
ChatGPT 侧三个参数最影响体验:model选 gpt-3.5-turbo 够用,gpt-4 成本高且延迟大;temperature0.8 左右;presence_penalty和frequency_penalty各设 0.3 到 0.5,避免角色反复说同一句话。VITS 侧关键是noise_scale和noise_scale_w,前者控制音色随机性,0.6 到 0.8 比较自然,后者控制音素时长,1.0 是默认值,调低会让语速变快。Live2d 侧主要是帧率和模型缩放,移动端建议锁 30fps,桌面端 60fps。
3. 把 VITS 换成自己的声线:少样本微调与推理加速的实操路径
3.1 数据准备和微调流程
VITS 支持少样本微调,一般 30 分钟到 1 小时的目标角色音频就能出效果。音频要求:单声道、22050Hz 或 44100Hz、无背景音乐、每段 3 到 10 秒。准备一个metadata.txt,每行格式是「音频路径|说话人名称|语言代码」。
# 音频重采样和切分(用 ffmpeg) ffmpeg -i raw.wav -ar 22050 -ac 1 -ss 00:00:00 -t 00:00:10 seg1.wav # 生成 metadata echo "seg1.wav|myrole|ZH" >> metadata.txt微调命令以官方 VITS 仓库为例:
python train.py -c configs/myrole.json -m myroleconfigs/myrole.json里关键字段:batch_size根据显存调,8G 显存设 4 到 6;learning_rate设 2e-4;epochs一般 100 到 200 就够,再多会过拟合。训练完后用inference.py测试,听感上如果出现金属音或断字,多半是数据里静音段太多,回去做 VAD 切分。
3.2 推理加速的三种常见做法
VITS 原始推理在 CPU 上跑一句话要 1 到 3 秒,实时对话体验很差。常见加速手段:一是导出 ONNX 用 onnxruntime 推理,速度能快 2 到 3 倍;二是用 TensorRT 进一步优化,但配置复杂;三是把推理服务独立部署,用 HTTP 或 gRPC 调用,避免和主调度抢资源。
# ONNX 推理示例 import onnxruntime as ort import numpy as np sess = ort.InferenceSession("vits.onnx") inputs = { "text": np.array([text_ids], dtype=np.int64), "noise_scale": np.array([0.667], dtype=np.float32), "length_scale": np.array([1.0], dtype=np.float32) } audio = sess.run(None, inputs)[0]noise_scale在 ONNX 里要显式传,默认值不一定和 PyTorch 一致。导出 ONNX 时注意 opset 版本,11 到 14 之间兼容性最好。
4. 避坑与排查:模型加载、接口超时、口型不同步的 5 个真实翻车记录
4.1 现象:Live2d 模型加载后白屏,控制台报 404
原因:model3.json里的贴图路径是相对路径,但实际文件目录结构和 JSON 里写的不一致。很多免费模型下载下来后文件夹名被改过,路径对不上。
解决:打开model3.json,检查textures数组里的路径,确保和实际文件位置一致。如果是 Web 端,路径相对于 HTML 文件,不是相对于 JSON 文件。
4.2 现象:ChatGPT 接口偶尔超时,角色卡住不说话
原因:默认超时时间太短,或者网络抖动。另外如果用了兼容接口,api_base末尾多写或少写/v1都会导致 404。
解决:设置request_timeout=30,并加一层重试。api_base统一写成https://api.openai.com/v1,不要带末尾斜杠。
openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=messages, request_timeout=30 )4.3 现象:VITS 合成出来的音频有杂音或断字
原因:输入文本里有特殊符号、emoji 或英文混排,VITS 的文本前端处理不了。另外noise_scale_w设太低会导致音素时长压缩过度。
解决:合成前做文本清洗,去掉 emoji 和非目标语言字符。noise_scale_w保持 1.0,noise_scale不低于 0.5。
4.4 现象:口型动作和音频对不上,嘴动完了声音还在放
原因:Live2d 的 motion 是预设动画,时长固定,和音频长度无关。如果音频 3 秒,motion 只有 1 秒,就会不同步。
解决:要么用音频振幅实时驱动口型参数,要么把长回复拆成短句,每句触发一次 motion。实时驱动方案需要读取音频的 RMS 值,映射到模型的ParamMouthOpenY参数。
const analyser = audioContext.createAnalyser(); const source = audioContext.createMediaElementSource(audio); source.connect(analyser); analyser.connect(audioContext.destination); function syncMouth() { const data = new Uint8Array(analyser.frequencyBinCount); analyser.getByteTimeDomainData(data); let sum = 0; for (let i = 0; i < data.length; i++) { sum += Math.abs(data[i] - 128); } const rms = sum / data.length; model.internalModel.coreModel.setParameterValueById( 'ParamMouthOpenY', Math.min(rms / 64, 1) ); requestAnimationFrame(syncMouth); }4.5 现象:多轮对话后角色开始重复之前说过的话
原因:ChatGPT 的上下文窗口有限,历史消息太长时早期内容被截断,模型丢失上下文。另外frequency_penalty设太低也会导致重复。
解决:限制历史消息条数,只保留最近 10 轮。frequency_penalty设 0.5,presence_penalty设 0.3。如果角色设定重要,把设定写成 system message 放在最前面,不要放在历史里。
5. 让角色「活」起来的进阶技巧:情绪映射与状态机
5.1 用 ChatGPT 的输出控制 Live2d 表情
单纯让角色说话还不够,真正让人觉得「活」的是表情和动作跟着对话内容变。做法是在 ChatGPT 的回复里附加情绪标签,前端解析后触发对应表情。
system_prompt = """你是一个二次元角色。每次回复时,在文本末尾附加情绪标签, 格式为 [emotion:happy]、[emotion:sad]、[emotion:angry]、[emotion:neutral] 之一。 只输出一个标签。"""前端解析:
const emotionMatch = data.reply.match(/\[emotion:(\w+)\]/); const emotion = emotionMatch ? emotionMatch[1] : 'neutral'; const expressionMap = { happy: 'exp_01', sad: 'exp_02', angry: 'exp_03', neutral: 'exp_00' }; model.expression(expressionMap[emotion]);expression的名字必须和模型 JSON 里expressions数组的Name字段一致。不同模型表情数量差异很大,有的只有两三个,有的十几个。如果模型没有对应表情,退化成 motion 切换也行。
5.2 用状态机管理对话节奏
连续对话时,角色不应该每句话都立刻回复,需要有「倾听」「思考」「说话」三个状态。用一个简单状态机控制:
| 状态 | 触发条件 | Live2d 表现 | 后端行为 |
|---|---|---|---|
| idle | 无输入超过 10 秒 | 待机动作循环 | 无 |
| listening | 收到用户输入 | 头部微倾 | 开始调 ChatGPT |
| thinking | ChatGPT 请求中 | 眨眼频率降低 | 等待接口返回 |
| speaking | 音频播放中 | 口型同步 | 播放音频 |
状态切换用事件驱动,避免轮询。idle状态超过 30 秒可以触发一个随机待机动作,让角色看起来不是完全静止。
5.3 一个我踩过的坑:别在状态机里做阻塞操作
最早我把 VITS 推理直接写在speaking状态的入口里,结果音频还没合成完,状态已经切到speaking,前端开始播一个空文件。后来改成先合成再切状态,并且加了一个synthesizing中间态。这个中间态不对外暴露,只是内部标记,防止重复触发。
另外,ChatGPT 的流式输出和 VITS 的整句合成天然冲突。流式适合文本逐字显示,但 VITS 需要完整句子才能合成自然语调。我的做法是:ChatGPT 用流式拿文本,前端逐字显示,同时后端等完整句子结束后再调 VITS。这样用户看到文字在动,听到的声音稍晚一点,但整体体验比等全部生成完再一起放要好。
这套方案值不值得做,取决于你想要多「真」。如果只是要一个会说话的看板娘,跑通最小链路就够了。如果要让它像直播角色一样有情绪、有节奏、有记忆,那状态机和情绪映射这两层是绕不过去的。我自己的习惯是先把最小链路跑通,录一段屏,回放看哪里别扭,再针对性加状态。别一上来就堆功能,最后连哪一步卡住都定位不到。希望帮到你。
本文还有配套的精品资源,点击获取