> 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 驱动面部动画

与角色语音一起流式传输 blendshape 帧，并将其应用到 3D 面部的 morph targets 上，使其嘴部随语音同步移动。

开启唇形同步，让 Convai 随角色音频流式发送面部 blendshape 帧，然后将这些帧播放到 3D 模型的 morph target 上。使用本页来在 three.js 或其他 WebGL 引擎中渲染你自己的角色。最终，角色的嘴会随着声音同步移动，并在停止说话时回到中性状态。

### 前提条件

* 已安装 Convai Web SDK，并且有一个可连接的客户端。参见 [Web SDK 快速入门](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/quickstart.md).
* 一个面部由 morph target 驱动的 3D 模型，已加载到你的渲染器中。
* 模型的 morph target 名称，以便你将它们匹配到某种流格式。

### 启用唇形同步

设置 `enableLipsync` 并在 `blendshapeConfig`中选择流格式。同样的选项也适用于 `new ConvaiClient(...)` 来自 `@convai/web-sdk/core` 和 `useConvaiClient(...)` 来自 `@convai/web-sdk/react`.

```ts
import { ConvaiClient } from '@convai/web-sdk/core';

const client = new ConvaiClient({
  apiKey: 'YOUR_API_KEY',
  characterId: 'YOUR_CHARACTER_ID',
  enableLipsync: true,
  blendshapeConfig: {
    format: 'mha', // 或 'arkit'
  },
});
```

当 `enableLipsync` 为 `true`时，SDK 会按每 10 帧一批向 Convai 请求 blendshape，并将它们缓冲到 `client.blendshapeQueue`中。当它为 `false` 或省略时，不会发送任何 blendshape 帧。

这里使用的 `format` 决定每一帧的长度和顺序：

| `format`     | 帧长度 | 通道名称                                                  |
| ------------ | --- | ----------------------------------------------------- |
| `"mha"` （默认） | 251 | MetaHuman 控制项，例如 `CTRL_expressions_jawOpen`           |
| `"arkit"`    | 61  | 52 个 ARKit blendshape，例如 `JawOpen`，然后是三个头部旋转值和六个眼部旋转值 |

两种格式的完整通道顺序见 [Blendshape 通道顺序参考](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/vanilla-typescript/mappings-reference.md).

### 调整传送

`blendshapeConfig` 提供三种传送选项。除非你有经过测量的理由，否则请保持默认值。

| 选项                       | 默认值                             | 效果                                                                 |
| ------------------------ | ------------------------------- | ------------------------------------------------------------------ |
| `deliver_chunks_ahead`   | `true` 当 `enableLipsync` 处于开启状态 | Convai 会在音频之前发送带索引的块，因此队列会在播放开始前保留缓冲。将 `false` 设为此值，以回退到旧版按节奏传送路径。 |
| `output_fps`             | `60`                            | 提前传送块的帧率。当 `deliver_chunks_ahead` 为 `false`.                       |
| `frames_buffer_duration` | `0.1`                           | Convai 在与音频一起释放前累计的 blendshape 秒数。                                 |

队列会以块元数据中携带的帧率播放帧；如果块不携带该信息，则以每秒 60 帧播放。可通过以下方式读取： `client.blendshapeQueue.getPlaybackFps()`.

### 从队列播放帧

在 `requestAnimationFrame`上运行一个循环。每一帧，推进时间累加器，取出到期的帧，将最新的一帧写入面部，并将它们从队列中移除。

```ts
let lastTime = 0;
let carryMs = 0;

function tick(now: number) {
  const queue = client.blendshapeQueue;

  // 中断会停止音频：立即将面部恢复为中性。
  if (queue.consumeNormalizationSignal()) {
    applyFrame(new Float32Array(frameLength));
    carryMs = 0;
  }

  const playing =
    queue.isBotSpeaking() || (queue.hasReceivedEndSignal() && queue.hasFrames());

  if (playing && queue.hasFrames()) {
    carryMs += lastTime ? now - lastTime : 0;
    const frameMs = 1000 / queue.getPlaybackFps();
    const due = Math.floor(carryMs / frameMs);
    if (due > 0) {
      const count = Math.min(due, queue.length);
      // 先读取再消费：淡出取决于已播放了多少帧。
      const frame = queue.getFrameWithAlpha(count - 1);
      if (frame) applyFrame(frame);
      queue.consumeFrames(count);
      carryMs -= due * frameMs;
    }
  } else {
    carryMs = 0;
  }

  // 这一轮已经结束，所有预期帧都已播放。
  if (queue.isConversationEnded()) {
    queue.reset();
  }

  lastTime = now;
  requestAnimationFrame(tick);
}

requestAnimationFrame(tick);
```

`frameLength` 是 251，对应 `mha` 以及 61，对应 `arkit`. `getFrameWithAlpha` 将一轮中的前 10 帧淡入、最后 10 帧淡出，因此嘴巴既不会突然张开，也不会在半个形状时冻结。 `getFrame` 返回相同的帧，但没有淡化效果。

{% hint style="warning" %}
不要将 `getFrameAtTime` 与 `consumeFrames`. `getFrameAtTime` 按经过时间从队列前端索引，而 `consumeFrames` 会将前端向前推进，因此两者一起使用会让播放推进两次，嘴巴会抢在声音前面。
{% endhint %}

每个 `BlendshapeQueue` 方法都列在 [ConvaiClient 参考文档](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/vanilla-typescript/convaiclient-core-api.md).

### 将帧应用到 morph target

当模型的 morph target 使用流中的通道名称时，在模型加载后先构建一次槽位表，然后通过它写入每一帧。这不需要映射器。

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

function bindFace(mesh: Mesh) {
  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;
  });

  return (frame: Float32Array) => {
    const influences = mesh.morphTargetInfluences;
    if (!influences) return;
    for (let i = 0; i < slots.length; i++) {
      if (slots[i] >= 0) influences[slots[i]] = frame[i] ?? 0;
    }
  };
}

const applyFrame = bindFace(faceMesh);
```

对于 `arkit`，使用 `ARKIT_ORDER_61` 替代 `METAHUMAN_ORDER_251`。两者都导出自 `@convai/web-sdk/lipsync-helpers`.

对于 `mha` 角色，也要应用 MetaHuman rig 耦合，否则每次下颌张开时嘴唇都会分开。 `createLipsyncProcessor('mha')` 将网格绑定、修复已知的名称混淆，并在一次调用中应用耦合、增益和淡入淡出包络。参见 [应用 MetaHuman 限制驱动器](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/lipsync-and-blendshape/metahuman-limit-drivers-required-for-mha.md).

### 使用自定义映射器重新映射通道

当 morph target 使用不同的名称时，传入一个映射器。SDK 会在每一帧进入队列时对其运行。

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

const mapper = createARKitNameMapper({
  jawOpen: ['Jaw_Open'],
  eyeBlinkLeft: ['Eye_Blink_L'],
  eyeBlinkRight: ['Eye_Blink_R'],
  mouthSmileLeft: ['Mouth_Smile'],
  mouthSmileRight: ['Mouth_Smile'], // 与 mouthSmileLeft 取平均
});

const client = new ConvaiClient({
  apiKey: 'YOUR_API_KEY',
  characterId: 'YOUR_CHARACTER_ID',
  enableLipsync: true,
  blendshapeConfig: { format: 'arkit', customMapper: mapper },
});
```

`createMetaHumanNameMapper` 对 `mha`也执行同样操作，键为完整的 `CTRL_expressions_*` 名称或简写（`jawOpen`）。不在该格式中的源名称会被跳过，并给出控制台警告。当多个源映射到一个目标时，它们的值会取平均。仅输出你映射的目标。

第二个参数选择输出形状。两者都会为每一帧提供自己的值，并且都能保留淡入效果。 `getFrameWithAlpha`:

| 输出              | 队列中的每一帧                                  | 如何读取                                                                              |
| --------------- | ---------------------------------------- | --------------------------------------------------------------------------------- |
| `'object'` （默认） | 一个 `Float32Array` 目标值数组，按目标顺序排列，并附带名称    | `getBlendshapeObject(frame)` 用于获取按名称到值映射的对象                                       |
| `'optimized'`   | `{ names, values, nameToIndex, length }` | `frame.values[frame.nameToIndex.get('Jaw_Open')]`，或者 `getBlendshapeObject(frame)` |

要在连接后更改映射器，请调用 `client.blendshapeQueue.setMapper(fn)`. `clearMapper()` 会恢复为原始帧，而 `hasMapper()` 会报告是否已设置。

### 验证唇形同步

连接、发送一条消息，并在角色回答时观察面部。

{% hint style="success" %}
当角色开始发声时，嘴巴会开始移动，跟随其音节，并在声音停止后的不到一秒内回到中性状态。
{% endhint %}

如果面部从不动，请确认 `enableLipsync` 为 `true` 并且 `client.blendshapeQueue.length` 在角色说话时会增加。如果帧已到达但面部仍静止，说明 morph target 名称与格式不匹配：请记录 `mesh.morphTargetDictionary` 并将其与通道顺序参考进行比较。

### 后续步骤

{% content-ref url="/pages/c52240c5f5c86a00159cb30d9d04c5b6d5ea3c4a" %}
[应用 MetaHuman 限制驱动器](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/lipsync-and-blendshape/metahuman-limit-drivers-required-for-mha.md)
{% endcontent-ref %}

{% content-ref url="/pages/22ae6f4a7ad0727295a752b42c53546668e69c5a" %}
[在 React 中添加口型同步](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/react/real-time-lipsync.md)
{% endcontent-ref %}

{% content-ref url="/pages/e842433c402c6a1f2b7938d74b7fd4adf7da7152" %}
[在原生 TypeScript 中添加口型同步](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/vanilla-typescript/real-time-lipsync.md)
{% endcontent-ref %}

{% content-ref url="/pages/4478f4a3cec9e37c9839b186fe7a71671f76fa3e" %}
[Blendshape 声道顺序参考](/api-docs/zh/cha-jian-yu-ji-cheng/web-plugins/convai-web-sdk/vanilla-typescript/mappings-reference.md)
{% endcontent-ref %}


---

# 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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
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=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
