# 实时转录

> 通过WebSocket把麦克风或标签页音频流式发送给transcript1，边说边收到字幕行，还可选实时翻译。

Canonical: https://transcript1.com/zh/docs/live
Language: zh-Hans
Last updated: 2026-11-03

实时转录会在人们还在说话时，把音频流转换成字幕行。客户端通过WebSocket发送原始PCM音频，transcript1返回字幕行：每一行先是临时结果，随后变为最终结果。加上`translate_to`后，每一行还会附带译文。会话结束后，结果会保存为一条普通的转录。

实时转录没有单独的套餐，使用的分钟余额与其他功能相同。连接时间如何计算，见[计费](https://transcript1.com/zh/docs/live#billing)。

## 什么时候用实时转录

| 适合用实时转录 | 适合用[普通转录](https://transcript1.com/zh/docs/getting-started) |
|---|---|
| 音频仍在产生：通话、演讲、直播，或你正在看的视频。 | 你有一个链接或一个已完成的文件。 |
| 你需要在说话后一两秒内看到字幕。 | 你要最高的准确率，并且可以等待结果。 |
| 你希望每一行旁边都有实时翻译。 | 你要翻译一份已完成的转录文本，或翻译成多种语言。 |

实时转录只接受原始PCM音频，不支持视频链接、上传文件、WebM、Opus或MP3。请在客户端采集音频，并先转换成PCM。[在浏览器中采集音频](https://transcript1.com/zh/docs/live#capture-audio-in-a-browser)演示了做法。

## 用SDK快速开始

请安装SDK 1.5.0或更新版本：`npm i @transcript1/sdk@latest`。`t1.live.start`会创建会话并连接，会话启动后才返回，所以可以立即发送音频。

```javascript
import { createReadStream } from 'node:fs';
import { setTimeout as sleep } from 'node:timers/promises';
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1(); // reads TRANSCRIPT1_API_KEY

const live = await t1.live.start({ language: 'en', translateTo: 'es', sampleRate: 16000 });

live.on('line', line => {
  const mark = line.final ? 'final  ' : 'partial';
  console.log(mark, line.text, '→', line.translation?.text ?? '');
});
live.on('warning', warning => console.warn(warning.code));

// speech.pcm: 16 kHz, mono, signed 16-bit little-endian, no header
for await (const chunk of createReadStream('./speech.pcm', { highWaterMark: 3200 })) {
  live.send(chunk);
  await sleep(100); // 3,200 bytes are 100 ms of audio: send at real-time speed
}

const result = await live.end();
console.log(result.reason, result.transcriptionId, result.minutesCharged);
```

行会就地更新。收到相同`id`的行时，替换你已有的那一行，也可以直接从`live.lines`读取全部行。`end()`会结束会话，等待最后几行，并保存转录文本。所有方法和事件见[Node.js SDK](https://transcript1.com/zh/docs/sdk#live-transcription)。

## 用原生WebSocket快速开始

不用SDK时分两步：先通过HTTPS创建会话，再用返回的一次性令牌打开WebSocket。

### 创建会话

```bash
curl -X POST https://api.transcript1.com/v1/live/sessions \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"language": "en", "translate_to": "es", "sample_rate": 16000}'
```

API返回`201`：

```json
{
  "id": "lv_8fK2Lm0PqRsTuVwXyZaBcD",
  "status": "waiting",
  "transcription_id": "tr_3NcQvXy7BtHjKd5ErFgLmA",
  "websocket_url": "wss://api.transcript1.com/v1/live/sessions/lv_8fK2Lm0PqRsTuVwXyZaBcD/stream",
  "token": "Zk3x...",
  "token_expires_at": "2026-11-02T12:01:00Z",
  "rate": 1
}
```

当`save`为`false`时，`transcription_id`为`null`。令牌只能使用一次，60秒后过期，所以请立即连接。`rate`表示这个会话每开始一分钟扣除多少余额分钟，见[计费](https://transcript1.com/zh/docs/live#billing)。

请求体支持以下字段，全部可选，所需权限范围为`transcriptions:write`：

| 字段 | 默认值 | 规则 |
|---|---|---|
| `language` | `auto` | `auto`，或实时转录支持的两位语言代码。 |
| `translate_to` | 无 | 两位语言代码。每个会话只能有一个目标语言。见[实时翻译](https://transcript1.com/zh/docs/live#translate-live)。 |
| `diarize` | `false` | 标注说话人。有些组合无法标注说话人，例如与`language: "auto"`一起使用，会返回`422`。请查看[支持范围](https://transcript1.com/zh/docs/live#capabilities)。 |
| `audio_format` | `pcm_s16le` | 唯一的取值。保留该字段是为了以后可以增加格式。 |
| `sample_rate` | `16000` | 8000到48000之间的整数，仅支持单声道。 |
| `save` | `true` | 设为`false`时，会话结束后不保留任何内容。见[保存的转录](https://transcript1.com/zh/docs/live#saved-transcripts)。 |

### 连接并发送音频流

把令牌作为第二个子协议来打开WebSocket。在浏览器和Node 22及以上版本中无需自定义请求头即可使用。请等收到`session.started`后再发送音频：更早发送的二进制帧会被丢弃，并收到`audio_dropped`警告。

```javascript
import { createReadStream } from 'node:fs';
import { setTimeout as sleep } from 'node:timers/promises';

const session = await fetch('https://api.transcript1.com/v1/live/sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TRANSCRIPT1_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ language: 'en', translate_to: 'es', sample_rate: 16000 }),
}).then(response => response.json());

const ws = new WebSocket(session.websocket_url, ['t1-live', session.token]);
const lines = new Map();

ws.addEventListener('message', async message => {
  const event = JSON.parse(message.data);

  if (event.type === 'session.started') {
    for await (const chunk of createReadStream('./speech.pcm', { highWaterMark: 3200 })) {
      ws.send(chunk);
      await sleep(100);
    }
    ws.send(JSON.stringify({ type: 'end' }));
  } else if (event.type === 'line') {
    lines.set(event.line.id, event.line); // the full current state: replace by id
    console.log(event.line.final, event.line.text, event.line.translation?.text);
  } else if (event.type === 'session.ended') {
    console.log('ended:', event.reason, event.minutes_charged);
  }
});

ws.addEventListener('close', event => console.log('closed', event.code));
```

Node 20没有内置的`WebSocket`，请使用`ws`包，它支持相同的`new WebSocket(url, protocols)`写法。

令牌有效时，服务器只会回应`t1-live`子协议。令牌无效也不会让握手失败：套接字会先打开，你会收到一个`error`事件，随后套接字以4xxx代码关闭，因此浏览器里能看到真正的原因，而不是笼统的1006。见[关闭代码](https://transcript1.com/zh/docs/live#close-codes)。

## 在浏览器中采集音频

实时音频必须是16位有符号小端序的单声道PCM，采样率与会话的`sample_rate`一致。浏览器给出的是浮点采样，需要在`AudioWorklet`中转换。下面的代码以16 kHz采集麦克风，每100 ms一块，并通过浏览器版SDK入口发送。`getAccessToken`返回OAuth访问令牌，切勿把API密钥放进浏览器。见[安全](https://transcript1.com/zh/docs/live#security)。

```javascript
import { createLiveClient } from '@transcript1/sdk/live';

const workletSource = `
class Pcm16Processor extends AudioWorkletProcessor {
  constructor() {
    super();
    this.buffer = new Int16Array(1600); // 100 ms at 16 kHz
    this.filled = 0;
  }

  process(inputs) {
    const channel = inputs[0][0];
    if (!channel) return true;
    for (let i = 0; i < channel.length; i++) {
      const sample = Math.max(-1, Math.min(1, channel[i]));
      this.buffer[this.filled++] = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
      if (this.filled === this.buffer.length) {
        this.port.postMessage(this.buffer.slice());
        this.filled = 0;
      }
    }
    return true;
  }
}
registerProcessor('pcm16', Pcm16Processor);
`;

const client = createLiveClient({ getAccessToken: () => myApp.getOAuthAccessToken() });

const stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1 } });
const context = new AudioContext({ sampleRate: 16000 });
const moduleUrl = URL.createObjectURL(new Blob([workletSource], { type: 'text/javascript' }));
await context.audioWorklet.addModule(moduleUrl);

// the context's real rate: a browser that cannot do 16 kHz picks its own
const live = await client.start({ language: 'auto', sampleRate: context.sampleRate });
live.on('line', renderLine);

const microphone = context.createMediaStreamSource(stream);
const capture = new AudioWorkletNode(context, 'pcm16');
capture.port.onmessage = event => live.send(event.data); // an Int16Array
const mute = context.createGain();
mute.gain.value = 0;
microphone.connect(capture).connect(mute).connect(context.destination);

async function stop() {
  stream.getTracks().forEach(track => track.stop());
  await context.close();
  return live.end();
}
```

这个worklet是按16 kHz编写的。如果上下文以其他采样率运行，每块就不再是100 ms，但由于`sampleRate`与上下文一致，PCM数据仍然正确。如果要采集标签页或视频的声音而不是麦克风，请用`getDisplayMedia`或媒体元素的`captureStream()`创建`MediaStream`，再用同样的方式连接。

## 实时翻译

在会话中加上`translate_to`，transcript1会在说话的同时翻译每一行。

- 每个会话只有一个目标语言。之后可以用[翻译接口](https://transcript1.com/zh/docs/live#saved-transcripts)为保存的转录文本增加更多语言。
- 每个`line`都包含原文（`text`、`final`）和一个`translation`对象（`language`、`text`、`final`）。未设置`translate_to`时，`translation`为`null`。
- 原文和译文各自独立定稿：一行的原文可能已是最终结果，译文却仍在变化。等两者都定稿后，再把这一行显示为最终状态。
- 已经是目标语言的行，会把自己的文本当作译文，并在这一行关闭时定稿。
- 一行会在以下情况关闭：说话人停顿、说话人切换、词间停顿超过0.9秒、该行达到两行字幕（84个字符）或15秒，或者你发送了`finalize`。开启翻译后，一行可能要多等约3秒才会关闭，以等待译文送达。此时原文定稿后，译文可能仍保持未定稿（`final: false`）并继续增长。
- 翻译免费，不消耗分钟。

可用的目标语言和语言组合取决于实时语音服务商。请读取[支持范围](https://transcript1.com/zh/docs/live#capabilities)，不要把规则写死。不支持的目标语言会返回`422 unsupported_translation`。例如，有的服务商要求明确指定`language`而不能用`auto`，或只允许包含英语的语言组合。

## WebSocket协议

### 连接

```javascript
new WebSocket(websocket_url, ['t1-live', token]);
```

令牌通过WebSocket子协议传递，不会出现在URL或访问日志中。不要发送`Authorization`请求头，令牌就是凭据。服务器每30秒发送一次ping，浏览器和`ws`包会自动回应。60秒内没有pong的套接字会被断开。

如果会话已经有一个套接字，新的有效连接会替换旧连接。见[恢复与接管](https://transcript1.com/zh/docs/live#resume-and-takeover)。

### 客户端消息

| 消息 | 含义 |
|---|---|
| 二进制帧 | 音频：与会话`sample_rate`一致的PCM s16le单声道。每帧长度为偶数字节，最大256 KB。每帧约100 ms比较合适。 |
| `{"type":"finalize"}` | 立即关闭当前行，例如视频暂停时。 |
| `{"type":"end"}` | 结束并保存会话。服务器回复`session.ended`并关闭套接字。 |
| 其他任何内容 | 收到一个不致命的`error`事件，代码为`invalid_message`。 |

长度为奇数或过大的帧属于协议违规，套接字会以`4008`关闭。

请在`session.started`之后再开始发送音频。如果发送音频的速度快于实时，会按音频时长而不是连接时长计费（见[计费](https://transcript1.com/zh/docs/live#billing)），所以请按音频产生的速度发送。

### 服务器事件

每个事件都是带有`type`的JSON文本帧。

| 事件 | 发送时机 |
|---|---|
| `session.started` | 连接已被接受。从现在起可以发送音频。 |
| `line` | 一行字幕被创建或发生变化。 |
| `usage` | 每次扣费之后。 |
| `warning` | 需要注意的情况，但会话继续。 |
| `error` | 不致命的问题，或立即关闭连接的原因。 |
| `reconnect` | 服务器即将重启，请恢复会话。 |
| `session.ended` | 会话已结束并保存。正常关闭前的最后一个事件。 |

`session.started`会说明这是否是一次恢复，以及会话已经处理了多少音频：

```json
{
  "type": "session.started",
  "session_id": "lv_8fK2Lm0PqRsTuVwXyZaBcD",
  "transcription_id": "tr_3NcQvXy7BtHjKd5ErFgLmA",
  "resumed": false,
  "offset_ms": 0
}
```

`line`携带某一行的完整当前状态，请用它替换你手里相同`id`的那一行。见[line对象](https://transcript1.com/zh/docs/live#the-line-object)。

```json
{
  "type": "line",
  "line": {
    "id": 12,
    "start_ms": 53120,
    "end_ms": 56040,
    "speaker": null,
    "language": "en",
    "text": "Welcome back to the stream",
    "final": true,
    "translation": { "language": "es", "text": "Bienvenidos de nuevo", "final": false }
  }
}
```

`usage`在每次扣费后发送，包含连接时长、音频时长、目前已扣除的余额分钟数，以及你剩余的分钟数：

```json
{
  "type": "usage",
  "connected_ms": 61240,
  "audio_ms": 60100,
  "minutes_charged": 2,
  "minutes_remaining": 898
}
```

`warning`带有`code`：

| `code` | 含义 |
|---|---|
| `balance_low` | 下一次扣费会失败。到期时会话会以`balance_exhausted`结束。开启[自动充值](https://transcript1.com/zh/docs/billing-and-limits#auto-top-up)时不会发送。 |
| `audio_dropped` | 会话就绪之前就收到了音频，或语音服务商连接期间收到的音频过多。最早的音频已被丢弃。 |
| `idle_timeout_soon` | 已经4分钟没有音频。连续5分钟没有音频，会话就会结束。继续发送音频即可保持会话。 |

```json
{ "type": "warning", "code": "idle_timeout_soon" }
```

`error`带有`code`和`message`。实时套接字上可能出现这些代码：

| `code` | 是否致命 | 含义 |
|---|---|---|
| `invalid_message` | 否 | 收到了不是`finalize`或`end`的文本帧。 |
| `invalid_token` | 是 | 令牌无效、已过期或已被使用。套接字以`4001`关闭。 |
| `not_found` | 是 | 会话不存在。套接字以`4004`关闭。 |
| `session_ended` | 是 | 会话已经结束。套接字以`4004`关闭。 |

```json
{ "type": "error", "code": "invalid_message", "message": "Expected {\"type\":\"finalize\"} or {\"type\":\"end\"}" }
```

`reconnect`会在服务器以`1012`关闭套接字之前到达，例如部署期间。请获取新令牌并恢复会话。

```json
{ "type": "reconnect", "reason": "server_restart" }
```

`session.ended`是每个会话的最后一个事件，其中的数字是最终合计：

```json
{
  "type": "session.ended",
  "reason": "client_ended",
  "transcription_id": "tr_3NcQvXy7BtHjKd5ErFgLmA",
  "minutes_charged": 3,
  "connected_ms": 142500,
  "audio_ms": 141900
}
```

### line对象

| 字段 | 含义 |
|---|---|
| `id` | 从0开始的编号，每多一行加1。它也是保存后转录文本中的片段索引。 |
| `start_ms`、`end_ms` | 以会话音频时间表示的起止时间。这个时间在重连后会继续累计。 |
| `speaker` | 开启`diarize`时为说话人标签，否则为`null`。标签与普通转录文本相同。 |
| `language` | 这一行检测到的或指定的语言，尚未确定时为`null`。 |
| `text` | 目前为止的原文。 |
| `final` | 原文不再变化时为`true`。 |
| `translation` | 没有`translate_to`时为`null`，否则包含`language`、`text`和`final`。 |

`final: false`的行文本还可能变化，请用较淡的样式显示。`final: true`的行文本不会再变，最终行也不会被删除。

SDK的行字段使用`camelCase`：`startMs`、`endMs`。

### 结束原因

`session.ended`和`GET /v1/live/sessions/:id`会给出`end_reason`：

| 原因 | 含义 |
|---|---|
| `client_ended` | 客户端发送了`end`。 |
| `balance_exhausted` | 下一次扣费失败。会话在整分钟的边界结束并保存。 |
| `idle_timeout` | 5分钟没有音频。 |
| `max_duration` | 会话已连接300分钟。请开始新的会话。 |
| `disconnected` | 客户端断开超过2分钟仍未恢复。 |
| `upstream_error` | 语音服务商出错。 |
| `internal_error` | transcript1一侧出了问题。 |
| `not_connected` | 会话在5分钟内从未连接。不会计费，只能通过`GET`看到。 |

### 关闭代码

| 代码 | 含义 |
|---|---|
| `1000` | 正常关闭。总是出现在`session.ended`之后。 |
| `1011` | 服务器意外错误。服务器会恢复会话，请用新令牌恢复。 |
| `1012` | 服务器重启。出现在`reconnect`之后，请用新令牌恢复。 |
| `4001` | 令牌无效、已过期或已被使用。 |
| `4004` | 找不到会话，或会话已经结束。 |
| `4008` | 协议违规：二进制帧长度为奇数或过大。 |
| `4010` | 被同一会话的更新连接取代。 |

API重启期间，新的WebSocket请求可能收到HTTP `503`。请稍后重试。

## 恢复与接管

会话可以经受网络中断、API部署和崩溃。断开后的2分钟内，会话会等你回来；超过这个时间，会话会以`disconnected`结束并保存。

1. 调用`POST /v1/live/sessions/:id/token`。响应是`token`、`token_expires_at`和`websocket_url`，字段与创建会话时相同。如果会话已经结束，会返回`409 session_ended`。
2. 用这个令牌打开新的WebSocket。
3. 收到`resumed: true`的`session.started`。`offset_ms`是会话已处理的音频量：请从这个位置继续发送音频。行的id也会接着往下编。

已经定稿的行会保留。断开瞬间正在进行的末尾部分会丢失，所以如果你缓存了音频，请从`offset_ms`起重放几秒音频。重连期间的空档不计费。

接管是同一个调用。即使服务器认为还有一个套接字处于打开状态，新的有效连接也会替换它，旧套接字在其行保存后以`4010`关闭。恢复不会受并发会话数限制的阻挡。

SDK会替你完成这一切：连接断开或收到`reconnect`事件时，它会获取令牌，并以0.5到8秒的退避间隔重连，最长持续2分钟，同时重放空档期间发送的最后10秒音频。

## 计费

实时转录与批量转录使用同一份分钟余额。

- **按连接时间计费**，从套接字被接受开始，到关闭为止。静音和暂停的视频都算在内。断开到恢复之间的时间不算。
- **按开始的分钟计，最少1分钟。** 第一次连接被接受时扣第一分钟，之后每一分钟在开始时扣费。
- **快于实时按音频时长计费。** 如果你发送的音频比连接时间更长，就按音频时长计费。
- **费率从API读取。** 开始的每一分钟扣除`rate`个余额分钟，向上取整。费率通常是1，也可能带小数。实时费率可从`GET /v1/account`的`rates.live`、`GET /v1/live/capabilities`的`rate`，以及你创建的会话中的`rate`读取。会话的费率在创建时就固定了。
- **翻译免费。**
- **从未连接则不计费。** 5分钟内没有连接的会话会以`not_connected`结束。
- **失败会退款。** 如果会话在产生任何一行之前就以`upstream_error`或`internal_error`结束，所有扣费都会退回。因静音而为空的会话会照常计费，并保存为空。
- **余额用完时**，如果开启了[自动充值](https://transcript1.com/zh/docs/billing-and-limits#auto-top-up)，transcript1会在会话持续传输的同时从已保存的银行卡扣款，会话继续进行。如果自动充值关闭或已暂停，或扣款失败，会话会定稿、保存，并在整分钟边界以`balance_exhausted`结束。会先收到`balance_low`警告，开启自动充值时除外。

当你可用的余额低于按当前费率开始一分钟所需的量时，创建会话会返回`402 insufficient_minutes`。如果开启了自动充值，transcript1会先尝试充值。见[计费与限额](https://transcript1.com/zh/docs/billing-and-limits#live-transcription)。

## 限额

| | Free | Monthly | Annual |
|---|---|---|---|
| 同时进行的实时会话 | 1 | 2 | 3 |

- 会话处于`waiting`、`connected`或`disconnected`（在2分钟窗口内）时都会计数。超出限额时，`POST /v1/live/sessions`返回`429 too_many_live_sessions`，并带有`Retry-After: 30`。
- 实时会话不计入活跃任务上限。
- 一个会话最多连接300分钟，之后会以`max_duration`结束。
- 5分钟没有音频的会话会以`idle_timeout`结束；4分钟后你会收到`idle_timeout_soon`。暂停的音频流仍会计连接时间，并被视为空闲，所以暂停很久时请结束会话。
- transcript1对所有实时会话的总数也有上限。全部占满时，`POST /v1/live/sessions`返回`503 live_capacity`，这个错误可以重试。请等待`Retry-After`（30秒）后再试。这个检查只在创建会话时进行，不会在会话进行中触发。
- 创建会话和获取令牌都计入每分钟请求数。
- 音频只能是原始PCM：`pcm_s16le`，单声道，8000到48000 Hz。

## 支持范围

`GET /v1/live/capabilities`说明实时转录当前支持什么，客户端因此不必写死服务商的规则。所需权限范围为`transcriptions:read`。

```json
{
  "available": true,
  "auto_language": true,
  "languages": ["en", "es"],
  "translation": {
    "targets": ["es", "de"],
    "pairs": "any",
    "requires_language": false
  },
  "diarize": true,
  "audio_formats": ["pcm_s16le"],
  "sample_rate": { "min": 8000, "max": 48000 },
  "max_session_minutes": 300,
  "rate": 1
}
```

这里的列表做了缩减，请从API读取完整内容。

- `translation.pairs`为`"any"`（任何检测到的语言可翻译成任何目标语言），或是由`from`和`to`组成的对象列表。
- `translation.requires_language`为`true`时，翻译需要明确指定`language`，不能用`auto`。
- 实时转录未配置时，响应是只含`rate`的`available: false`，创建会话会返回`503 live_unavailable`。

使用SDK：`await t1.live.capabilities()`。

## 查询会话

`GET /v1/live/sessions/:id`返回会话的状态，所需权限范围为`transcriptions:read`。

```bash
curl https://api.transcript1.com/v1/live/sessions/lv_8fK2Lm0PqRsTuVwXyZaBcD \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```json
{
  "id": "lv_8fK2Lm0PqRsTuVwXyZaBcD",
  "status": "ended",
  "end_reason": "client_ended",
  "transcription_id": "tr_3NcQvXy7BtHjKd5ErFgLmA",
  "options": {
    "language": "en",
    "translate_to": "es",
    "diarize": false,
    "audio_format": "pcm_s16le",
    "sample_rate": 16000,
    "save": true
  },
  "connected_ms": 142500,
  "audio_ms": 141900,
  "minutes_charged": 3,
  "rate": 1,
  "created_at": "2026-11-02T11:58:12Z",
  "ended_at": "2026-11-02T12:00:35Z"
}
```

`status`为`waiting`、`connected`、`disconnected`或`ended`。会话结束前，`end_reason`为`null`。使用SDK：`await t1.live.get(id)`。

## 错误

请求错误使用常见的[错误信封](https://transcript1.com/zh/docs/errors)。以下是`POST /v1/live/sessions`及相关接口返回的错误：

| HTTP状态码 | `code` | 可重试 | 触发场景 |
|---|---|---|---|
| 422 | `invalid_request` | 否 | 字段缺失或无效，例如`sample_rate`不在8000到48000范围内。 |
| 422 | `unsupported_language` | 否 | 实时转录不支持该`language`。 |
| 422 | `unsupported_translation` | 否 | 不支持该目标语言或语言组合。 |
| 402 | `insufficient_minutes` | 否 | 你可用的余额低于按当前费率开始一分钟所需的量。 |
| 403 | `email_not_verified` | 否 | 请先验证邮箱。 |
| 429 | `too_many_live_sessions` | 是 | 你的套餐允许的同时实时会话已用满。请等待`Retry-After`。 |
| 503 | `live_capacity` | 是 | 实时转录的容量已全部占用。请等待`Retry-After`（30秒）。 |
| 503 | `live_unavailable` | 否 | 实时转录暂时不可用。 |
| 409 | `session_ended` | 否 | 对已结束的会话调用了`POST /v1/live/sessions/:id/token`。 |

套接字上的问题会以`error`事件和[关闭代码](https://transcript1.com/zh/docs/live#close-codes)的形式出现。

## 保存的转录

`save: true`（默认）时，会话结束后会保存为一条普通转录。音频本身不会被保存。

- 创建会话时就会返回`transcription_id`。会话进行期间它的状态是`live`，结束后是`completed`。从未连接的会话会留下一条`canceled`的转录。
- 该转录的`source`为`{ "platform": "live", "session_id": "lv_…" }`，没有链接或文件。控制台会把它列为实时会话。
- 每一行成为一个片段，索引与行的`id`相同。你可以像读取其他转录一样，以JSON、文本、SRT或VTT读取，见[API参考](https://transcript1.com/zh/docs/api-reference)。
- 实时翻译也会一并保存，可用`translation`参数读取，详见[翻译](https://transcript1.com/zh/docs/translation#read-the-translation)。
- 转录变为`completed`之后，可以再用`POST /v1/transcriptions/:id/translations`增加更多翻译语言，见[翻译](https://transcript1.com/zh/docs/translation#translate-an-existing-transcript)。

`save: false`时，会话结束后不会保留任何内容：没有转录，`transcription_id`为`null`。你仍会通过套接字收到每一行，并照常计费。

## 安全

- 创建会话时拿到的令牌只能使用一次，60秒后过期。它通过WebSocket子协议传递，而不是放在URL中，因此不会出现在访问日志里。泄露的令牌一旦用过或过期就失效了。
- API密钥只能在你自己控制的服务器上使用，绝不要把API密钥放进网页或浏览器扩展。
- 在浏览器或扩展中，请通过OAuth让用户登录，并把OAuth访问令牌传给`createLiveClient`的`getAccessToken`。令牌需要`transcriptions:write`权限范围才能开始会话。OAuth访问令牌1小时后过期，每次恢复都会向`getAccessToken`获取新的令牌。
- 你的服务器也可以用API密钥创建会话，只把`websocket_url`和`token`交给浏览器，由客户端直接连接。这种情况下，没有你提供的新令牌，客户端无法恢复会话。
