> 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`。The `已更改` 事件现在携带 `TranscriptChangeBatch` 而不是 `TranscriptUpdateBatch`，而轮次是 `TranscriptTurn` 而不是 `TranscriptTurnSnapshot`。整个旧版展示和历史层已被移除： `ITranscriptUI`, `ITranscriptListener`, `TranscriptViewModel`, `TranscriptUIController`, `ChatPresentationStrategy`, `ITranscriptPresentationStrategy`, `ConversationHistoryService`, `TranscriptEntry`，以及 `ConversationExportFormat` 已不再存在。请将 `ConversationHistoryService.Entries` 与 `CurrentTimeline.Turns` 或 `GetTurns(...)`，并将 `EntryAdded` 与 `SubscribeCommitted(...)`，并将 `Export(ConversationExportFormat)` 与 `Export(TranscriptExportFormat)`。如果你使用过测试版 `TranscriptSubscriptionOptions.IncludeInterim`/`IncludeCommitted` 字段，请将其重命名为 `IncludeActive` 并且 `IncludeTerminal`.
{% endhint %}

***

### 推送与拉取

|          | 事件转发（`ConvaiTranscriptEventRelay`, `ConvaiEvents`)                 | `ConvaiTranscripts`                                  |
| -------- | ------------------------------------------------------------------ | ---------------------------------------------------- |
| **交付**   | 推送 — 检视器 `UnityEvent`s 或 C# 事件会在每次更新时触发                            | 拉取（`CurrentTimeline`, `GetTurns`）`已更改`, `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`                      | 随附的展示组件是否应渲染转录更新。只读；无论如何，标准历史都会继续记录。 |

#### 事件

| 事件                           | 参数                          | 在以下情况下触发                          |
| ---------------------------- | --------------------------- | --------------------------------- |
| `已更改`                        | `TranscriptChangeBatch`     | 一个或多个轮次被添加、更新、提交、中断、更正或移除         |
| `TurnUpdated`                | `TranscriptTurn`            | 轮次收到新文本或发生非终结状态更改                 |
| `TurnCommitted`              | `TranscriptTurn`            | 轮次转换为 `Committed` 或 `Interrupted` |
| `TurnCorrected`              | `TranscriptTurn`            | 先前已提交轮次的文本被更正                     |
| `TurnRemoved`                | `string` （轮次 ID）            | 轮次从时间线中移除                         |
| `字幕变更`                       | `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`

| 属性               | 类型                                            | 描述                                                          |
| ---------------- | --------------------------------------------- | ----------------------------------------------------------- |
| `Cursor`         | `long`                                        | 每当时间线更新时都会变化的单调递增值                                          |
| `ActiveTurns`    | `IReadOnlyList<TranscriptTurn>`               | 尚未提交的轮次（`Listening`, `Streaming`，或 `Stable`)                |
| `CommittedTurns` | `IReadOnlyList<TranscriptTurn>`               | 处于终结状态的轮次（`Committed` 或 `Interrupted`)                      |
| `TurnsById`      | `IReadOnlyDictionary<string, TranscriptTurn>` | 按以下项索引的所有轮次： `TranscriptTurn.Id`                            |
| `Turns`          | `IReadOnlyList<TranscriptTurn>`               | `ActiveTurns` 并且 `CommittedTurns` 合并并按以下项排序： `RoomSequence` |

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

***

### `TranscriptTurn`

| 属性                  | 类型                                 | 描述                                             |
| ------------------- | ---------------------------------- | ---------------------------------------------- |
| `Id`                | `string`                           | 此轮次的唯一标识符                                      |
| `消息 ID`             | `string`                           | 与此轮次关联的消息标识符                                   |
| `响应 ID`             | `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` enum

| Value             | 描述                                                           |
| ----------------- | ------------------------------------------------------------ |
| `Listening` (0)   | 轮次处于开启状态，正在等待语音或文本输入；尚未捕获文本                                  |
| `Streaming` (1)   | 轮次正在主动接收文本； `InterimText` 正在更新                               |
| `Stable` (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` enum

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

***

### `TranscriptSpeaker`

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

#### `TranscriptSpeakerType` enum

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

***

### `TranscriptChange` 并且 `TranscriptChangeBatch`

`TranscriptChange`:

| 属性       | 类型                     | 描述                                  |
| -------- | ---------------------- | ----------------------------------- |
| `Kind`   | `TranscriptChangeKind` | 此实例表示的更改类型                          |
| `Turn`   | `TranscriptTurn`       | 受影响的轮次； `null` 时 `Kind` 是 `Removed` |
| `TurnId` | `string`               | 受影响轮次的 ID                           |

`TranscriptChangeBatch`:

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

#### `TranscriptChangeKind` enum

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

***

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

| 字段                      | 类型                           | 默认          | 描述                  |
| ----------------------- | ---------------------------- | ----------- | ------------------- |
| `ParticipantKind`       | `TranscriptParticipantKind?` | `null` （全部） | 筛选为 `玩家` 或 `角色` 仅轮次 |
| `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` enum

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

#### `TranscriptParticipantRef` struct

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

| 属性                    | 类型                          | 描述                                          |
| --------------------- | --------------------------- | ------------------------------------------- |
| `Kind`                | `TranscriptParticipantKind` | 此参与者是否为 `玩家` 或 `角色`                         |
| `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`      | 生成此字幕的主体                                 |
| `文本`             | `string`                 | 字幕文本                                     |
| `State`          | `TranscriptCaptionState` | 当前字幕状态                                   |
| `UpdatedAtUtc`   | `DateTime`               | 最近一次更新的 UTC 时间                           |
| `HasText`        | `bool`                   | `true` 时 `文本` 非空                         |
| `IsFinal`        | `bool`                   | `true` 时 `State` 是 `已完成` 或 `Interrupted` |
| `WasInterrupted` | `bool`                   | `true` 时 `State` 是 `Interrupted`         |

#### `TranscriptCaptionState` enum

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

#### `TranscriptCaptionSnapshot`

| 属性       | 类型                                 | 描述                |
| -------- | ---------------------------------- | ----------------- |
| `Cursor` | `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` enum

| Value           | 描述                                  |
| --------------- | ----------------------------------- |
| `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)` 而不是 `Transcripts` 属性在早期 `OnEnable` 或 `Awake`                                                                 |
| `GetTurns()` 返回空列表                                                       | 当前还没有任何轮次，或者查询的 `IncludeActiveTurns` 是 `false` 而所有当前轮次仍处于活动状态时                              | 省略 `TranscriptQuery`，或者设置 `IncludeActiveTurns = true` 以包含进行中的轮次                                                                                                  |
| `Subscribe` 回调从不触发                                                       | 订阅得太晚，或者 `IncludeActive`/`IncludeTerminal` 排除了所有匹配的轮次                                       | 请在以下之前订阅，或在其后立即订阅 `ConnectAsync`；设置 `ReplayExisting = true` 以立即接收现有轮次                                                                                            |
| `TranscriptTurn.StableText` 为空                                           | 轮次仍然 `Streaming` 或 `Stable` ——在轮次提交之前，文本并不稳定                                                | 使用 `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)。有关以下内容上的所有门面访问器完整列表，请参见 `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.
