> 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

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

{% hint style="warning" %}
**SDK 4.4.0 中的破坏性变更。** 基于快照的转录模型已被替换。 `CurrentTimeline` 现在返回 `TranscriptTimeline` 而不是 `TranscriptTimelineSnapshot`。 `Changed` 事件现在携带 `TranscriptChangeBatch` 而不是 `TranscriptUpdateBatch`，而轮次为 `TranscriptTurn` 而不是 `TranscriptTurnSnapshot`。整个旧版展示和历史层已被移除： `ITranscriptUI`, `ITranscriptListener`, `TranscriptViewModel`, `TranscriptUIController`, `ChatPresentationStrategy`, `ITranscriptPresentationStrategy`, `ConversationHistoryService`, `TranscriptEntry`，以及 `ConversationExportFormat` 已不再存在。替换 `ConversationHistoryService.Entries` 和 `CurrentTimeline.Turns` 或 `GetTurns(...)`，请将 `EntryAdded` 和 `SubscribeCommitted(...)`，并将 `Export(ConversationExportFormat)` 和 `Export(TranscriptExportFormat)`。如果你使用过 beta 版 `TranscriptSubscriptionOptions.IncludeInterim`/`IncludeCommitted` 字段，请将其重命名为 `IncludeActive` 和 `IncludeTerminal`.
{% endhint %}

***

### 推送 vs. 拉取

|              | 事件中继（`ConvaiTranscriptEventRelay`, `ConvaiEvents`)                 | `ConvaiTranscripts`                                          |
| ------------ | ------------------------------------------------------------------ | ------------------------------------------------------------ |
| **Delivery** | 推送 — 检视器 `UnityEvent`或 C# 事件会在每次更新时触发                              | 拉取（`CurrentTimeline`, `GetTurns`）与推送（`Changed`, `Subscribe`) |
| **历史**       | 仅当前更新                                                              | 完整历史：活动中的轮次、已提交轮次和实时字幕                                       |
| **使用场景**     | 字幕渲染、按角色动画触发器                                                      | 自定义聊天 UI、会话后导出、轮次级评估逻辑                                       |
| **访问**       | `ConvaiTranscriptEventRelay`, `ConvaiManager.ActiveManager.Events` | `ConvaiManager.ActiveManager.Transcripts`                    |

***

### `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` | `bool`                      | 已交付的展示组件是否应渲染转录更新。只读；无论如何，规范历史都会继续记录。 |

#### 事件

| 事件                           | 参数                          | 当                                   |
| ---------------------------- | --------------------------- | ----------------------------------- |
| `Changed`                    | `TranscriptChangeBatch`     | 一个或多个轮次已被添加、更新、提交、中断、更正或移除          |
| `TurnUpdated`                | `TranscriptTurn`            | 某个轮次收到了新文本或非终结状态变更                  |
| `TurnCommitted`              | `TranscriptTurn`            | 某个轮次转换为 `Committed` 或 `Interrupted` |
| `TurnCorrected`              | `TranscriptTurn`            | 先前已提交轮次的文本被更正                       |
| `TurnRemoved`                | `string` （轮次 ID）            | 某个轮次从时间线中移除                         |
| `CaptionsChanged`            | `TranscriptCaptionSnapshot` | 实时字幕快照发生变化                          |
| `PresentationEnabledChanged` | `bool`                      | `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)`                                                                      | `string`                        | 将每个已提交轮次序列化为纯文本、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`

| 属性               | 类型                                            | 描述                                                   |
| ---------------- | --------------------------------------------- | ---------------------------------------------------- |
| `游标`             | `long`                                        | 当时间线更新时单调递增的值                                        |
| `ActiveTurns`    | `IReadOnlyList<TranscriptTurn>`               | 尚未提交的轮次（`倾听`, `流式传输中`，或 `稳定`)                        |
| `CommittedTurns` | `IReadOnlyList<TranscriptTurn>`               | 处于终结状态的轮次（`Committed` 或 `Interrupted`)               |
| `TurnsById`      | `IReadOnlyDictionary<string, TranscriptTurn>` | 按 `TranscriptTurn.Id`                                |
| `Turns`          | `IReadOnlyList<TranscriptTurn>`               | `ActiveTurns` 和 `CommittedTurns` 组合并按 `RoomSequence` |

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

***

### `TranscriptTurn`

| 属性                  | 类型                                 | 描述                                                  |
| ------------------- | ---------------------------------- | --------------------------------------------------- |
| `Id`                | `string`                           | 此轮次的唯一标识符                                           |
| `MessageId`         | `string`                           | 与此轮次关联的消息标识符                                        |
| `ResponseId`        | `string`                           | 与此轮次关联的响应标识符（如适用）                                   |
| `RoomSequence`      | `long`                             | 房间内单调递增的序列号                                         |
| `Revision`          | `int`                              | 每当轮次内容或状态变化时递增                                      |
| `Speaker`           | `TranscriptSpeaker`                | 此轮次的生成者                                             |
| `State`             | `TranscriptTurnState`              | 此轮次当前的生命周期状态                                        |
| `PrimaryTextSource` | `TranscriptTextSource`             | 支撑此轮次显示文本的主要文本来源                                    |
| `StableText`        | `string`                           | 在后续更新中不会改变的最终文本                                     |
| `InterimText`       | `string`                           | 来自当前流式片段的进行中文本                                      |
| `DisplayText`       | `string`                           | 此轮次要渲染的文本——结合 `StableText` 和 `InterimText`          |
| `StartedAtUtc`      | `DateTime`                         | 轮次开始的 UTC 时间                                        |
| `LastUpdatedAtUtc`  | `DateTime`                         | 最近一次更新的 UTC 时间                                      |
| `CommittedAtUtc`    | `DateTime?`                        | 轮次被提交的 UTC 时间； `null` 在活动期间                         |
| `WasInterrupted`    | `bool`                             | `true` 当轮次因中断而结束时                                   |
| `Segments`          | `IReadOnlyList<TranscriptSegment>` | 构成此轮次的各个转录片段                                        |
| `HasText`           | `bool`                             | `true` 时自动连接， `DisplayText` 非空                      |
| `IsCommitted`       | `bool`                             | `true` 时自动连接， `State` 为 `Committed` 或 `Interrupted` |

{% hint style="info" %}
使用 `DisplayText` 用于实时渲染。它组合 `StableText` 和 `InterimText`，因此无论 `State`.
{% endhint %}

#### `TranscriptTurnState` 枚举

| 值                 | 描述                                                         |
| ----------------- | ---------------------------------------------------------- |
| `倾听` (0)          | 轮次处于打开状态并等待语音或文本输入；尚未捕获任何文本                                |
| `流式传输中` (1)       | 轮次正在主动接收文本； `InterimText` 正在更新                             |
| `稳定` (2)          | 流式传输已暂停；文本已稳定，但轮次尚未提交                                      |
| `Committed` (4)   | 轮次已完全提交； `StableText` 为最终版                                 |
| `Interrupted` (5) | 轮次因中断而结束（`WasInterrupted` 为 `true`)                        |
| `已丢弃` (6)         | 轮次已在没有文本的情况下关闭，并从以下两者中排除： `ActiveTurns` 和 `CommittedTurns` |

***

### `TranscriptSegment`

| 属性             | 类型                     | 描述                          |
| -------------- | ---------------------- | --------------------------- |
| `Id`           | `string`               | 此片段的唯一标识符                   |
| `TurnId`       | `string`               | 父级的 ID `TranscriptTurn`     |
| `Speaker`      | `TranscriptSpeaker`    | 此片段的生成者                     |
| `StableText`   | `string`               | 此片段的最终文本                    |
| `InterimText`  | `string`               | 此片段的进行中文本                   |
| `DisplayText`  | `string`               | 此片段要渲染的文本                   |
| `State`        | `TranscriptTurnState`  | 此片段的生命周期状态                  |
| `来源`           | `TranscriptTextSource` | 此片段文本的来源                    |
| `StartedAtUtc` | `DateTime`             | 此片段开始的 UTC 时间               |
| `UpdatedAtUtc` | `DateTime`             | 最近一次更新的 UTC 时间              |
| `StoppedAtUtc` | `DateTime?`            | 此片段停止的 UTC 时间； `null` 在活动期间 |

#### `TranscriptTextSource` 枚举

| 值                         | 描述                   |
| ------------------------- | -------------------- |
| `未知` (0)                  | 无法确定来源               |
| `InterimAsr` (1)          | 进行中的语音转文本识别          |
| `AsrFinal` (2)            | 最终语音转文本识别            |
| `ProcessedFinal` (3)      | 经玩家端处理后的最终文本         |
| `TypedText` (4)           | 由玩家输入而非说出的文本         |
| `BotOutput` (5)           | 最终角色回应文本             |
| `BotPreview` (6)          | 进行中的角色回应预览（LLM 流式传输） |
| `LegacyBotTranscript` (7) | 来自旧版管线的角色转录文本        |

***

### `TranscriptSpeaker`

| 属性              | 类型                      | 描述                                       |
| --------------- | ----------------------- | ---------------------------------------- |
| `类型`            | `TranscriptSpeakerType` | 此说话者是否为 `Player`, `Character`，或 `System` |
| `Id`            | `string`                | 此说话者的角色 ID 或玩家 ID                        |
| `DisplayName`   | `string`                | 可读名称                                     |
| `ParticipantId` | `string`                | 房间级参与者标识符                                |

#### `TranscriptSpeakerType` 枚举

| 值               | 描述                  |
| --------------- | ------------------- |
| `Player` (0)    | 人类玩家参与者             |
| `Character` (1) | AI 角色参与者            |
| `System` (2)    | 系统来源的说话者，不关联任何玩家或角色 |

***

### `TranscriptChange` 和 `TranscriptChangeBatch`

`TranscriptChange`:

| 属性       | 类型                     | 描述                                     |
| -------- | ---------------------- | -------------------------------------- |
| `类型`     | `TranscriptChangeKind` | 此实例代表的变更类型                             |
| `Turn`   | `TranscriptTurn`       | 受影响的轮次； `null` 时自动连接， `类型` 为 `Removed` |
| `TurnId` | `string`               | 受影响轮次的 ID                              |

`TranscriptChangeBatch`:

| 属性             | 类型                                | 描述                        |
| -------------- | --------------------------------- | ------------------------- |
| `时间线`          | `TranscriptTimeline`              | 本批变更后的完整时间线               |
| `变更`           | `IReadOnlyList<TranscriptChange>` | 此批次中包含的每一项变更              |
| `ChangedTurns` | `IReadOnlyList<TranscriptTurn>`   | 便捷访问器：每个非空 `Turn` 来自 `变更` |

#### `TranscriptChangeKind` 枚举

| 值                 | 描述                    |
| ----------------- | --------------------- |
| `Added` (0)       | 创建了一个新轮次              |
| `Updated` (1)     | 现有轮次收到了新文本或非终结状态变更    |
| `Committed` (2)   | 某个轮次转换为 `Committed`   |
| `Interrupted` (3) | 某个轮次转换为 `Interrupted` |
| `Corrected` (4)   | 先前已提交轮次的文本被更正         |
| `Removed` (5)     | 某个轮次从时间线中移除           |

***

### `TranscriptQuery` — 过滤 `GetTurns` 结果

| 字段                      | 类型                           | 默认值         | 描述                             |
| ----------------------- | ---------------------------- | ----------- | ------------------------------ |
| `ParticipantKind`       | `TranscriptParticipantKind?` | `null` （全部） | 过滤为 `Player` 或 `Character` 仅轮次 |
| `PlayerOrCharacterId`   | `string`                     | `null` （全部） | 过滤为特定玩家或角色 ID                  |
| `ParticipantId`         | `string`                     | `null` （全部） | 过滤为特定房间参与者 ID                  |
| `IncludeActiveTurns`    | `bool`                       | `true`      | 包含仍处于活动状态的轮次（尚未提交）             |
| `IncludeCommittedTurns` | `bool`                       | `true`      | 包含已提交或已中断的轮次                   |

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

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

`TranscriptQuery` 与早期 SDK 版本相比保持不变——它保留了 `IncludeActiveTurns`/`IncludeCommittedTurns` 字段名称。只有较新的 `TranscriptSubscriptionOptions`，由 `Subscribe`使用，才采用重命名后的 `IncludeActive`/`IncludeTerminal` 字段。

#### `TranscriptParticipantKind` 枚举

| 值               | 描述       |
| --------------- | -------- |
| `Player` (0)    | 人类玩家参与者  |
| `Character` (1) | AI 角色参与者 |

#### `TranscriptParticipantRef` 结构体

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

| 属性                    | 类型                          | 描述                                               |
| --------------------- | --------------------------- | ------------------------------------------------ |
| `类型`                  | `TranscriptParticipantKind` | 此参与者是否为 `Player` 或 `Character`                   |
| `PlayerOrCharacterId` | `string`                    | 此参与者的角色 ID 或玩家 ID                                |
| `DisplayName`         | `string`                    | 可读名称                                             |
| `ParticipantId`       | `string`                    | 房间级参与者标识符                                        |
| `IsEmpty`             | `bool`                      | `true` 时自动连接， `PlayerOrCharacterId` 为 null 或仅为空白 |

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

***

### `TranscriptSubscriptionOptions` — 过滤 `Subscribe` 回调

| 字段                | 类型                       | 默认值         | 描述                                             |
| ----------------- | ------------------------ | ----------- | ---------------------------------------------- |
| `ReplayExisting`  | `bool`                   | `false`     | 当 `true`, `Subscribe` 会立即为已在 `CurrentTimeline` |
| `IncludeActive`   | `bool`                   | `true`      | 包含尚未提交的轮次                                      |
| `IncludeTerminal` | `bool`                   | `true`      | 包含已提交或已中断的轮次                                   |
| `SpeakerType`     | `TranscriptSpeakerType?` | `null` （全部） | 过滤为特定说话者类型                                     |
| `SpeakerId`       | `string`                 | `null` （全部） | 过滤为特定说话者 ID                                    |
| `ParticipantId`   | `string`                 | `null` （全部） | 过滤为特定房间参与者 ID                                  |

在 SDK 4.4.0 中，这些字段从以下名称重命名而来 `IncludeInterim` 和 `IncludeCommitted` 到 `IncludeActive` 和 `IncludeTerminal`.

***

### 实时字幕

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

#### `TranscriptCaption`

| 属性               | 类型                       | 描述                                            |
| ---------------- | ------------------------ | --------------------------------------------- |
| `TurnId`         | `string`                 | 此字幕所对齐的转录轮次 ID                                |
| `Speaker`        | `TranscriptSpeaker`      | 此字幕的生成者                                       |
| `Text`           | `string`                 | 字幕文本                                          |
| `State`          | `TranscriptCaptionState` | 当前字幕状态                                        |
| `UpdatedAtUtc`   | `DateTime`               | 最近一次更新的 UTC 时间                                |
| `HasText`        | `bool`                   | `true` 时自动连接， `Text` 非空                       |
| `IsFinal`        | `bool`                   | `true` 时自动连接， `State` 为 `已完成` 或 `Interrupted` |
| `WasInterrupted` | `bool`                   | `true` 时自动连接， `State` 为 `Interrupted`         |

#### `TranscriptCaptionState` 枚举

| 值                 | 描述                |
| ----------------- | ----------------- |
| `流式传输中` (0)       | 字幕文本正在主动更新        |
| `稳定` (1)          | 字幕文本已暂停更新，但尚未最终确定 |
| `已完成` (2)         | 字幕正常结束            |
| `Interrupted` (3) | 字幕因轮次被中断而结束       |

#### `TranscriptCaptionSnapshot`

| 属性   | 类型                                 | 描述            |
| ---- | ---------------------------------- | ------------- |
| `游标` | `long`                             | 每当字幕更新时单调递增的值 |
| `字幕` | `IReadOnlyList<TranscriptCaption>` | 当前的实时字幕集合     |

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

#### `TranscriptCaptionSubscriptionOptions`

| 字段                 | 类型                       | 默认值         | 描述                                              |
| ------------------ | ------------------------ | ----------- | ----------------------------------------------- |
| `ReplayLatest`     | `bool`                   | `true`      | 当 `true`, `SubscribeCaptions` 会立即为每个当前匹配的字幕调用回调 |
| `IncludeStreaming` | `bool`                   | `true`      | 包含仍在更新的字幕                                       |
| `IncludeFinal`     | `bool`                   | `true`      | 包含已完成或已中断的字幕                                    |
| `SpeakerType`      | `TranscriptSpeakerType?` | `null` （全部） | 过滤为特定说话者类型                                      |
| `SpeakerId`        | `string`                 | `null` （全部） | 过滤为特定说话者 ID                                     |
| `ParticipantId`    | `string`                 | `null` （全部） | 过滤为特定房间参与者 ID                                   |

***

### 导出转录

`Export(TranscriptExportFormat format)` 将 `CurrentTimeline.CommittedTurns`中的每个轮次序列化，按 `RoomSequence`排序，生成一个单一字符串。说话者标签回退为 `Speaker.Type` 时自动连接， `Speaker.DisplayName` 时为空。

#### `TranscriptExportFormat` 枚举

| 值              | 描述                                    |
| -------------- | ------------------------------------- |
| `纯文本` (0)      | 每个轮次一行： `speaker: text`               |
| `Markdown` (1) | 每个轮次一行： `**speaker:** text`，轮次之间留一个空行 |
| `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()` 返回空列表                                                        | 尚未存在任何轮次，或者查询的 `IncludeActiveTurns` 为 `false` 而每个当前轮次仍处于活动状态时                               | 省略 `TranscriptQuery`，或者设置 `IncludeActiveTurns = true` 以包含进行中的轮次                                                                                                  |
| `Subscribe` 回调从未触发                                                        | 订阅太晚，或者 `IncludeActive`/`IncludeTerminal` 排除了所有匹配的轮次                                        | 请在之前或紧接着之后订阅 `ConnectAsync`；设置 `ReplayExisting = true` 可立即接收现有轮次                                                                                                 |
| `TranscriptTurn.StableText` 为空                                            | 轮次仍处于 `流式传输中` 或 `稳定` — 在回合提交之前，文本都不稳定                                                       | 使用 `DisplayText` 用于进行中的渲染，或使用以下方式订阅 `SubscribeCommitted`                                                                                                         |
| `SubscribeCaptions` 回调从未触发                                                | `IncludeStreaming`/`IncludeFinal` 或者说话人筛选器排除了每一条匹配的字幕，或者 `ReplayLatest` 为 `false` 且尚未有新字幕到达 | 检查 `IncludeStreaming`, `IncludeFinal`, `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`的 SDK 方法，请参见 [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.
