> 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/convai-unity-sdk/scripting-reference/transcript-api.md).

# 转录 API

使用转录外观接口从 Unity 脚本查询转录轮次、订阅实时变更、读取字幕并导出会话历史。

`ConvaiTranscripts` 是 Convai Unity SDK 的规范转录门面：一个实时、驻留内存的房间内所有玩家和角色回合时间线，支持拉取式查询、推送式变更事件、实时字幕以及会话导出辅助工具。在编写自定义聊天 UI、转录导出或回合级对话逻辑时，请使用此页面。通过以下方式访问该门面： `ConvaiManager.ActiveManager.Transcripts`.

***

### 推送 vs. 拉取

|          | `ConvaiCharacter.OnTranscriptReceived`                    | 事件转发组件（`ConvaiTranscriptEventRelay`, `ConvaiEvents`)               | `ConvaiTranscripts`                                      |
| -------- | --------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------- |
| **传递**   | 推送——普通 C# 事件在每次更新时触发，针对某个角色自己的语音台词                        | 推送——检查器 `UnityEvent`s 或 C# 事件在每次更新时触发                              | 拉取（`CurrentTimeline`, `GetTurns`）和推送（`已变更`, `Subscribe`) |
| **历史**   | 仅当前更新，作用范围限于该角色                                           | 仅当前更新                                                              | 完整历史：活动回合、已提交回合和实时字幕                                     |
| **使用场景** | 按角色范围的推送响应——凝视、手势、单角色字幕                                   | 字幕渲染、按角色的动画触发                                                      | 自定义聊天 UI、会话后导出、回合级评估逻辑                                   |
| **访问**   | `character.OnTranscriptReceived` 在一个 `ConvaiCharacter` 引用 | `ConvaiTranscriptEventRelay`, `ConvaiManager.ActiveManager.Events` | `ConvaiManager.ActiveManager.Transcripts`                |

***

### `ConvaiCharacter.OnTranscriptReceived`

`OnTranscriptReceived` 是在以下对象上声明的普通 C# 事件： `ConvaiCharacter`: `event Action<string, bool> OnTranscriptReceived`。它是以下内容的按角色范围对应项： `ConvaiTranscripts` 下面的门面——当你已经持有某个其实例的引用时就使用它 `ConvaiCharacter` 并且希望直接接收该角色自己的语音台词推送通知，而无需订阅整个房间级门面或添加事件转发组件。

```csharp
character.OnTranscriptReceived += HandleTranscript;

private void HandleTranscript(string text, bool isFinal)
{
    if (!isFinal) return; // 丢弃中间传递，这样句子不会被追加两次
    _log.text += $"\n{text}";
}
```

第一个参数是转录文本。第二个， `isFinal`，来自消息自身的生命周期——已稳定的台词会报告 `是`；仍在流式传输的台词会报告 `否`.

文本按句子到达，而不是按合成块到达。一个在稳定前就开始流式传输的句子会被传递 **两次**：一次带有 `isFinal` `否` 当它仍在进行中时，另一次带有 `isFinal` `是` 在其稳定后 `isFinal` 会重复该句子。

`OnTranscriptReceived` 仅会针对该角色自己的语音输出触发——安静的、未朗读的机器人文本会被过滤掉，传入消息会先按参与者 ID 与该角色匹配，若不匹配则回退到角色 ID。

使用 `ConvaiCharacter.OnTranscriptReceived` 用于单角色推送响应。使用 `ConvaiTranscripts` 下面的内容，当你需要完整房间历史、多角色或多玩家，或基于拉取的查询时。

***

### `ConvaiTranscripts` 门面

```csharp
ConvaiManager manager = ConvaiManager.ActiveManager;
if (manager == null || !manager.TryGetTranscripts(out ConvaiTranscripts transcripts))
    return;
```

`ConvaiManager.ActiveManager.Transcripts` 会抛出 `InvalidOperationException` 如果 SDK 尚未完成启动引导。使用 `manager.TryGetTranscripts(out ConvaiTranscripts transcripts)` 当调用方可能在初始化完成前运行时，例如 `OnEnable`.

#### 属性

| 成员                      | 类型                          | 说明                                |
| ----------------------- | --------------------------- | --------------------------------- |
| `CurrentTimeline`       | `TranscriptTimeline`        | 当前转录时间线。在底层引擎快照发生变化之前，返回同一个实例。    |
| `CurrentCaptions`       | `TranscriptCaptionSnapshot` | 用于与语音对齐字幕的当前实时字幕快照。               |
| `IsPresentationEnabled` | `布尔值`                       | 随附的展示组件是否应渲染转录更新。只读；规范历史记录仍会继续记录。 |

#### 事件

| 事件          | 参数                          | 当                            |
| ----------- | --------------------------- | ---------------------------- |
| `已变更`       | `TranscriptChangeBatch`     | 一个或多个回合被添加、更新、提交、中断、更正或移除    |
| `回合已更新`     | `TranscriptTurn`            | 某个回合收到新文本或非终态状态变更            |
| `回合已提交`     | `TranscriptTurn`            | 某个回合转换为 `已提交` 或 `被打断`        |
| `回合已更正`     | `TranscriptTurn`            | 先前已提交回合的文本被更正                |
| `回合已移除`     | `字符串` （回合 ID）               | 某个回合从时间线中移除                  |
| `字幕已变更`     | `TranscriptCaptionSnapshot` | 实时字幕快照发生变化                   |
| `展示启用状态已变更` | `布尔值`                       | `IsPresentationEnabled` 发生变化 |

#### 方法

| 方法                                                                                                           | 返回值                             | 说明                                                                                        |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------- | ----------------------------------------------------------------------------------------- |
| `GetTurns(TranscriptQuery query = null)`                                                                     | `IReadOnlyList<TranscriptTurn>` | 返回所有匹配可选查询的回合。传入 `null` 可获取每个回合。                                                          |
| `GetTurn(string turnId)`                                                                                     | `TranscriptTurn`                | 按 ID 检索特定回合。返回 `null` 如果未找到则返回。                                                           |
| `GetLatestTurn(TranscriptParticipantRef participant)`                                                        | `TranscriptTurn`                | 返回给定参与者的最新回合。返回 `null` 如果没有则返回。                                                           |
| `Subscribe(Action<TranscriptChange> callback, TranscriptSubscriptionOptions options = null)`                 | `IDisposable`                   | 为匹配的回合变更注册回调。释放返回值即可取消订阅。                                                                 |
| `SubscribeCommitted(Action<TranscriptChange> callback, TranscriptSubscriptionOptions options = null)`        | `IDisposable`                   | 快捷方式，对应 `Subscribe` 与 `IncludeActive = false` 和 `IncludeTerminal = true` ——仅包含已提交和已中断的回合。 |
| `SubscribeCaptions(Action<TranscriptCaption> callback, TranscriptCaptionSubscriptionOptions options = null)` | `IDisposable`                   | 为实时字幕更新注册回调。                                                                              |
| `Clear()`                                                                                                    | `void`                          | 清除规范转录历史。                                                                                 |
| `Export(TranscriptExportFormat format)`                                                                      | `字符串`                           | 将每个已提交回合序列化为纯文本、Markdown 或 JSON。                                                          |
| `Dispose()`                                                                                                  | `void`                          | 取消订阅内部引擎事件。在所属组件销毁时调用。                                                                    |

```csharp
transcripts.Changed += OnTranscriptChanged;

private void OnTranscriptChanged(TranscriptChangeBatch batch)
{
    foreach (TranscriptChange change in batch.Changes)
    {
        if (change.Kind == TranscriptChangeKind.Removed || change.Turn == null) continue;
        Debug.Log($"[{change.Turn.Speaker.DisplayName}] {change.Turn.DisplayText}");
    }
}
```

***

### `TranscriptTimeline`

| 属性           | 类型                                            | 说明                                  |
| ------------ | --------------------------------------------- | ----------------------------------- |
| `Cursor`     | `long`                                        | 单调递增的值，每当时间线更新时都会变化                 |
| `活动回合`       | `IReadOnlyList<TranscriptTurn>`               | 尚未提交的回合（`倾听中`, `传输中`，或 `Stable`)    |
| `已提交回合`      | `IReadOnlyList<TranscriptTurn>`               | 处于终态的回合（`已提交` 或 `被打断`)              |
| `按 ID 索引的回合` | `IReadOnlyDictionary<string, TranscriptTurn>` | 所有回合按以下字段索引： `TranscriptTurn.Id`    |
| `回合`         | `IReadOnlyList<TranscriptTurn>`               | `活动回合` 和 `已提交回合` 合并并按以下字段排序： `房间序列` |

`TranscriptTimeline.Empty` 是一个静态、可复用的空实例——在会话连接前的安全默认值。

***

### `TranscriptTurn`

| 属性               | 类型                                 | 说明                             |
| ---------------- | ---------------------------------- | ------------------------------ |
| `ID`             | `字符串`                              | 此回合的唯一标识符                      |
| `MessageId`      | `字符串`                              | 与此回合关联的消息标识符                   |
| `ResponseId`     | `字符串`                              | 与此回合关联的响应标识符（如适用）              |
| `房间序列`           | `long`                             | 在房间内单调递增的序列号                   |
| `修订`             | `整数`                               | 每当回合内容或状态变化时递增                 |
| `发言者`            | `TranscriptSpeaker`                | 是谁生成了此回合                       |
| `状态`             | `TranscriptTurnState`              | 此回合当前的生命周期状态                   |
| `主要文本来源`         | `TranscriptTextSource`             | 支撑此回合显示文本的主要文本来源               |
| `稳定文本`           | `字符串`                              | 已定稿文本，后续更新不会再变化                |
| `中间文本`           | `字符串`                              | 来自当前流式片段的进行中文本                 |
| `显示文本`           | `字符串`                              | 要为此回合渲染的文本——结合 `稳定文本` 和 `中间文本` |
| `开始时间（UTC）`      | `DateTime`                         | 回合开始的 UTC 时间                   |
| `最后更新时间（UTC）`    | `DateTime`                         | 最近一次更新的 UTC 时间                 |
| `提交时间（UTC）`      | `DateTime?`                        | 回合被提交的 UTC 时间； `null` 在活动期间    |
| `WasInterrupted` | `布尔值`                              | `是` 当回合因中断而结束时                 |
| `片段`             | `IReadOnlyList<TranscriptSegment>` | 构成此回合的各个转录片段                   |
| `有文本`            | `布尔值`                              | `是` 当 `显示文本` 非空                |
| `是否已提交`          | `布尔值`                              | `是` 当 `状态` 是 `已提交` 或 `被打断`     |

{% hint style="info" %}
使用 `显示文本` 用于实时渲染。它结合了 `稳定文本` 和 `中间文本`，因此无论 `状态`.
{% endhint %}

#### `TranscriptTurnState` 枚举

| 值            | 说明                                       |
| ------------ | ---------------------------------------- |
| `倾听中` (0)    | 回合处于开启状态，正在等待语音或文本输入；尚未捕获文本              |
| `传输中` (1)    | 回合正在主动接收文本； `中间文本` 正在更新                  |
| `Stable` (2) | 流式传输已暂停；文本已稳定，但回合尚未提交                    |
| `已提交` (4)    | 回合已完全提交； `稳定文本` 为最终                      |
| `被打断` (5)    | 回合因被中断而结束（`WasInterrupted` 是 `是`)        |
| `已丢弃` (6)    | 回合在没有文本的情况下关闭，并被排除在两者之外 `活动回合` 和 `已提交回合` |

***

### `TranscriptSegment`

| 属性          | 类型                     | 说明                          |
| ----------- | ---------------------- | --------------------------- |
| `ID`        | `字符串`                  | 此片段的唯一标识符                   |
| `TurnId`    | `字符串`                  | 父项的 ID `TranscriptTurn`     |
| `发言者`       | `TranscriptSpeaker`    | 是谁生成了此片段                    |
| `稳定文本`      | `字符串`                  | 此片段的已定稿文本                   |
| `中间文本`      | `字符串`                  | 此片段的进行中文本                   |
| `显示文本`      | `字符串`                  | 要为此片段渲染的文本                  |
| `状态`        | `TranscriptTurnState`  | 此片段的生命周期状态                  |
| `来源`        | `TranscriptTextSource` | 此片段文本的来源                    |
| `开始时间（UTC）` | `DateTime`             | 此片段开始的 UTC 时间               |
| `更新时间（UTC）` | `DateTime`             | 最近一次更新的 UTC 时间              |
| `停止时间（UTC）` | `DateTime?`            | 此片段停止的 UTC 时间； `null` 在活动期间 |

#### `TranscriptTextSource` 枚举

| 值               | 说明                   |
| --------------- | -------------------- |
| `未知` (0)        | 无法确定来源               |
| `中间 ASR` (1)    | 进行中的语音转文本识别          |
| `ASR 最终` (2)    | 已定稿的语音转文本识别          |
| `处理后最终` (3)     | 经玩家端处理后的已定稿文本        |
| `输入文本` (4)      | 由玩家键入而非说出的文本         |
| `BotOutput` (5) | 已定稿的角色回复文本           |
| `机器人预览` (6)     | 进行中的角色回复预览（LLM 流式传输） |
| `旧版机器人转录` (7)   | 来自旧版流水线的角色转录文本       |

***

### `TranscriptSpeaker`

| 属性              | 类型                      | 说明                        |
| --------------- | ----------------------- | ------------------------- |
| `类型`            | `TranscriptSpeakerType` | 此发言者是否为 `玩家`, `角色`，或 `系统` |
| `ID`            | `字符串`                   | 此发言者的角色 ID 或玩家 ID         |
| `DisplayName`   | `字符串`                   | 人类可读名称                    |
| `ParticipantId` | `字符串`                   | 房间级参与者标识符                 |

#### `TranscriptSpeakerType` 枚举

| 值        | 说明                 |
| -------- | ------------------ |
| `玩家` (0) | 人类玩家参与者            |
| `角色` (1) | AI 角色参与者           |
| `系统` (2) | 系统来源的发言者，不绑定到玩家或角色 |

***

### `TranscriptChange` 和 `TranscriptChangeBatch`

`TranscriptChange`:

| 属性       | 类型                     | 说明                            |
| -------- | ---------------------- | ----------------------------- |
| `类型`     | `TranscriptChangeKind` | 此实例表示的变更类型                    |
| `回合`     | `TranscriptTurn`       | 受影响的回合； `null` 当 `类型` 是 `已移除` |
| `TurnId` | `字符串`                  | 受影响回合的 ID                     |

`TranscriptChangeBatch`:

| 属性      | 类型                                | 说明                      |
| ------- | --------------------------------- | ----------------------- |
| `时间线`   | `TranscriptTimeline`              | 此批变更后的完整时间线             |
| `变更`    | `IReadOnlyList<TranscriptChange>` | 此批中包含的每一项变更             |
| `已变更回合` | `IReadOnlyList<TranscriptTurn>`   | 便捷访问器：每个非空 `回合` 来自 `变更` |

#### `TranscriptChangeKind` 枚举

| 值         | 说明                 |
| --------- | ------------------ |
| `已添加` (0) | 创建了一个新回合           |
| `已更新` (1) | 现有回合收到了新文本或非终态状态变更 |
| `已提交` (2) | 某个回合转换为 `已提交`      |
| `被打断` (3) | 某个回合转换为 `被打断`      |
| `已更正` (4) | 先前已提交回合的文本被更正      |
| `已移除` (5) | 某个回合从时间线中移除        |

***

### `TranscriptQuery` ——筛选 `GetTurns` 结果

| 字段                    | 类型                           | 默认          | 说明                  |
| --------------------- | ---------------------------- | ----------- | ------------------- |
| `参与者类型`               | `TranscriptParticipantKind?` | `null` （全部） | 筛选为 `玩家` 或 `角色` 仅回合 |
| `PlayerOrCharacterId` | `字符串`                        | `null` （全部） | 筛选到特定玩家或角色 ID       |
| `ParticipantId`       | `字符串`                        | `null` （全部） | 筛选到特定房间参与者 ID       |
| `包含活动回合`              | `布尔值`                        | `是`         | 包含仍处于活动状态（尚未提交）的回合  |
| `包含已提交回合`             | `布尔值`                        | `是`         | 包含已提交或已中断的回合        |

```csharp
var query = new TranscriptQuery
{
    ParticipantKind      = TranscriptParticipantKind.Character,
    PlayerOrCharacterId  = "char_instructor_01",
    IncludeActiveTurns   = false,
    IncludeCommittedTurns = true
};

IReadOnlyList<TranscriptTurn> turns = transcripts.GetTurns(query);
```

`TranscriptQuery` 使用以下 `包含活动回合`/`包含已提交回合` 字段名称。 `TranscriptSubscriptionOptions`，由以下内容使用： `Subscribe`，使用 `IncludeActive`/`IncludeTerminal` 改为此项。

#### `TranscriptParticipantKind` 枚举

| 值        | 说明       |
| -------- | -------- |
| `玩家` (0) | 人类玩家参与者  |
| `角色` (1) | AI 角色参与者 |

#### `TranscriptParticipantRef` 结构体

用作 `参与者` 参数传入 `GetLatestTurn`.

| 属性                    | 类型                          | 说明                                     |
| --------------------- | --------------------------- | -------------------------------------- |
| `类型`                  | `TranscriptParticipantKind` | 此参与者是否为 `玩家` 或 `角色`                    |
| `PlayerOrCharacterId` | `字符串`                       | 此参与者的角色 ID 或玩家 ID                      |
| `DisplayName`         | `字符串`                       | 人类可读名称                                 |
| `ParticipantId`       | `字符串`                       | 房间级参与者标识符                              |
| `IsEmpty`             | `布尔值`                       | `是` 当 `PlayerOrCharacterId` 为空或仅包含空白字符 |

使用以下方式构造： `new TranscriptParticipantRef(TranscriptParticipantKind kind, string playerOrCharacterId, string displayName, string participantId = null)`。实现 `IEquatable<TranscriptParticipantRef>` 以及 `==`/`!=` 运算符。

***

### `TranscriptSubscriptionOptions` ——筛选 `Subscribe` 回调，恢复了首轮 LipSync

| 字段                | 类型                       | 默认          | 说明                                                       |
| ----------------- | ------------------------ | ----------- | -------------------------------------------------------- |
| `重放现有内容`          | `布尔值`                    | `否`         | 当 `是`, `Subscribe` 会立即对已在其中的每个匹配回合调用回调 `CurrentTimeline` |
| `IncludeActive`   | `布尔值`                    | `是`         | 包含尚未提交的回合                                                |
| `IncludeTerminal` | `布尔值`                    | `是`         | 包含已提交或已中断的回合                                             |
| `SpeakerType`     | `TranscriptSpeakerType?` | `null` （全部） | 筛选到特定发言者类型                                               |
| `SpeakerId`       | `字符串`                    | `null` （全部） | 筛选到特定发言者 ID                                              |
| `ParticipantId`   | `字符串`                    | `null` （全部） | 筛选到特定房间参与者 ID                                            |

***

### 实时字幕

字幕是语音对齐文本的独立、短暂投影——它与持久聊天历史分开存放，因此短暂的 TTS 预览文本绝不会被当作规范对话历史。

#### `TranscriptCaption`

| 属性               | 类型                       | 说明                         |
| ---------------- | ------------------------ | -------------------------- |
| `TurnId`         | `字符串`                    | 此字幕所对齐的转录回合 ID             |
| `发言者`            | `TranscriptSpeaker`      | 是谁生成了此字幕                   |
| `文本`             | `字符串`                    | 字幕文本                       |
| `状态`             | `TranscriptCaptionState` | 当前字幕状态                     |
| `更新时间（UTC）`      | `DateTime`               | 最近一次更新的 UTC 时间             |
| `有文本`            | `布尔值`                    | `是` 当 `文本` 非空              |
| `IsFinal`        | `布尔值`                    | `是` 当 `状态` 是 `已完成` 或 `被打断` |
| `WasInterrupted` | `布尔值`                    | `是` 当 `状态` 是 `被打断`         |

#### `TranscriptCaptionState` 枚举

| 值            | 说明                |
| ------------ | ----------------- |
| `传输中` (0)    | 字幕文本正在积极更新        |
| `Stable` (1) | 字幕文本已暂停更新，但尚未最终确定 |
| `已完成` (2)    | 字幕正常结束            |
| `被打断` (3)    | 字幕因回合中断而结束        |

#### `TranscriptCaptionSnapshot`

| 属性       | 类型                                 | 说明                 |
| -------- | ---------------------------------- | ------------------ |
| `Cursor` | `long`                             | 单调递增的值，每当字幕更新时都会变化 |
| `字幕`     | `IReadOnlyList<TranscriptCaption>` | 当前的实时字幕集合          |

`TranscriptCaptionSnapshot.Empty` 是一个静态、可复用的空实例。

#### `TranscriptCaptionSubscriptionOptions`

| 字段              | 类型                       | 默认          | 说明                                           |
| --------------- | ------------------------ | ----------- | -------------------------------------------- |
| `重放最新`          | `布尔值`                    | `是`         | 当 `是`, `SubscribeCaptions` 会立即对每个当前匹配的字幕调用回调 |
| `包含流式传输`        | `布尔值`                    | `是`         | 包含仍在更新的字幕                                    |
| `包含最终字幕`        | `布尔值`                    | `是`         | 包含已完成或已中断的字幕                                 |
| `SpeakerType`   | `TranscriptSpeakerType?` | `null` （全部） | 筛选到特定发言者类型                                   |
| `SpeakerId`     | `字符串`                    | `null` （全部） | 筛选到特定发言者 ID                                  |
| `ParticipantId` | `字符串`                    | `null` （全部） | 筛选到特定房间参与者 ID                                |

***

### 导出转录

`Export(TranscriptExportFormat format)` 将其中的每个回合序列化为 `CurrentTimeline.CommittedTurns`，按以下字段排序： `房间序列`，并合并为单个字符串。发言者标签将回退到 `Speaker.Type` 当 `Speaker.DisplayName` 为空。

#### `TranscriptExportFormat` 枚举

| 值               | 说明                                   |
| --------------- | ------------------------------------ |
| `PlainText` (0) | 每个回合一行： `发言者：文本`                     |
| `Markdown` (1)  | 每个回合一行： `**说话者：** 文本`，轮次之间空一行        |
| `JSON` (2)      | 已提交对象的缩进 JSON 数组 `TranscriptTurn` 对象 |

***

### 使用示例

#### 示例 1 — 会话结束后转录导出

一个医疗培训模拟在会话结束后将完整会话转录导出为 JSON，供主管审阅。

{% code title="TranscriptExporter.cs" %}

```csharp
using Convai.Domain.Models;
using Convai.Runtime.Components;
using Convai.Runtime.Facades;
using System.IO;
using UnityEngine;

public class TranscriptExporter : MonoBehaviour
{
    public void ExportToJson(string outputPath)
    {
        ConvaiManager manager = ConvaiManager.ActiveManager;
        if (manager == null || !manager.TryGetTranscripts(out ConvaiTranscripts transcripts))
            return;

        string json = transcripts.Export(TranscriptExportFormat.Json);
        File.WriteAllText(outputPath, json);
        Debug.Log($"转录已保存到 {outputPath}");
    }
}
```

{% endcode %}

#### 示例 2 — 在提交时追加的响应式聊天日志

一个企业入职模拟会构建可滚动的聊天历史，仅在轮次被提交时追加消息——避免中间更新造成的闪烁。

{% code title="CompletedTurnChatLog.cs" %}

```csharp
using Convai.Domain.Models;
using Convai.Runtime.Components;
using Convai.Runtime.Facades;
using System;
using TMPro;
using UnityEngine;

public class CompletedTurnChatLog : MonoBehaviour
{
    [SerializeField] private TMP_Text _log;

    private IDisposable _subscription;

    private void OnEnable()
    {
        ConvaiManager manager = ConvaiManager.ActiveManager;
        if (manager == null || !manager.TryGetTranscripts(out ConvaiTranscripts transcripts))
            return;

        _subscription = transcripts.SubscribeCommitted(OnTurnCommitted, new TranscriptSubscriptionOptions
        {
            ReplayExisting = true
        });
    }

    private void OnDisable() => _subscription?.Dispose();

    private void OnTurnCommitted(TranscriptChange change)
    {
        if (change.Turn == null) return;
        _log.text += $"\n<b>{change.Turn.Speaker.DisplayName}:</b> {change.Turn.DisplayText}";
    }
}
```

{% endcode %}

#### 示例 3 — 带实时字幕的聊天历史

一场工业安全演练在启用时重放已提交的聊天历史，然后让字幕行与实时字幕保持同步——与持久的聊天日志分开。

{% code title="LiveChatAndSubtitleUI.cs" %}

```csharp
using Convai.Domain.Models;
using Convai.Runtime.Components;
using Convai.Runtime.Facades;
using System;
using System.Linq;
using TMPro;
using UnityEngine;

public class LiveChatAndSubtitleUI : MonoBehaviour
{
    [SerializeField] private TMP_Text _chatContent;
    [SerializeField] private TMP_Text _subtitleText;

    private IDisposable _chatSubscription;
    private IDisposable _captionSubscription;

    private void OnEnable()
    {
        ConvaiManager manager = ConvaiManager.ActiveManager;
        if (manager == null || !manager.TryGetTranscripts(out ConvaiTranscripts transcripts))
            return;

        _chatContent.text = string.Join("\n",
            transcripts.CurrentTimeline.CommittedTurns
                .OrderBy(turn => turn.RoomSequence)
                .Select(turn => $"<b>{turn.Speaker.DisplayName}:</b> {turn.DisplayText}"));

        _chatSubscription = transcripts.SubscribeCommitted(OnTurnCommitted);
        _captionSubscription = transcripts.SubscribeCaptions(OnCaption);
    }

    private void OnDisable()
    {
        _chatSubscription?.Dispose();
        _captionSubscription?.Dispose();
    }

    private void OnTurnCommitted(TranscriptChange change)
    {
        if (change.Turn == null) return;
        _chatContent.text += $"\n<b>{change.Turn.Speaker.DisplayName}:</b> {change.Turn.DisplayText}";
    }

    private void OnCaption(TranscriptCaption caption) => _subtitleText.text = caption.Text;
}
```

{% endcode %}

***

### 故障排查

| 症状                                                                        | 可能原因                                                           | 修复方法                                                                                                                                            |
| ------------------------------------------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `ConvaiManager.ActiveManager.Transcripts` 会抛出 `InvalidOperationException` | 在 SDK 完成引导之前被访问                                                | 使用 `manager.TryGetTranscripts(out var transcripts)` 而不是 `转录` 属性在早期 `OnEnable` 或 `Awake`                                                         |
| `GetTurns()` 返回空列表                                                        | 尚无轮次，或者查询的 `包含活动回合` 是 `否` 而每个当前轮次仍然处于活动状态时                     | 省略 `TranscriptQuery`，或设置 `IncludeActiveTurns = true` 以包含进行中的轮次                                                                                  |
| `Subscribe` 回调从未触发                                                        | 订阅得太晚，或者 `IncludeActive`/`IncludeTerminal` 排除了所有匹配的轮次          | 在之前或紧接着之后订阅 `ConnectAsync`；设置 `ReplayExisting = true` 以立即接收现有轮次                                                                                 |
| `TranscriptTurn.StableText` 为空                                            | 轮次仍然 `传输中` 或 `Stable` ——在轮次提交之前，文本并不稳定                         | 使用 `显示文本` 用于进行中的渲染，或者订阅时使用 `SubscribeCommitted`                                                                                                 |
| `SubscribeCaptions` 回调从未触发                                                | `包含流式传输`/`包含最终字幕` 或者说话人筛选器排除了所有匹配的字幕，或者 `重放最新` 是 `否` 且尚未收到新的字幕 | 检查 `包含流式传输`, `包含最终字幕`, `SpeakerType`, `SpeakerId`以及 `ParticipantId` 时 `TranscriptCaptionSubscriptionOptions`；设置 `ReplayLatest = true` 以立即接收当前字幕 |

***

### 下一步

如需在不查询时间线的情况下进行事件驱动的转录响应，请使用 `ConvaiCharacterEventRelay` 或 `ConvaiTranscriptEventRelay` 来驱动目标——参见 [角色事件](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/character-events.md)。完整的角色脚本 API 请参见 [角色与玩家 API](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/character-and-player-api.md)。有关 facade 访问器的完整列表，请参见 `ConvaiManager`，请参见 [ConvaiManager API](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/convaimanager-api.md).


---

# 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/convai-unity-sdk/scripting-reference/transcript-api.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.
