文档实时转录

实时转录

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

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

实时转录没有单独的套餐,使用的分钟余额与其他功能相同。连接时间如何计算,见计费。

什么时候用实时转录

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

实时转录只接受原始PCM音频,不支持视频链接、上传文件、WebM、Opus或MP3。请在客户端采集音频,并先转换成PCM。在浏览器中采集音频演示了做法。

用SDK快速开始

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

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。

用原生WebSocket快速开始

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

创建会话

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:

{
  "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表示这个会话每开始一分钟扣除多少余额分钟,见计费。

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

字段默认值规则
languageautoauto,或实时转录支持的两位语言代码。
translate_to无两位语言代码。每个会话只能有一个目标语言。见实时翻译。
diarizefalse标注说话人。有些组合无法标注说话人,例如与language: "auto"一起使用,会返回422。请查看支持范围。
audio_formatpcm_s16le唯一的取值。保留该字段是为了以后可以增加格式。
sample_rate160008000到48000之间的整数,仅支持单声道。
savetrue设为false时,会话结束后不保留任何内容。见保存的转录。

连接并发送音频流

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

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。见关闭代码。

在浏览器中采集音频

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

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会在说话的同时翻译每一行。

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

可用的目标语言和语言组合取决于实时语音服务商。请读取支持范围,不要把规则写死。不支持的目标语言会返回422 unsupported_translation。例如,有的服务商要求明确指定language而不能用auto,或只允许包含英语的语言组合。

WebSocket协议

连接

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

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

如果会话已经有一个套接字,新的有效连接会替换旧连接。见恢复与接管。

客户端消息

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

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

请在session.started之后再开始发送音频。如果发送音频的速度快于实时,会按音频时长而不是连接时长计费(见计费),所以请按音频产生的速度发送。

服务器事件

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

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

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

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

line携带某一行的完整当前状态,请用它替换你手里相同id的那一行。见line对象。

{
  "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在每次扣费后发送,包含连接时长、音频时长、目前已扣除的余额分钟数,以及你剩余的分钟数:

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

warning带有code:

code含义
balance_low下一次扣费会失败。到期时会话会以balance_exhausted结束。开启自动充值时不会发送。
audio_dropped会话就绪之前就收到了音频,或语音服务商连接期间收到的音频过多。最早的音频已被丢弃。
idle_timeout_soon已经4分钟没有音频。连续5分钟没有音频,会话就会结束。继续发送音频即可保持会话。
{ "type": "warning", "code": "idle_timeout_soon" }

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

code是否致命含义
invalid_message否收到了不是finalize或end的文本帧。
invalid_token是令牌无效、已过期或已被使用。套接字以4001关闭。
not_found是会话不存在。套接字以4004关闭。
session_ended是会话已经结束。套接字以4004关闭。
{ "type": "error", "code": "invalid_message", "message": "Expected {\"type\":\"finalize\"} or {\"type\":\"end\"}" }

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

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

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

{
  "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_timeout5分钟没有音频。
max_duration会话已连接300分钟。请开始新的会话。
disconnected客户端断开超过2分钟仍未恢复。
upstream_error语音服务商出错。
internal_errortranscript1一侧出了问题。
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结束,所有扣费都会退回。因静音而为空的会话会照常计费,并保存为空。
  • 余额用完时,如果开启了自动充值,transcript1会在会话持续传输的同时从已保存的银行卡扣款,会话继续进行。如果自动充值关闭或已暂停,或扣款失败,会话会定稿、保存,并在整分钟边界以balance_exhausted结束。会先收到balance_low警告,开启自动充值时除外。

当你可用的余额低于按当前费率开始一分钟所需的量时,创建会话会返回402 insufficient_minutes。如果开启了自动充值,transcript1会先尝试充值。见计费与限额。

限额

FreeMonthlyAnnual
同时进行的实时会话123
  • 会话处于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。

{
  "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。

curl https://api.transcript1.com/v1/live/sessions/lv_8fK2Lm0PqRsTuVwXyZaBcD \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
{
  "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)。

错误

请求错误使用常见的错误信封。以下是POST /v1/live/sessions及相关接口返回的错误:

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

套接字上的问题会以error事件和关闭代码的形式出现。

保存的转录

save: true(默认)时,会话结束后会保存为一条普通转录。音频本身不会被保存。

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

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

安全

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

最后更新 2026年11月3日