> 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` 为你提供基于拉取的、可快照访问房间完整转录时间线的能力。与事件中继组件不同，后者会在各个更新到达时逐个推送更新，而 `ConvaiTranscripts` 该门面维护着一个实时的内存时间线，你可以按需查询。这使它非常适合用于转录历史回放、自定义聊天 UI 构建、会后导出，以及任何需要一次读取多轮内容的功能。通过以下方式访问该门面： `ConvaiManager.ActiveManager.Transcripts`.

***

### 推送 vs. 拉取

|          | 事件中继 / `ConvaiEvents`                     | `ConvaiTranscripts`                       |
| -------- | ----------------------------------------- | ----------------------------------------- |
| **传递**   | 推送——每次更新都会调用你的回调                          | 拉取——你需要时随时读取时间线                           |
| **历史**   | 每个回调仅返回最近一次更新                             | 完整历史：活动轮次 + 已提交轮次                         |
| **使用场景** | 字幕渲染、实时动画触发                               | 会后导出、自定义聊天记录、评估回顾                         |
| **访问**   | `ConvaiSessionEventRelay`, `ConvaiEvents` | `ConvaiManager.ActiveManager.Transcripts` |

***

### `ConvaiTranscripts` 门面

#### 属性

| 成员                | 类型                           | 说明             |
| ----------------- | ---------------------------- | -------------- |
| `CurrentTimeline` | `TranscriptTimelineSnapshot` | 访问时刻整个转录时间线的快照 |

#### 方法

| 方法                                        | 返回                                      | 说明                                    |
| ----------------------------------------- | --------------------------------------- | ------------------------------------- |
| `GetTurns(TranscriptQuery?)`              | `IReadOnlyList<TranscriptTurnSnapshot>` | 返回所有与可选查询匹配的轮次。传入 `null` 以获取所有轮次。     |
| `GetTurn(string turnId)`                  | `TranscriptTurnSnapshot`                | 按 ID 获取特定轮次。若未找到则返回 `null` 。          |
| `GetLatestTurn(TranscriptParticipantRef)` | `TranscriptTurnSnapshot`                | 返回给定参与者的最近一次轮次。若没有则返回 `null` 。        |
| `Dispose()`                               | `void`                                  | 取消订阅内部变更事件。如果你订阅了该事件，请在组件销毁时调用 `已更改`. |

#### 事件

| 事件    | 参数                      | 触发时                        |
| ----- | ----------------------- | -------------------------- |
| `已更改` | `TranscriptUpdateBatch` | 时间线发生变化——轮次被添加、更新、完成、中断或移除 |

```csharp
var transcripts = ConvaiManager.ActiveManager.Transcripts;
transcripts.Changed += OnTranscriptChanged;

private void OnTranscriptChanged(TranscriptUpdateBatch batch)
{
    foreach (var turnId in batch.CompletedTurnIds)
    {
        var turn = batch.Timeline.TurnsById[turnId];
        Debug.Log($"[{turn.Participant.DisplayName}] {turn.CommittedText}");
    }
}
```

***

### `TranscriptQuery` — 过滤轮次

| 字段                    | 类型                           | 默认值         | 说明                 |
| --------------------- | ---------------------------- | ----------- | ------------------ |
| `参与者类型`               | `TranscriptParticipantKind?` | `null` （全部） | 过滤到 `玩家` 或 `角色` 回合 |
| `PlayerOrCharacterId` | `string`                     | `null` （全部） | 过滤到特定玩家或角色 ID      |
| `ParticipantId`       | `string`                     | `null` （全部） | 过滤到特定房间参与者 ID      |
| `包含活动回合`              | `bool`                       | `true`      | 包含仍在流式传输中的轮次       |
| `包含已提交回合`             | `bool`                       | `true`      | 包含已最终确定并提交的轮次      |

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

IReadOnlyList<TranscriptTurnSnapshot> turns =
    ConvaiManager.ActiveManager.Transcripts.GetTurns(query);
```

***

### `TranscriptTurnSnapshot`

| 属性                              | 类型                                         | 说明                                     |
| ------------------------------- | ------------------------------------------ | -------------------------------------- |
| `TurnId`                        | `string`                                   | 此轮次的唯一标识符（与 `MessageId`)               |
| `MessageId`                     | `string`                                   | 别名 `TurnId`                            |
| `RoomSequence`                  | `long`                                     | 房间内单调递增的序列号                            |
| `参与者`                           | `TranscriptParticipantRef`                 | 创建此轮次的是谁                               |
| `StartedAtUtc`                  | `DateTime`                                 | 回合开始的 UTC 时间                           |
| `LastUpdatedAtUtc`              | `DateTime`                                 | 最近一次更新的 UTC 时间                         |
| `CompletedAtUtc`                | `DateTime?`                                | 轮次被提交的 UTC 时间； `null` 在活动期间            |
| `生命周期`                          | `TranscriptLifecycle`                      | 此轮次当前的生命周期阶段                           |
| `CommittedText`                 | `string`                                   | 不会再更改的最终文本                             |
| `InterimText`                   | `string`                                   | 来自当前流式片段的进行中文本                         |
| `DisplayText`                   | `string`                                   | `CommittedText + InterimText` ——用于实时显示 |
| `WasInterrupted`                | `bool`                                     | 当轮次因中断而结束时为 True                       |
| `HasText`                       | `bool`                                     | 当 `DisplayText` 非空时为 True              |
| `Segments`                      | `IReadOnlyList<TranscriptSegmentSnapshot>` | 此轮次中的各个转录片段                            |
| `ConversationTargetCharacterId` | `string`                                   | 如果已知，则为此玩家轮次所针对的角色                     |

{% hint style="info" %}
使用 `DisplayText` 用于实时字幕或聊天渲染。它结合了 `CommittedText` 是位于 `InterimText` 因此无论生命周期阶段如何，你总是显示可用的最完整文本。
{% endhint %}

***

### `TranscriptLifecycle` enum

| 值          | 说明                                                |
| ---------- | ------------------------------------------------- |
| `流式传输` (0) | 轮次正在积极接收文本； `InterimText` 正在更新                    |
| `稳定` (1)   | 流式传输已暂停；文本稳定，但轮次尚未提交                              |
| `已完成` (2)  | 轮次已完全提交； `CommittedText` 是最终的，并且 `InterimText` 为空 |

***

### `TranscriptTimelineSnapshot`

| 属性                        | 类型                                                    | 说明                                   |
| ------------------------- | ----------------------------------------------------- | ------------------------------------ |
| `光标`                      | `long`                                                | 单调递增的值；每次时间线更新都会变化                   |
| `ActiveTurns`             | `IReadOnlyList<TranscriptTurnSnapshot>`               | 当前正在流式传输或稳定的轮次——尚未提交                 |
| `CommittedTurns`          | `IReadOnlyList<TranscriptTurnSnapshot>`               | 已完全最终确定的轮次                           |
| `TurnsById`               | `IReadOnlyDictionary<string, TranscriptTurnSnapshot>` | 所有轮次按 `TurnId` 索引，可实现 O(1) 查找        |
| `LatestTurnByParticipant` | `IReadOnlyDictionary<string, TranscriptTurnSnapshot>` | 按参与者索引的每位参与者最近一次轮次，按 `ParticipantId` |

`TranscriptTimelineSnapshot.Empty` 在没有活动会话时是安全的默认值。

***

### `TranscriptUpdateBatch`

| 属性         | 类型                                      | 说明                         |
| ---------- | --------------------------------------- | -------------------------- |
| `时间线`      | `TranscriptTimelineSnapshot`            | 此批更改后的时间线完整快照              |
| `光标`       | `long`                                  | 便捷访问器，用于 `Timeline.Cursor` |
| `已更改回合`    | `IReadOnlyList<TranscriptTurnSnapshot>` | 此批中发生变化的所有轮次               |
| `新增回合 ID`  | `IReadOnlyList<string>`                 | 在此批中首次添加的轮次 ID             |
| `已更新回合 ID` | `IReadOnlyList<string>`                 | 接收到新文本或新状态的轮次 ID           |
| `已完成回合 ID` | `IReadOnlyList<string>`                 | 已转换为 `已完成` 的轮次 ID          |
| `被中断回合 ID` | `IReadOnlyList<string>`                 | 因中断而结束的轮次 ID               |
| `已移除回合 ID` | `IReadOnlyList<string>`                 | 从时间线中移除的轮次 ID              |

***

### `TranscriptParticipantRef` struct

| 属性                    | 类型                          | 说明                                   |
| --------------------- | --------------------------- | ------------------------------------ |
| `种类`                  | `TranscriptParticipantKind` | 此参与者是否为 `玩家` 或 `角色`                  |
| `PlayerOrCharacterId` | `string`                    | 与此参与者关联的角色 ID 或玩家 ID                 |
| `DisplayName`         | `string`                    | 人类可读名称                               |
| `ParticipantId`       | `string`                    | 房间级参与者标识符                            |
| `IsEmpty`             | `bool`                      | 当 `PlayerOrCharacterId` 为 null 或空白字符 |

支持相等比较和 `==`/`!=` 运算符。

#### `TranscriptParticipantKind` enum

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

***

### 使用示例

#### 示例 1——会后转录导出

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

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

```csharp
using Convai.Domain.Models;
using Convai.Runtime.Facades;
using Newtonsoft.Json;
using System.IO;
using System.Linq;
using UnityEngine;

public class TranscriptExporter : MonoBehaviour
{
    public void ExportToJson(string outputPath)
    {
        var transcripts = ConvaiManager.ActiveManager?.Transcripts;
        if (transcripts == null) return;

        var turns = transcripts.GetTurns()
            .Where(t => t.Lifecycle == TranscriptLifecycle.Completed)
            .OrderBy(t => t.RoomSequence)
            .Select(t => new
            {
                speaker   = t.Participant.DisplayName,
                role      = t.Participant.Kind.ToString(),
                text      = t.CommittedText,
                startedAt = t.StartedAtUtc.ToString("O"),
                completed = t.CompletedAtUtc?.ToString("O")
            })
            .ToArray();

        string json = JsonConvert.SerializeObject(turns, Formatting.Indented);
        File.WriteAllText(outputPath, json);
        Debug.Log($"Transcript saved to {outputPath}");
    }
}
```

{% endcode %}

#### 示例 2——在完成时追加的响应式聊天记录

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

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

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

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

    private ConvaiTranscripts _transcripts;

    private void OnEnable()
    {
        _transcripts = ConvaiManager.ActiveManager?.Transcripts;
        if (_transcripts != null)
            _transcripts.Changed += OnTranscriptChanged;
    }

    private void OnDisable()
    {
        if (_transcripts != null)
            _transcripts.Changed -= OnTranscriptChanged;
    }

    private void OnTranscriptChanged(TranscriptUpdateBatch batch)
    {
        foreach (var turnId in batch.CompletedTurnIds)
        {
            if (!batch.Timeline.TurnsById.TryGetValue(turnId, out var turn)) continue;
            _log.text += $"\n<b>{turn.Participant.DisplayName}:</b> {turn.CommittedText}";
        }
    }
}
```

{% endcode %}

#### 示例 3——带历史回放的自定义实时聊天 UI

一次工业安全演练构建了完整的聊天 UI，在启用时回放所有已提交的转录历史——因此后加入的观众也能看到完整对话——然后监听实时变化。

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

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

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

    private ConvaiTranscripts _transcripts;

    private void OnEnable()
    {
        _transcripts = ConvaiManager.ActiveManager?.Transcripts;
        if (_transcripts == null) return;

        var history = _transcripts.GetTurns(new TranscriptQuery
        {
            IncludeActiveTurns    = false,
            IncludeCommittedTurns = true
        }).OrderBy(t => t.RoomSequence);

        _chatContent.text = string.Join("\n",
            history.Select(t => $"<b>{t.Participant.DisplayName}:</b> {t.CommittedText}"));

        _transcripts.Changed += OnChanged;
    }

    private void OnDisable()
    {
        if (_transcripts != null)
            _transcripts.Changed -= OnChanged;
    }

    private void OnChanged(TranscriptUpdateBatch batch)
    {
        foreach (var turn in batch.ChangedTurns)
        {
            if (turn.Lifecycle == TranscriptLifecycle.Streaming)
                UpdateLiveLine(turn.Participant.DisplayName, turn.DisplayText);
        }

        foreach (var turnId in batch.CompletedTurnIds)
        {
            if (batch.Timeline.TurnsById.TryGetValue(turnId, out var turn))
                CommitLine(turn.Participant.DisplayName, turn.CommittedText);
        }
    }

    private void UpdateLiveLine(string speaker, string text) =>
        Debug.Log($"[Live] {speaker}: {text}");

    private void CommitLine(string speaker, string text) =>
        _chatContent.text += $"\n<b>{speaker}:</b> {text}";
}
```

{% endcode %}

***

### 故障排查

| 症状                                        | 可能原因                                                     | 修复                                                                       |
| ----------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------ |
| `GetTurns()` 返回空列表                        | 尚不存在任何轮次，或者 `IncludeActiveTurns = false` 且当前所有轮次都仍在流式传输中 | 省略 `TranscriptQuery` 即可获取所有轮次，或者设置 `IncludeActiveTurns = true` 以包含进行中的轮次 |
| `已更改` 会触发，但 `batch.CompletedTurnIds` 为空   | 轮次仍然处于活动状态——该批次反映的是对流式轮次的更新，而不是完成                        | 检查 `batch.ChangedTurns` 用于活动轮次更新； `已完成回合 ID` 仅在轮次结束时填充                   |
| `TranscriptTurnSnapshot.CommittedText` 为空 | 轮次仍然 `流式传输` ——文本在轮次完成前不会被提交                              | 使用 `DisplayText` 获取进行中文本，或等待轮次出现在 `batch.CompletedTurnIds`               |
| `已更改` 连接后从不触发                             | 订阅太晚，或者 `Dispose()` 在连接之前调用                              | 订阅 `已更改` 在之前或紧接着之后 `ConnectAsync`；不要调用 `Dispose()` 直到组件被销毁               |

***

### 下一步

若要在不查询时间线的情况下进行事件驱动的转录响应，请使用 `ConvaiCharacterEventRelay` 或 `ConvaiTranscriptEventRelay` 监控这一点——参见 [角色事件](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/character-events.md)。如需完整的角色脚本 API，请参见 [Character & Player API](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/character-and-player-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:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/transcript-api.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
