> 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`.

***

### 推送与拉取

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

***

### `ConvaiCharacter.OnTranscriptReceived`

`OnTranscriptReceived` 是在 `ConvaiCharacter`: `event Action<string, bool> OnTranscriptReceived`上声明的普通 C# 事件。它是下面门面在角色作用域中的对应项——当你已经持有某个 `ConvaiTranscripts` 的引用，并且希望直接接收该角色自己的发言推送通知，而不必订阅房间级门面或添加事件中继组件时，请使用它。 `ConvaiCharacter` character.OnTranscriptReceived += HandleTranscript;

```csharp
private void HandleTranscript(string text, bool isFinal)

if (!isFinal) return; // 丢弃中间结果，避免句子被追加两次
{
    _log.text += $"\n{text}";
    第一个参数是转录文本。第二个，
}
```

，来自消息自身的生命周期——已完成的行报告 `isFinal`；仍在流式传输中的行报告 `true`。 `false`.

文本按句子到达，而不是按合成块到达。一个在完成前流式输出的句子会被发送 **两次**：一次带有 `isFinal` `false` ，表示它仍在进行中；等它完成后再发送一次带有 `isFinal` `true` 。如果处理程序在每次调用时都追加文本而不检查 `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` | `bool`                      | 已发布的演示组件是否应渲染转录更新。只读；无论如何，规范历史都会继续记录。 |

#### 事件

| 事件                           | 参数                          | 触发条件：                      |
| ---------------------------- | --------------------------- | -------------------------- |
| `Changed`                    | `TranscriptChangeBatch`     | 添加、更新、提交、中断、更正或移除了一个或多个轮次  |
| `TurnUpdated`                | `TranscriptTurn`            | 某个轮次收到新文本或发生了非终态状态变化       |
| `TurnCommitted`              | `TranscriptTurn`            | 某个轮次过渡到 `已提交` 或 `被打断`      |
| `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`

| 属性               | 类型                                            | 说明                                                    |
| ---------------- | --------------------------------------------- | ----------------------------------------------------- |
| `Cursor`         | `long`                                        | 单调递增的值，每当时间线更新时都会变化                                   |
| `ActiveTurns`    | `IReadOnlyList<TranscriptTurn>`               | 尚未提交的轮次（`Listening`, `流式传输中`，或 `Stable`)              |
| `CommittedTurns` | `IReadOnlyList<TranscriptTurn>`               | 处于终态的轮次（`已提交` 或 `被打断`)                                |
| `TurnsById`      | `IReadOnlyDictionary<string, TranscriptTurn>` | 按 `TranscriptTurn.Id`                                 |
| `索引的所有轮次`        | `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` 是 `已提交` 或 `被打断`            |

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

#### `TranscriptTurnState` 枚举

| 值               | 说明                                                        |
| --------------- | --------------------------------------------------------- |
| `Listening` (0) | 轮次处于开启状态并等待语音或文本输入；尚未捕获到文本                                |
| `流式传输中` (1)     | 轮次正在积极接收文本； `InterimText` 正在更新                            |
| `Stable` (2)    | 流式传输已暂停；文本已稳定，但轮次尚未提交                                     |
| `已提交` (4)       | 轮次已完全提交； `StableText` 是最终的                                |
| `被打断` (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`，或 `系统` |
| `Id`            | `string`                | 此说话者的角色 ID 或玩家 ID                    |
| `DisplayName`   | `string`                | 可读名称                                 |
| `ParticipantId` | `string`                | 房间级参与者标识符                            |

#### `TranscriptSpeakerType` 枚举

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

***

### `TranscriptChange` 和 `TranscriptChangeBatch`

`TranscriptChange`:

| 属性       | 类型                     | 说明                                  |
| -------- | ---------------------- | ----------------------------------- |
| `Kind`   | `TranscriptChangeKind` | 此实例所表示的变化类型                         |
| `Turn`   | `TranscriptTurn`       | 受影响的轮次； `null` 当 `Kind` 是 `Removed` |
| `TurnId` | `string`               | 受影响轮次的 ID                           |

`TranscriptChangeBatch`:

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

#### `TranscriptChangeKind` 枚举

| 值               | 说明                 |
| --------------- | ------------------ |
| `Added` (0)     | 创建了一个新轮次           |
| `Updated` (1)   | 现有轮次收到了新文本或非终态状态变化 |
| `已提交` (2)       | 某个轮次过渡到 `已提交`      |
| `被打断` (3)       | 某个轮次过渡到 `被打断`      |
| `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` 使用 `IncludeActiveTurns`/`IncludeCommittedTurns` 字段名。 `TranscriptSubscriptionOptions`，由 `Subscribe`使用时，采用 `IncludeActive`/`IncludeTerminal` 替代。

#### `TranscriptParticipantKind` 枚举

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

#### `TranscriptParticipantRef` 结构体

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

| 属性                    | 类型                          | 说明                                        |
| --------------------- | --------------------------- | ----------------------------------------- |
| `Kind`                | `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                                   |

***

### 实时字幕

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

#### `TranscriptCaption`

| 属性               | 类型                       | 说明                               |
| ---------------- | ------------------------ | -------------------------------- |
| `TurnId`         | `string`                 | 此字幕所对齐的转录轮次 ID                   |
| `Speaker`        | `TranscriptSpeaker`      | 生成此字幕的主体                         |
| `文本`             | `string`                 | 字幕文本                             |
| `State`          | `TranscriptCaptionState` | 当前字幕状态                           |
| `UpdatedAtUtc`   | `DateTime`               | 最近一次更新的 UTC 时间                   |
| `HasText`        | `bool`                   | `true` 当 `文本` 非空                 |
| `IsFinal`        | `bool`                   | `true` 当 `State` 是 `已完成` 或 `被打断` |
| `WasInterrupted` | `bool`                   | `true` 当 `State` 是 `被打断`         |

#### `TranscriptCaptionState` 枚举

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

#### `TranscriptCaptionSnapshot`

| 属性         | 类型                                 | 说明                |
| ---------- | ---------------------------------- | ----------------- |
| `Cursor`   | `long`                             | 字幕每次更新时都会变化的单调递增值 |
| `Captions` | `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` 枚举

| 值               | 说明                                  |
| --------------- | ----------------------------------- |
| `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($"Transcript saved to {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` 为空                                              | 轮次仍然 `流式传输中` 或 `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)。有关 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 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/convai-unity-sdk/scripting-reference/transcript-api.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.
