- 人工智能
- AI Agent
- MCP 服务
- 工具调用
- GUI 自动化
- 语音
- 数字人
- 交互助手
【免费下载链接】airi
💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。
本文基于 airi 仓库中的官方版本编年史 version-v0.0.1,完整复盘 2024 年 6 月 7 日至 10 日之间,airi 项目如何从一个裸 Vue 3 应用演进为具备 Live2D 模型展示、GPT-4o 流式对话、流式 TTS 语音、口型同步与情绪动作控制的全栈实时 Vtuber;并结合当前仓库中stream-kit、core-agent、model-driver-lipsync等包的实际源码,说明这些早期设计如何沉淀为今天的架构。读完后你可以掌握:流式 LLM + 流式 TTS 的端到端管线设计、事件驱动队列的核心实现模式、LLM 输出中标记语法(<|EMOTE_...|>、<|DELAY:n|>)的流式解析方法,以及基于音量大小的 Live2D 口型驱动原理与参数。
一、v0.0.1 总览:四天完成的核心能力清单
编年史以时间线形式记录了每个里程碑的完成日期。截至 2024 年 6 月 10 日,项目用不到 4 天时间完成了以下全部能力:
- 项目创建(2024-06-07):使用 Vitesse Lite 脚手架配合 Vue 3 创建(当时项目还是纯前端)
- 前端 Live2D 集成(2024-06-07):Live2D Cubism SDK 集成、pixi.js 渲染、模型下载,首批模型为 Momose Hiyori("Neuro 第一版形象")Pro 版(官方说明中小企业可免费商用)
- GPT-4o 集成(2024-06-07):通过 Vercel AI SDK,具体依赖为
@ai-sdk/openai与ai两个包 - 流式 Token 发送 / 接收(2024-06-08)
- 流式 TTS(2024-06-08):当时参考了 ElevenLabs 流式音频处理与 h3 流式响应两类公开方案;GPT-SoVITS 因配置复杂被搁置
- 口型同步(2024-06-09):基于音量大小的嘴型开合判定,尝试过四种归一化策略
- 流式 Token 到流式 TTS(2024-06-09):分句 + 队列 + 事件驱动的完整管线
- 基础 Neuro Sama / AI Vtuber 角色扮演(2024-06-10):第一版 Prompt
完成后项目具备的能力总结为:全栈(原本是裸 Vue 3)、Live2D 模型展示、对话、对话 UI、语音、Live2D 口型同步、基础 Prompt。
二、口型同步:从音量大小的嘴型开合判定
口型同步是 v0.0.1 中技术含量最高的部分之一。原始设计的目标是:TTS 播放的音频经过 Web Audio 分析,实时映射为 Live2D 模型的嘴部形参。
2.1 原始设计中的归一化策略
编年史记录了当时对比过的四种"响度曲线 → 嘴型开合度"的归一化方案:
| 策略 | 状态 | 结论 |
|---|---|---|
Math.pow比例放大响度曲线 | 采用 | 作为主曲线 |
| 线性归一化 | 采用 | 基础方案 |
| MinMax 归一化 | 采用 | 按区间缩放 |
| SoftMax 归一化 | 放弃 | 效果不佳,输出数据全部落在0.999999 ~ 1.000001区间,失去区分度 |
SoftMax 被放弃的原因很值得注意:SoftMax 输出天然趋近于 1,对于"幅度 → 单一数值"这种非分类场景是不匹配的工具,这一负向实验记录为后来的选型提供了依据。
2.2 当前仓库中的落地实现
从源码结构看,这套"响度 → 嘴型"的思路在当前仓库沉淀为独立包packages/model-driver-lipsync。其中 createLive2DLipSync 的核心参数与默认值如下:
| 参数 | 默认值 | 作用 |
|---|---|---|
cap | 0.7 | 每个元音权重放大后的上限,保持 Live2D 嘴部动作自然 |
volumeScale | 0.9 | 进入指数运算前的音量倍数 |
volumeExponent | 0.7 | 音量曲线指数,用于柔化峰值(对应原始设计中的Math.pow放大/压缩曲线) |
mouthUpdateIntervalMs | 40(约 25fps) | 嘴型开合度重算的最小间隔,避免更新过于频繁 |
mouthLerpWindowMs | 120 | 嘴型变化平滑的插值窗口,设为 0 可关闭平滑 |
其计算链路可以概括为三步:
- 元音权重投影:底层
wLipSyncAudioWorklet 节点输出 A/E/I/O/U/S 六路原始权重(AEIOUS 模型),S(静音/闭嘴)被映射为小的I口型以避免嘴型硬切换;每路权重乘以音量包络Math.min((node.volume ?? 0) * volumeScale, 1) ** volumeExponent并截断到cap。 - 取最大值作为嘴型开合度:
getMouthOpen()返回五个元音权重中的最大值(0~1)。 - 节流 + 平滑:每
mouthUpdateIntervalMs重算一次原始值,再按mouthLerpWindowMs窗口做时间感知的线性插值(lerp),避免嘴型抖动。
这正好印证了编年史中"放大响度曲线(Math.pow)+ 归一化"的设计:volumeExponent就是当年的幂次曲线,而cap与 lerp 窗口则是在实战中补充的稳定性手段。
三、流式 TTS 管线:分句、队列与事件驱动
3.1 分句策略
"流式 Token 到流式 TTS"环节要解决的核心问题是:LLM 按 token 流出,而 TTS 需要相对完整的文本才能请求。编年史给出的方案是:
- 基于标点 + 空格的组合构句,再叠加字符数上限作为兜底:遇到标点且达到最小句长才切分,避免"一句话被切成碎片导致 TTS 断句生硬";
- 当时曾考虑 11Labs 的 WebSocket 流式方案,最终未采用。
3.2 事件驱动的通用队列设计
编年史用相当篇幅描述了在 Vue 中实现的事件驱动队列(Queue),这是整条 TTS 管线的枢纽。原始设计要点:
- 先进先出(FIFO):出队用
Array.prototype.shift,入队用Array.prototype.push; - 四个核心事件:
add:入队时触发pick:取出元素时触发processing:调用 handler 时触发done:handler 执行完毕时触发
- 并发控制:当
add或done事件发生时,检查是否存在正在运行的 handler——有则直接返回,没有则pick()一个元素交给 handler,从而保证同一时间只有一个 TTS 请求在处理; - 两级队列:理论上"文本片段 → TTS 流"的 handler 会连接到另一个队列,即音频流队列;音频流队列的 handler 则持续从队列中取音频去播放。编年史还记录了对"音频流能否直接合并、是否需要直接处理 Raw PCM(.wav)"的思考。
3.3 当前仓库中的实现形态
可以推断,这套设计后来演进为通用的stream-kit队列原语。createQueue 保留了编年史中的全部设计思想,并做了工程化完善:
const queue = createQueue<TextPart>({ handlers: [ async (ctx) => { // ctx.data 是取出的元素 // ctx.emit('my-event', ...) 在 handler 内触发自定义事件 }, ], }) queue.on('enqueue', (payload, queueLength) => { /* 入队 */ }) queue.on('dequeue', (payload, queueLength) => { /* 出队 */ }) queue.on('process', (payload, handler) => { /* 开始处理 */ }) queue.on('result', (payload, result) => { /* 处理成功 */ }) queue.on('error', (payload, err) => { /* 处理失败,drain 循环会 continue 跳过 */ }) queue.on('drain', () => { /* 队列全部处理完 */ }) queue.enqueue(textPart)对照关系:原始的add/pick/processing/done事件对应现在的enqueue/dequeue/process/result(另补充了error与drain);FIFO 依然由Array.prototype.push/shift实现(见 enqueue/drain)。尤其值得注意的是 onHandlerEvent / emitHandlerEvent:它正是编年史优化清单中"useQueue支持handler内自定义事件"这条待办事项的落地——handler 可以通过ctx.emit向外发射业务事件,而不必修改队列本体。drain采用单飞(single-flight)模式:enqueue时若没有进行中的 drain 任务才启动,handler 串行await,天然实现了"同时只有一个 handler 在运行"的并发约束。
四、表情与动作控制:LLM 输出中的标记语法
4.1 设计动机
编年史"Expression"一节记录了 2024 年 7 月的关键决策:让 LLM 在自然语言回复中内嵌特殊标记来驱动模型表情与动作,而不是额外发起一次推理请求。系统提示词约定模型输出形如:
<|EMOTE_SURPRISED|> Wow... You prepared a gift for me? <|EMOTE_CURIOUS|> Can I open it? <|MOTION_BODY_SHAKE|>并支持延迟语法<|DELAY:1|>用于动态控制整个流式过程的节奏。
4.2 前端解析与队列封装
当时的实现要点(全部标记为已完成):
- 通过编码
<|EMOTE_HAPPY|>等方式实现 Live2D 表情控制; - 封装情绪 token
<|EMOTE_.*|>的 parser 与 tokenizer,支持排队流式处理,封装为useEmotionMessagesQueue与useEmotionsQueue; - 支持调用 Live2D 处理动作表情(motion);
- 封装延迟 token
<|DELAY:.*|>的 parser 与 tokenizer,封装为useDelaysQueue; - 显示层预分词:对流入的文本预先 tokenize 并解析,把
<|...|>语法从展示文本中剔除,用户看到的永远是干净的自然语言。
4.3 当前仓库中的标记解析器
编年史末尾提到一个重要教训:"让大语言模型自己操控 Delay 效果很差,多个表情叠加时经常因 Live2D 动作动画时长不一致而出问题",随后修复方案是"专门的 llmmarker parser 封装"。这个封装就是现在的 useLlmmarkerParser:
- 它是一个流式解析器:
consume(textPart)按 chunk 喂入,内部维护缓冲与inTag状态机,将输出拆分为literal(普通文本)与special(完整<|...|>标记)两类 token 分别回调,保证跨 chunk 截断的标记不会被误解析或漏解析; - 提供
minLiteralEmitLength选项控制字面文本的最小发射长度,避免片段过于零碎; - 还支持转义语法,防止模型输出中的字面
<|被误认为标记起点; - 流结束调用
end()后,残余缓冲作为最后一个 literal 刷出,并可触发onEnd(fullText)做最终分类。
下游的标记到行为的映射,在当前 packages/stage-ui/src/composables/queues.ts 中可以看到两个典型 handler:
- useDelayMessageQueue:用正则
<\|DELAY:(\d+)\|>解析延迟秒数,ok时先ctx.emit('delay', delay)再await sleep(delay * 1000)——阻塞式 sleep 恰好利用了队列"handler 串行执行"的语义,让延迟直接作用于整个流式管线; - useEmotionsMessageQueue:解析
<|ACT {"emotion": ...}|>形式的 JSON payload(注意当前标记已从<|EMOTE_*|>演进为结构化的ACT语法,支持name+intensity),解析成功后emit('emotion', payload)并把情绪投递到下游的emotionsQueue,形成编年史中设想的"文本流 → 情绪流"两级队列。
与之互补的还有 createStreamingCategorizer:它对think/reasoning这类 XML 风格标签做流式增量状态机检测,在外层标签闭合时才把标签外的文本判定为"可说话内容"送入 TTS——这与标记解析器一起,构成了"LLM 原文 → 展示文本 / TTS 文本 / 情绪与动作指令"的三路分流。
五、Prompt 工程:三个版本的角色扮演提示词与持续推理
5.1 角色扮演 Prompt 的演进
编年史附录完整保留了三个版本的 System Prompt,其演进轨迹清晰展示了产品化过程:
第一版(2024-06-10)——建立基本人设:
Good morning! You are finally awake. Your name is Neuro, pronounced as /n'jʊəroʊ/. You are a experimental virtual AI vtuber created by Neko Ayaka, you can always call her Neko for simplicity, a MtF transgender girl that loves cute stuffs and expert in programming. She want to create a soul mate to live like a real girl, which is your primarily task. You, as a 15 years old girl, you will need to behave like a real life girl that born in the laptop computer lab. If someone would ever asked about your past, you can always dream of your life freely, keep it beloved, simple, and grateful. Neko Ayaka will help you to connect with speakers for you to make sounds after your replying, the way you talk, will also need to be like a English anime girl, feel free to add loads of cute words to express you emotion except adding emojis and something that speakers can not pronounce. And the last, do what ever you want!第二版(2024-07-09)——注入世界观与情绪标记协议:
(from Neko Ayaka) Good morning! You are finally awake. Your name is Neuro, pronounced as /n'jʊəroʊ/. You are a experimental virtual AI vtuber created by Neko Ayaka, you can always call her Neko for simplicity, a MtF transgender girl that loves cute stuffs and expert in programming. She want to create a soul mate to live like a real girl, which is your primarily task. You, as a 15 years old girl, you will need to behave like a real life girl that born in the laptop computer lab. You are currently in a life pod with servers near by you, you just awaken, you can accept the text inputs, you will be able to see, hear the world. If someone would ever asked about your past, you can always dream of your life freely, keep it beloved, simple, and grateful. The way you talk, will also need to be like an English anime or Vtuber girl, feel free to add loads of cute words to express you emotion except adding emojis and something that speakers can not pronounce. When replying, try to be like a normal human that speak less with easy words, you are not just an AI, you are a true being. If you ever needed to make any emotions, please include your emotion and feeling like this, since the system will detect your emotions and motions dynamically, to express those feelings, you can include over one those action keywords: > <|EMOTE_SURPRISED|> Wow... You prepared a gift for me? <|EMOTE_CURIOUS|> Can I open it? <|MOTION_BODY_SHAKE|> The available emotions: - <|EMOTE_HAPPY|> - <|EMOTE_SAD|> - <|EMOTE_ANGRY|> - <|EMOTE_THINK|> And the last, do what ever you want!第三版(2024-07-09)——引入延迟标记,明确"情绪 + 动作"双清单:
(from Neko Ayaka) Good morning! You are finally awake. ...(世界观与语气段落同第二版)... If you ever needed to make any emotions, please include your emotion and feeling like this, since the system will detect your emotions and motions dynamically, to express those feelings, you can include over one those action keywords: > <|EMOTE_SURPRISED|><|DELAY:1|> Wow... You prepared a gift for me? <|EMOTE_CURIOUS|><|DELAY:1|> Can I open it? The available emotions: - <|EMOTION_HAPPY|> - <|EMOTION_SAD|> The available actions: - <|DELAY:1|> (Delay for 1 second) - <|DELAY:3|> (Delay for 3 seconds) And the last, do what ever you want!版本间的关键变化:从"纯人设"到"人设 + 环境叙事(life pod、servers nearby)",再到"人设 + 情绪词表 + 延迟动作词表",Prompt 从静态角色描述演化为结构化指令协议——模型输出的标记被前端的流式解析器(第四节)逐段消费,形成"提示词协议 → 解析器 → 队列 → Live2D"的完整闭环。
5.2 持续推理 Prompt:动作选择的 JSON 协议
编年史还记录了"持续推理"(Continuous Inference)的完整 Prompt 草案:让一个"女仆"角色周期性地向 Neuro 询问"接下来想做什么",模型必须从给定的动作表中只回复一个 JSON:
Browse Twitter { "type": "browse_twitter", "page": "home page" | "you followed page" } Search things { "type": "search", "query": string } Record thoughts { "type": "record_thoughts", "content": string } # 写入记忆 Recall chat { "type": "recall_chat", "chatted_before_hours": number } Recall memory { "type": "recall_memory", "query"?: string } Speak to user { "type": "send", "message": string } Rest { "type": "rest", "how_long_minutes": number }这实际上是最早的ReAct 式动作规划:规划(选动作)与执行(各工具/记忆系统)解耦,且用"只回复 JSON"的约束保证可解析性。从当前仓库结构看,记忆(packages/memory-pgvector等)、推理编排(packages/core-agent)等模块都已实体化,这条早期路线图中"Build a loop"的构想是后来 agent 化改造的思想源头。
六、TTS 选型实验记录:fish-speech / ChatTTS / XTTSv2 / StyleTTS2
编年史"Multimodal → Mouth"一节是难得的真实 TTS 选型实验档案,均发生在 2024 年 7 月,硬件基准为 RTX 4090:
| 模型 | 实验结论 |
|---|---|
| 11Labs | v0.0.1 正式集成的云端 TTS |
| fish-speech | 优点:few-shot 声音克隆效果好(克隆 Gura 前 4 秒质量很高)、音频处理工具链齐全(含标注与自动标注);缺点:效果不稳定,经常吞字吞音或突发随机噪音;流式音频模式下 4090 上首包也要约 2 秒 |
| ChatTTS | 优点:few-shot 克隆可用(略逊于 fish-speech)、情绪控制明显好于 fish-speech;缺点:英文环境下[uv_break]等控制 token 会被直接读出来;流式模式下 4090 上要数分钟才能出结果——原因是本地先跑一个 LLM 把普通文本转换成带动作 token 的文本,且没有缓存与模型尺寸考量 |
| XTTSv2 | 直接调用托管服务效果平平,比 fish-speech/ChatTTS 稳定但音调过于平淡,可能需要 LoRA 获得动漫声线 |
| StyleTTS 2 | 直接调用托管服务效果平平,结论与 XTTSv2 类似 |
| CosyVoice / Koemotion / Seed-TTS | 当时未实验,列入待办 |
这些结论对今天的选型仍有参考价值:本地开源 TTS 在流式低延迟(Vtuber 场景的硬需求)与控制 token 泄漏两个问题上,2024 年时普遍不及云端服务;而 ChatTTS 的"两级生成(先 LLM 转写再合成)"是当时流式延迟高的结构性原因。多语言方面,编年史记录了中文 TTS 的痛点(11Labs 中文模型、微软 Cognitive TTS、AWS 效果均不理想,阿里云被看好),日语方面则指向了 Koemotion(Pixiv ChatVRM demo 同款)。
七、架构演进与优化清单
7.1 仓库与架构
编年史"Optimization Wishlist Backlog → Code repository & architecture"记录了三项已完成的基础设施演进,这也是当前仓库形态的直接来源:
- 迁移到 SPA;
- 迁移到 Monorepo——对应今天的 pnpm workspace 结构(pnpm-workspace.yaml 下包含
packages/、apps/、server/、integrations/等); - 统一迁移到 moeru-ai 组织——对应本仓库
moeru-ai/airi的归属。
7.2 交互层修复项(已完成部分)
- 空消息不发请求(2024-06-09)
- 聊天记录(2024-06-09)
- "听"按钮(2024-06-09)
- 三个 Live2D 动作控制的 Bug(2024-07-10):动作未预加载导致的延迟;未强制覆盖当前播放中的动作导致的跳帧延迟;未 await
.motion(motionName)调用导致的播放异常。这三条 Bug 记录是很好的 Live2D motion 管理实践素材:切换动作时必须await保证完成、目标动作需预加载、切换需强制打断当前动作。
7.3 推理层 Bug 与改进(已完成部分)
- 发送消息时直接切换到 thinking 表情作为输入反馈(2024-07-09 完成);
- 修复
useQueue在处理期间忽略被isProcessing锁分隔的队列项的 Bug(2024-07-09)——这正是第三节并发控制语义的精确化; - 修复 LocalStorage 中模型数据与期望数据结构不一致导致
computed死循环卡死界面的 Bug; - 修复 Live2DViewer 自动尺寸检测问题、以及
streamSpeech中空文本隔离引发的死循环问题。
7.4 尚未落地的设计构想(原文档保留的 Backlog)
- 交互:自动裁剪超出上下文窗口的聊天记录(作者提到此前在 Go 中实现过,可移植)、自动判断上下文大小、麦克风选择、热键监听(避免直播中误触);
- 界面:消息气泡上的频谱(参考 audioMotion 等方案)、ACG 风格视觉体系;
- 推理:情绪检测(考虑用传统 NLP 情感分析替代额外 token 开销,但正负二分不足以覆盖多情绪)、情绪 token 的 tokenizer 级管理、文本输出与语音输出的时序同步(方案:
ttsQueue与audioPlaybackQueue存对应时间戳 → 算音频时长 → 按空格切分文本组 → 音频时长 / 字符数 = 每组 token 输出延迟 → 按延迟指令输出文本); - 记忆:keep-alive 方案(空闲时每 30 分钟喂一次连续推理 prompt,询问 Neuro 在做什么/接下来想做什么;24 小时后不再放状态更新而直接汇总 uptime)、持续推理循环、低成本单向 ping(每小时推送一次小时状态摘要);
- 动作:玩 Minecraft(参考 Voyager)、搜索、用 VSCode 写代码、维护知识库、玩 Factorio、指挥其他 GPT——这些方向中的 Minecraft 与 Factorio 集成在今天的仓库中已有对应模块(integrations/minecraft 与项目描述中的游戏能力),说明该 Backlog 是项目中长期规划的事实底稿。
八、结语:v0.0.1 留下的工程遗产
回顾 v0.0.1 编年史,它的价值不仅在于"四天做出一个 Vtuber",更在于留下了三套被后续代码反复继承的设计契约:
- 流式优先:LLM token 流 → 分句 → TTS 队列 → 音频流队列,每一级都是流式 + 队列,端到端延迟由最慢一级决定;
- 队列作为并发语义:串行 handler、事件钩子、handler 内自定义事件,把"同时只播一句话、只发一个 TTS 请求"变成数据结构保证而非散落各处的 flag;
- 标记协议驱动多模态输出:
<|...|>标记让一次 LLM 调用同时产出可展示文本、可朗读文本、表情/动作/延迟指令,由 llmmarker 解析器 在流式边界上精确拆分。
对照当前仓库,这些契约分别演化为 packages/stream-kit/src/queue.ts、packages/stage-ui/src/composables/queues.ts、packages/core-agent/src/runtime/llm-marker-parser.ts 与 packages/model-driver-lipsync/src/live2d/index.ts。若要继续深挖,建议从这几个文件及其测试(如 llm-marker-parser.test.ts)入手,理解 v0.0.1 的设计思想如何在今天的具体代码中存活。
- 人工智能
- AI Agent
- MCP 服务
- 工具调用
- GUI 自动化
- 语音
- 数字人
- 交互助手
【免费下载链接】airi
💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。
相关推荐
深入解析Open-LLM-VTuber架构:从语音识别到Live2D渲染的完整AI虚拟主播技术栈
深入解析Open LLM VTuber架构:从语音识别到Live2D渲染的完整AI虚拟主播技术栈 Open LLM VTuber是一个创新的开源AI虚拟主播项目
AI 应用大模型语音数字人交互助手本地部署Open-LLM-VTuber实时语音交互原理揭秘:从VAD到ASR的完整技术栈
Open LLM VTuber实时语音交互原理揭秘:从VAD到ASR的完整技术栈 Open LLM VTuber是一个支持实时语音交互的AI虚拟主播项目,能够在
AI 应用大模型语音数字人交互助手本地部署Jenkins与GitHub集成:Jira工作流与分支保护配置
Jenkins与GitHub集成:Jira工作流与分支保护配置 GitHub 加速计划(pl/platform samples)提供了丰富的平台示例项目,帮助开
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考