> For the complete documentation index, see [llms.txt](https://docs.convai.com/api-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/lipsync-and-blendshape.md).

# 唇形同步与 Blendshape

### 工作原理

1. 在配置中启用唇形同步 —— 服务器会在生成 TTS 音频的同时开始生成 blendshape 帧。
2. 帧会以 10 帧为一批到达，并缓冲到 `client.blendshapeQueue`.
3. 你的渲染循环以 60 fps 从队列中读取帧，并与机器人说话时同步。
4. 当机器人停止说话时，队列会清空并发出对话结束信号。

***

### 启用唇形同步

```ts
const client = useConvaiClient({
  apiKey: '...',
  characterId: '...',
  enableLipsync: true,
  blendshapeConfig: {
    format: 'arkit', // 或 'mha'（MetaHuman 251，默认）
  },
});
```

格式（由服务器验证 — 传入未知字符串会被拒绝连接，并返回有效列表）：

| `格式`             | 帧维度 | 通道命名                  | 备注                                                                 |
| ---------------- | --- | --------------------- | ------------------------------------------------------------------ |
| `"mha"` （默认）     | 251 | `CTRL_expressions_*`  | Unreal MetaHuman Animation；MetaHuman-Lite 角色（103 个形变）会从同一流中使用其命名子集 |
| `"arkit"`        | 61  | ARKit 名称（`JawOpen`，…） | Apple ARKit 标准                                                     |
| `"cc4_extended"` | 170 | CC4 ExpressionPlus    | Reallusion Character Creator 4                                     |
| `"cc5_hd"`       | —   | —                     | 服务器接受，但目前输出 **没有帧**                                                |
| `"visemes"`      | 15  | OVR viseme 集          | `sil, PP, FF, TH, DD, kk, CH, SS, nn, RR, aa, E, ih, oh, ou`       |

***

### 在渲染循环中读取帧

访问 `client.blendshapeQueue` 从你的动画循环中访问。队列以 60 fps 进行时间索引。

```ts
// Three.js / requestAnimationFrame 示例
let lipsyncStartTime: number | null = null;

function animate() {
  requestAnimationFrame(animate);

  const queue = client.blendshapeQueue;

  if (queue.isBotSpeaking()) {
    // 当机器人开始说话时启动时钟
    if (lipsyncStartTime === null) {
      lipsyncStartTime = performance.now();
    }

    const elapsed = (performance.now() - lipsyncStartTime) / 1000;
    const result = queue.getFrameAtTime(elapsed);

    if (result) {
      applyBlendshapes(morphTargets, result.frame);
      queue.consumeFrames(result.frameIndex + 1);
    }
  } else {
    lipsyncStartTime = null;
  }

  renderer.render(scene, camera);
}
```

#### `getFrameAtTime(elapsedSeconds)`

返回最接近给定经过时间的帧（假设 60 fps），或者 `null` 如果队列为空。

```ts
const result = queue.getFrameAtTime(elapsed);
// result.frame：blendshape 值的 Float32Array
// result.frameIndex：在队列中的索引
```

#### `getFrameWithAlpha(index)`

与 `getFrame` 相同，但会自动应用淡入（前 10 帧）和淡出（最后 10 帧或被中断时）：

```ts
const frame = queue.getFrameWithAlpha(index);
// 在开始时平滑淡入，在结束时平滑淡出
```

***

### 将 blendshape 应用到 Three.js 网格

```ts
import { ARKIT_ORDER_61 } from '@convai/web-sdk/lipsync-helpers';

function applyBlendshapes(mesh: THREE.SkinnedMesh, frame: Float32Array) {
  const morphInfluences = mesh.morphTargetInfluences;
  if (!morphInfluences) return;

  ARKIT_ORDER_61.forEach((name, i) => {
    const morphIndex = mesh.morphTargetDictionary?.[name];
    if (morphIndex !== undefined) {
      morphInfluences[morphIndex] = frame[i];
    }
  });
}
```

`ARKIT_ORDER_61` 是一个有序数组，包含 61 个与帧索引相匹配的 ARKit blendshape 名称。

对于 MetaHuman（`"mha"` 格式），请改为导入 `METAHUMAN_ORDER_251` 。

***

### 自定义映射器

使用自定义映射器将传入的 blendshape 转换为与你角色的 morph target 名称匹配。

#### ARKit 名称映射器

```ts
import { createARKitNameMapper } from '@convai/web-sdk/lipsync-helpers';

const mapper = createARKitNameMapper({
  // SDK 名称 → 你角色的 morph target 名称
  'jawOpen': ['Jaw_Open', 'Mouth_Open'],
  'eyeBlinkLeft': ['Eye_Blink_L'],
  'eyeBlinkRight': ['Eye_Blink_R'],
  'mouthSmileLeft': ['Smile_L'],
  'mouthSmileRight': ['Smile_R'],
}, 'optimized'); // 'optimized' 只提取已映射的目标；省略则表示全部 61 个

const client = useConvaiClient({
  apiKey: '...',
  characterId: '...',
  enableLipsync: true,
  blendshapeConfig: {
    format: 'arkit',
    customMapper: mapper,
  },
});
```

映射器会在每一帧进入队列时应用。

#### 直接名称映射的角色（无需映射器）

如果你角色的 morph target 名称 **完全** 与流的通道名称一致，那么就完全跳过映射器，并按名称写入帧：

* `format: 'mha'` 通道命名为 `CTRL_expressions_jawOpen`, `CTRL_expressions_eyeBlinkL`，… — 与 MetaHuman 风格导出相匹配。
* `format: 'arkit'` 通道使用 ARKit 名称（`JawOpen`, `EyeBlinkLeft`，…）。

每个网格只构建一次索引表，然后以零每帧查找的方式应用帧：

```ts
import { METAHUMAN_ORDER_251 } from '@convai/web-sdk/lipsync-helpers';

// 一次性，在模型加载后：
const slots = new Int32Array(METAHUMAN_ORDER_251.length).fill(-1);
METAHUMAN_ORDER_251.forEach((name, i) => {
  const slot = mesh.morphTargetDictionary?.[name];
  if (slot !== undefined) slots[i] = slot;
});

// 每帧：
function applyFrame(frame: Float32Array) {
  const infl = mesh.morphTargetInfluences!;
  for (let i = 0; i < slots.length; i++) {
    if (slots[i] >= 0) infl[slots[i]] = frame[i];
  }
}
```

> 导出有时会把名称弄乱（例如缺少分隔符： `CTRL_expressionsjawOpen`）。在加载时规范化 `morphTargetDictionary` 的键，而不是调整每个使用者。

#### 连接后设置映射器

```ts
client.blendshapeQueue.setMapper((frame) => {
  // frame：原始 blendshape 值的 Float32Array
  const output = new Float32Array(52); // 你角色的 morph 数量
  // ... 重映射索引 ...
  return output;
});

// 移除映射器（透传）
client.blendshapeQueue.clearMapper();
```

***

### BlendshapeQueue API

`client.blendshapeQueue` 是实时队列实例。

#### 状态检查

```ts
queue.isBotSpeaking()         // 在机器人音频播放期间为 true
queue.isConversationActive()  // 从用户消息开始，到 turn-stats 到达前为 true
queue.isConversationEnded()   // 在回合结束且所有帧都被消费后为 true
queue.hasFrames()             // 当有帧在等待时为 true
queue.length                  // 队列中的帧数
```

#### 帧访问

```ts
queue.getFrameAtTime(elapsed) // { frame, frameIndex } | null — 基于时间的查找
queue.getFrame(index)         // Float32Array | null — 直接按索引访问
queue.getFrameWithAlpha(index) // Float32Array | null — 自动应用淡入/淡出
queue.getFrames()             // Float32Array[] — 所有帧
```

#### 消费

```ts
queue.consumeFrames(count)    // 从队列前端移除 N 帧
queue.getFramesConsumed()     // 本回合已消费的总帧数
```

#### 回合统计

```ts
queue.getTurnStats()
// { fps, total_audio_bytes, total_audio_duration_ms, total_blendshapes, total_turn_duration_ms }

queue.getTimeLeftMs()         // 队列清空前的预计毫秒数
queue.isAllFramesConsumed()   // 当 framesConsumed >= total_blendshapes 时为 true
```

#### 中断

```ts
queue.consumeNormalizationSignal()
// 当机器人被打断时返回 true（仅一次）—— 用于将 morph 平滑回 0
```

#### 调试

```ts
console.log(queue.getDebugInfo());
// { frameCount, conversationActive, botSpeaking, conversationEnded,
//   interrupted, framesConsumed, turnStats, timeLeftMs }
```

***

### 让唇形同步更自然

在生产角色上验证过的模式——每一种都针对你否则会看到的特定瑕疵：

#### 让流淡入淡出

原始帧在第一帧时会以满强度冲到脸上，而在帧停止时会冻结最后一个 viseme。将每个由流驱动的值乘以一个平滑包络：在语音开始时约 0.25 秒内渐强，在最后一帧后约 0.5 秒内渐弱（保留最后一帧并将其淡到 0——不要在半途中丢弃）。

```ts
// 每个渲染帧：
const target = isSpeaking ? 1 : 0;
const tau = target > env ? 0.09 : 0.18; // 快速起音，更柔和的释音
env += (target - env) * (1 - Math.exp(-delta / tau));
// ...然后应用：infl[slot] = frame[i] * env;
```

(`getFrameWithAlpha` 如果你直接消费队列，这会自动完成；如果你自己缓冲/应用帧，请应用自己的包络。）

#### 让程序系统拥有自己的通道

如果你运行程序化眨眼或随镜头跟随的视线，流必须 **绝不能写入眼睛通道** —— 两个使用不同定时器的写入者会彼此竞争并导致闪烁（眨眼甚至不会明显闭合）。保留一份流应保持不动的通道索引跳过掩码，并让你的眨眼/视线代码完全拥有它们。

#### 语音结束检测

帧可能会在任何显式结束事件之前停止到达。将大约 300 毫秒内没有新帧视为语音结束并开始淡出——这样官方结束信号即使稍后到达，也不会出现可见冻结。

#### 按通道增益

流是为参考脸型调校的；你角色的形状可能会显得过强。按通道的增益表可以精准修正这一点——例如在一个 MetaHuman 风格的绑定上，我们会设置 `jawOpen × 0.7` 以及横向的下颌/嘴部位移 × 0（它们会显得歪斜），其余全部保持 1。

#### 嘴部对称性

如果流对 L/R 嘴部对称通道的驱动不均衡（我们测到左右最多可差 1.5×），在应用前对每一对侧向通道取平均（`…L`/`…R`）——张开的嘴看起来会对称得多。

#### 在动画混合器之后应用

如果身体动画片段也会影响头部，请在 **之后** 混合器每个渲染帧更新后，重新应用当前唇形同步帧，这样说话始终在脸部上占优。

***

### 缓冲调优

该 `frames_buffer_duration` 选项控制服务器在连同音频一起释放之前累积多少秒的 blendshape。更高的值会提升唇形同步精度，但会增加延迟。

```ts
blendshapeConfig: {
  format: 'arkit',
  frames_buffer_duration: 0.2, // 200 毫秒缓冲（默认：0.1）
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/lipsync-and-blendshape.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
