别再构建另一个聊天机器人:使用 Rive 架构“Duolingo‑Style”AI 伴侣
Source: Dev.to
我们正被“一堆 AI 包装层”淹没。如果你在构建 AI 语言导师、角色扮演应用或心理健康伴侣,你会遇到一个问题:文字界面太无聊。
目前赢得竞争的应用(比如 Duolingo 的 Lily 或 character.ai)并不仅仅是输出 token;它们在呈现表现。
作为一名专注于 AI 交互的 Rive 动画师,我看过许多此类项目的后端。一个“玩具”应用和一个“产品”之间的区别往往归结为一点:唇形同步架构。
在本文中,我将拆解使用 Rive 构建响应式、唇形同步 AI 角色所需的技术设置,超越简单的音量弹跳,实现音素级别的精准语音。
架构:木偶 vs. 木偶师
要构建一个有生命感的角色,需要分离关注点:
- 木偶(Rive) – 一个基于数值输入处理形状变形的状态机。
- 木偶师(你的代码) – 负责解析音频并向木偶发送信号的 React/Flutter/Swift 逻辑。
第 1 级:“木偶”方法(振幅)
快速方式。 如果你明天就需要一个 MVP,从这里开始。分析音频振幅的均方根(RMS)。
Rive 设置:一个 1‑维混合状态。输入 0 = 嘴闭合,输入 100 = 嘴张开。
// Example (pseudo‑code)
riveInput.value = normalizedVolume;
问题: 看起来像个木偶。角色对 “OO” 与 “EE” 声音的张口幅度相同,缺乏细微差别。
第 2 级:“音素”方法(语音映射)
Duolingo 的做法。 停止使用音量,改用音素的视觉等价——viseme。许多 TTS 提供商(Azure Speech SDK、AWS Polly)会返回 viseme 事件——描述特定时间点嘴形的整数。
Rive 状态机
不要只用一个 “嘴巴张开” 的混合,而是构建一个拥有约 12‑15 个离散嘴形的状态机,例如:
| Viseme | 描述 |
|---|---|
| Sil | 静音 / 空闲 |
| PP | 嘴唇紧闭 – P、B、M |
| FF | 上齿触唇 – F、V |
| TH | 舌头伸出 – TH |
| DD | 舌头靠后牙齿 – T、D、S |
| kk | 后部张开 – K、G |
| aa | 大张 – A |
| O | 圆形 – O |
| … | (依此类推) |
将这些映射到名为 viseme_id 的 Number Input。
代码逻辑
在前端(React Native、Flutter 等)监听 viseme 事件并推送到 Rive:
ttsService.on('visemeReceived', (visemeID) => {
// 1. 获取 Rive 输入
const mouthInput = riveArtboard.findInput('viseme_id');
// 2. 将 TTS 提供商的 ID 映射到你的 Rive ID
// (Azure 有 21 种形状,Rive 可能只需要 12 种)
const mappedID = mapAzureToRive(visemeID);
// 3. 更新状态
mouthInput.value = mappedID;
});
秘密:分层微行为
唇形同步只占幻觉的约 50 %。如果角色在说话时目不转睛,就会陷入“恐怖谷”。
解决方案: 在 Rive 中使用分层状态机,让多个时间线同时播放且不冲突。
- 第 1 层 – 嘴巴(由代码控制)。
- 第 2 层 – 眼睛(自包含循环)。在 Rive 内部的 “Randomize” 监听器每 2–5 秒自动触发一次眨眼或眼球转动。
- 第 3 层 – 情感(布尔输入,如
isBored、isHappy、isThinking)。
处理“暂停”(延迟)
AI 语音聊天中最大的用户体验杀手是 LLM 生成答案时的 2–3 秒沉默。角色不能卡死。
- 用户停止说话 → 应用设置
isThinking = true。 - Rive 动画 – 角色抬头、敲击手指,或(对讽刺人格)翻白眼。
- 音频流开始 → 将
isThinking = false;viseme 数据恢复流动。