# Chuyển thành văn bản trực tiếp

> Gửi âm thanh từ micro hoặc tab đến transcript1 qua WebSocket và nhận các dòng phụ đề, kèm bản dịch theo thời gian thực nếu muốn, ngay khi mọi người đang nói.

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

Chuyển thành văn bản trực tiếp biến luồng âm thanh thành các dòng phụ đề ngay khi mọi người còn đang nói. Client gửi âm thanh PCM thô qua WebSocket, còn transcript1 trả về các dòng: lúc đầu là bản nhận dạng tạm, sau đó thành bản cuối. Thêm `translate_to` và mỗi dòng sẽ có kèm bản dịch. Khi phiên kết thúc, kết quả được lưu thành một transcription bình thường.

Không có gói riêng cho tính năng trực tiếp: nó dùng chung số dư phút với mọi tính năng khác. Xem [Tính phí](https://transcript1.com/vi/docs/live#billing) để biết thời gian kết nối được tính thế nào.

## Khi nào nên dùng chế độ trực tiếp

| Dùng chế độ trực tiếp khi | Dùng [chuyển thành văn bản thông thường](https://transcript1.com/vi/docs/getting-started) khi |
|---|---|
| Âm thanh vẫn đang được tạo ra: cuộc gọi, buổi nói chuyện, buổi phát trực tiếp, video bạn đang xem. | Bạn có sẵn một đường dẫn hoặc một file đã hoàn chỉnh. |
| Bạn cần phụ đề trong vòng một hai giây sau lời nói. | Bạn cần độ chính xác cao nhất và có thể chờ kết quả. |
| Bạn muốn có bản dịch theo thời gian thực bên cạnh từng dòng. | Bạn muốn dịch một transcript đã hoàn tất, hoặc dịch sang nhiều ngôn ngữ. |

Chế độ trực tiếp chỉ nhận âm thanh dạng PCM thô. Nó không nhận link video, file tải lên, WebM, Opus hay MP3. Hãy ghi âm thanh ở phía client và chuyển sang PCM trước. Mục [Ghi âm thanh trong trình duyệt](https://transcript1.com/vi/docs/live#capture-audio-in-a-browser) hướng dẫn cách làm.

## Bắt đầu nhanh với SDK

Cài SDK 1.5.0 trở lên: `npm i @transcript1/sdk@latest`. `t1.live.start` tạo phiên, kết nối và hoàn tất khi phiên đã bắt đầu, nên bạn có thể gửi âm thanh ngay.

```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);
```

Các dòng được cập nhật tại chỗ. Khi một dòng có cùng `id` đến, hãy thay dòng bạn đang giữ, hoặc đọc tất cả từ `live.lines`. `end()` kết thúc phiên, chờ các dòng cuối và lưu transcript. Xem [SDK Node.js](https://transcript1.com/vi/docs/sdk#live-transcription) để biết đủ phương thức và sự kiện.

## Bắt đầu nhanh với WebSocket thuần

Không dùng SDK thì có hai bước: tạo phiên qua HTTPS, rồi mở WebSocket bằng token dùng một lần được trả về.

### Tạo phiên

```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 trả về `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
}
```

`transcription_id` là `null` khi `save` là `false`. Token chỉ dùng được một lần và hết hạn sau 60 giây, nên hãy kết nối ngay. `rate` là số phút số dư bị tính cho mỗi phút đã bắt đầu của phiên này; xem [Tính phí](https://transcript1.com/vi/docs/live#billing).

Body của request nhận các trường sau. Tất cả đều không bắt buộc, và quyền cần có là `transcriptions:write`:

| Trường | Mặc định | Quy tắc |
|---|---|---|
| `language` | `auto` | `auto`, hoặc mã hai chữ cái mà chế độ trực tiếp hỗ trợ. |
| `translate_to` | không có | Mã hai chữ cái. Mỗi phiên chỉ có một ngôn ngữ đích. Xem [Dịch trực tiếp](https://transcript1.com/vi/docs/live#translate-live). |
| `diarize` | `false` | Gắn nhãn người nói. Một số cấu hình không gắn nhãn được, ví dụ khi dùng cùng `language: "auto"`, và sẽ trả về `422`. Hãy kiểm tra [khả năng hỗ trợ](https://transcript1.com/vi/docs/live#capabilities). |
| `audio_format` | `pcm_s16le` | Giá trị duy nhất. Trường này có để sau này thêm được định dạng khác. |
| `sample_rate` | `16000` | Số nguyên từ 8000 đến 48000. Chỉ hỗ trợ mono. |
| `save` | `true` | `false` sẽ không lưu gì sau khi phiên kết thúc. Xem [Transcript đã lưu](https://transcript1.com/vi/docs/live#saved-transcripts). |

### Kết nối và truyền dữ liệu

Mở WebSocket với token ở vị trí subprotocol thứ hai. Cách này dùng được trong trình duyệt và trong Node 22+ mà không cần header tùy chỉnh. Hãy chờ `session.started` rồi mới gửi âm thanh: các frame nhị phân gửi sớm hơn sẽ bị bỏ, và bạn nhận cảnh báo `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 không có `WebSocket` tích hợp sẵn: hãy dùng gói `ws`, có cùng dạng `new WebSocket(url, protocols)`.

Với token hợp lệ, server chỉ trả lời bằng subprotocol `t1-live`. Token sai không bao giờ làm hỏng bước handshake: socket vẫn mở, bạn nhận sự kiện `error`, rồi socket đóng với mã 4xxx, nhờ đó trình duyệt hiển thị lý do thật thay vì mã 1006 chung chung. Xem [Mã đóng](https://transcript1.com/vi/docs/live#close-codes).

## Ghi âm thanh trong trình duyệt

Âm thanh cho chế độ trực tiếp phải là PCM mono 16-bit có dấu, little-endian, theo `sample_rate` của phiên. Trình duyệt cung cấp mẫu dạng số thực, vì vậy hãy chuyển đổi trong một `AudioWorklet`. Đoạn mã dưới đây ghi âm micro thành PCM 16 kHz theo từng khối 100 ms và gửi đi bằng entry SDK dành cho trình duyệt. `getAccessToken` trả về OAuth access token; đừng bao giờ đặt khóa API trong trình duyệt. Xem [Bảo mật](https://transcript1.com/vi/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 được viết cho 16 kHz. Nếu context chạy ở tần số khác, mỗi khối không còn là 100 ms, nhưng PCM vẫn đúng vì `sampleRate` khớp với context. Để ghi âm thanh của một tab hoặc video thay vì micro, hãy tạo `MediaStream` bằng `getDisplayMedia` hoặc từ `captureStream()` của một phần tử media, rồi nối vào theo cách tương tự.

## Dịch trực tiếp

Thêm `translate_to` vào phiên và transcript1 sẽ dịch từng dòng ngay khi được nói.

- Mỗi phiên chỉ có một ngôn ngữ đích. Có thể thêm ngôn ngữ khác vào transcript đã lưu sau đó bằng [endpoint dịch](https://transcript1.com/vi/docs/live#saved-transcripts).
- Mỗi `line` có bản gốc (`text`, `final`) và một object `translation` (`language`, `text`, `final`). `translation` là `null` khi không đặt `translate_to`.
- Bản gốc và bản dịch được chốt độc lập với nhau: một dòng có thể đã chốt bản gốc trong khi bản dịch vẫn đang thay đổi. Hãy hiển thị dòng là đã chốt khi cả hai đều đã chốt.
- Dòng vốn đã ở ngôn ngữ đích sẽ dùng chính văn bản của nó làm bản dịch, và được chốt khi dòng đóng lại.
- Một dòng đóng lại khi người nói ngắt quãng, đổi người nói, khoảng nghỉ giữa các từ dài hơn 0.9 giây, dòng đạt hai hàng phụ đề (84 ký tự) hoặc 15 giây, hoặc bạn gửi `finalize`. Khi bật dịch, một dòng có thể đóng chậm hơn khoảng 3 giây trong lúc chờ bản dịch. Khi đó bản dịch có thể vẫn mở (`final: false`) và tiếp tục dài thêm sau khi bản gốc đã chốt.
- Dịch miễn phí. Không tốn phút.

Những ngôn ngữ đích và cặp ngôn ngữ nào dùng được tùy vào nhà cung cấp nhận dạng giọng nói trực tiếp. Hãy đọc [khả năng hỗ trợ](https://transcript1.com/vi/docs/live#capabilities) thay vì viết cứng các quy tắc vào code. Ngôn ngữ đích không được hỗ trợ sẽ trả về `422 unsupported_translation`. Ví dụ, nhà cung cấp có thể yêu cầu `language` cụ thể thay vì `auto`, hoặc chỉ cho phép các cặp có tiếng Anh.

## Giao thức WebSocket

### Kết nối

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

Token đi kèm dưới dạng subprotocol của WebSocket. Nó không bao giờ xuất hiện trong URL hay log truy cập. Đừng gửi header `Authorization`: token chính là thông tin xác thực. Server gửi ping mỗi 30 giây; trình duyệt và gói `ws` tự động trả lời. Socket không gửi pong trong 60 giây sẽ bị ngắt.

Một kết nối hợp lệ vào phiên đã có socket sẽ thay thế socket cũ. Xem [Tiếp tục và tiếp quản](https://transcript1.com/vi/docs/live#resume-and-takeover).

### Thông điệp từ client

| Thông điệp | Ý nghĩa |
|---|---|
| Frame nhị phân | Âm thanh: PCM s16le mono theo `sample_rate` của phiên. Mỗi frame có độ dài chẵn và tối đa 256 KB. Khoảng 100 ms mỗi frame là kích thước phù hợp. |
| `{"type":"finalize"}` | Đóng dòng hiện tại ngay, ví dụ khi video bị tạm dừng. |
| `{"type":"end"}` | Kết thúc phiên và lưu lại. Server trả lời `session.ended` rồi đóng socket. |
| Bất cứ thứ gì khác | Sự kiện `error` không nghiêm trọng với mã `invalid_message`. |

Frame có độ dài lẻ hoặc quá lớn là vi phạm giao thức và làm socket đóng với mã `4008`.

Hãy bắt đầu gửi âm thanh sau `session.started`. Client gửi âm thanh nhanh hơn thời gian thực sẽ bị tính theo thời lượng âm thanh thay vì thời gian kết nối (xem [Tính phí](https://transcript1.com/vi/docs/live#billing)), vì vậy hãy gửi theo đúng tốc độ âm thanh được tạo ra.

### Sự kiện từ server

Mỗi sự kiện là một frame văn bản JSON có `type`.

| Sự kiện | Được gửi khi |
|---|---|
| `session.started` | Kết nối được chấp nhận. Từ đây hãy gửi âm thanh. |
| `line` | Một dòng phụ đề được tạo hoặc thay đổi. |
| `usage` | Sau mỗi lần tính phí. |
| `warning` | Có điều cần lưu ý nhưng phiên vẫn tiếp tục. |
| `error` | Một sự cố không nghiêm trọng, hoặc lý do dẫn đến đóng ngay. |
| `reconnect` | Server sắp khởi động lại. Hãy tiếp tục phiên. |
| `session.ended` | Phiên đã kết thúc và được lưu. Sự kiện cuối cùng trước khi đóng bình thường. |

`session.started` cho biết đây có phải lần tiếp tục không và phiên đã xử lý bao nhiêu âm thanh:

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

`line` mang toàn bộ trạng thái hiện tại của một dòng, vì vậy hãy thay dòng bạn đang giữ có cùng `id`. Xem [Object dòng](https://transcript1.com/vi/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` được gửi sau mỗi lần tính phí, kèm thời gian kết nối, thời lượng âm thanh, số phút số dư đã bị tính cho đến lúc đó và số phút còn lại:

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

`warning` có một `code`:

| `code` | Ý nghĩa |
|---|---|
| `balance_low` | Lần tính phí tiếp theo sẽ thất bại. Phiên kết thúc với `balance_exhausted` khi đến hạn. Không được gửi khi [tự động nạp](https://transcript1.com/vi/docs/billing-and-limits#auto-top-up) đang bật. |
| `audio_dropped` | Âm thanh đến trước khi phiên sẵn sàng, hoặc quá nhiều âm thanh đến trong lúc nhà cung cấp giọng nói đang kết nối. Âm thanh cũ nhất bị bỏ. |
| `idle_timeout_soon` | Đã 4 phút không có âm thanh. Phiên kết thúc sau 5 phút không có âm thanh. Hãy gửi âm thanh để giữ phiên. |

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

`error` có `code` và `message`. Các mã sau có thể đến trên socket trực tiếp:

| `code` | Nghiêm trọng | Ý nghĩa |
|---|---|---|
| `invalid_message` | Không | Một frame văn bản không phải `finalize` hay `end`. |
| `invalid_token` | Có | Token không hợp lệ, đã hết hạn hoặc đã được dùng. Socket đóng với mã `4001`. |
| `not_found` | Có | Phiên không tồn tại. Socket đóng với mã `4004`. |
| `session_ended` | Có | Phiên đã kết thúc. Socket đóng với mã `4004`. |

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

`reconnect` đến trước khi server đóng socket với mã `1012`, ví dụ khi đang triển khai bản mới. Hãy lấy token mới và tiếp tục phiên.

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

`session.ended` là sự kiện cuối cùng của mọi phiên. Các con số là tổng cuối cùng:

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

### Object dòng

| Trường | Ý nghĩa |
|---|---|
| `id` | Số bắt đầu từ 0 và tăng thêm một cho mỗi dòng. Đây là chỉ số segment trong transcript đã lưu. |
| `start_ms`, `end_ms` | Thời điểm bắt đầu và kết thúc theo thời gian âm thanh của phiên. Thời gian này liên tục qua các lần kết nối lại. |
| `speaker` | Nhãn người nói khi bật `diarize`, nếu không thì `null`. Nhãn giống như trong transcript thông thường. |
| `language` | Ngôn ngữ được nhận diện hoặc được chỉ định của dòng, hoặc `null` khi chưa biết. |
| `text` | Văn bản gốc cho đến lúc này. |
| `final` | `true` khi văn bản gốc sẽ không đổi nữa. |
| `translation` | `null` nếu không có `translate_to`, nếu không thì gồm `language`, `text` và `final`. |

Hãy hiển thị các dòng có `final: false` bằng kiểu nhạt hơn: văn bản của chúng vẫn có thể đổi. Văn bản của dòng có `final: true` không bao giờ đổi nữa, và dòng đã chốt không bao giờ bị xóa.

SDK dùng `camelCase` cho các trường của dòng: `startMs`, `endMs`.

### Lý do kết thúc

`session.ended` và `GET /v1/live/sessions/:id` trả về `end_reason`:

| Lý do | Ý nghĩa |
|---|---|
| `client_ended` | Bạn đã gửi `end`. |
| `balance_exhausted` | Lần tính phí tiếp theo thất bại. Phiên kết thúc ở ranh giới phút và được lưu. |
| `idle_timeout` | Không có âm thanh trong 5 phút. |
| `max_duration` | Phiên đã kết nối 300 phút. Hãy bắt đầu phiên mới. |
| `disconnected` | Client biến mất hơn 2 phút mà không tiếp tục phiên. |
| `upstream_error` | Nhà cung cấp giọng nói gặp lỗi. |
| `internal_error` | Có sự cố ở phía chúng tôi. |
| `not_connected` | Phiên chưa từng được kết nối trong 5 phút. Phiên này không bao giờ bị tính phí và chỉ xem được qua `GET`. |

### Mã đóng

| Mã | Ý nghĩa |
|---|---|
| `1000` | Đóng bình thường. Luôn đi sau `session.ended`. |
| `1011` | Lỗi server ngoài dự kiến. Server tự khôi phục phiên: hãy tiếp tục bằng token mới. |
| `1012` | Server khởi động lại. Đi sau `reconnect`: hãy tiếp tục bằng token mới. |
| `4001` | Token không hợp lệ, đã hết hạn hoặc đã được dùng. |
| `4004` | Không tìm thấy phiên hoặc phiên đã kết thúc. |
| `4008` | Vi phạm giao thức: frame nhị phân có độ dài lẻ hoặc quá lớn. |
| `4010` | Bị thay thế bởi một kết nối mới hơn vào cùng phiên. |

Trong lúc API khởi động lại, một request WebSocket mới có thể nhận HTTP `503`. Hãy thử lại sau ít phút.

## Tiếp tục và tiếp quản

Phiên vẫn tồn tại khi mạng đứt, khi API được triển khai lại và khi có sự cố. Sau khi mất kết nối, phiên chờ bạn trong 2 phút; quá thời gian đó phiên kết thúc với lý do `disconnected` và được lưu.

1. Gọi `POST /v1/live/sessions/:id/token`. Response gồm `token`, `token_expires_at` và `websocket_url`, giống các trường khi bạn tạo phiên. Lệnh này thất bại với `409 session_ended` khi phiên đã kết thúc.
2. Mở một WebSocket mới bằng token đó.
3. `session.started` đến với `resumed: true`. `offset_ms` là lượng âm thanh phiên đã xử lý: hãy tiếp tục âm thanh từ điểm đó. Id của dòng được đánh số tiếp.

Các dòng đã chốt được giữ lại. Phần đang dở tại thời điểm mất kết nối sẽ mất, vì vậy nếu bạn có đệm âm thanh, hãy gửi lại vài giây âm thanh từ `offset_ms`. Khoảng thời gian kết nối lại không bị tính phí.

Tiếp quản cũng là cùng lệnh gọi đó. Nếu server vẫn nghĩ một socket đang mở, kết nối hợp lệ mới sẽ thay thế nó, và socket cũ bị đóng với mã `4010` sau khi các dòng của nó được lưu. Giới hạn số phiên đồng thời không bao giờ chặn việc tiếp tục phiên.

SDK làm tất cả việc này giúp bạn: khi mất kết nối hoặc nhận sự kiện `reconnect`, SDK lấy token và kết nối lại với khoảng chờ tăng dần từ 0.5 đến 8 giây trong tối đa 2 phút, rồi gửi lại 10 giây âm thanh cuối cùng mà bạn đã gửi trong lúc gián đoạn.

## Tính phí

Chuyển thành văn bản trực tiếp trừ vào cùng số dư phút với các transcription thông thường.

- **Thời gian kết nối được tính phí**, từ lúc socket được chấp nhận đến khi đóng. Im lặng và video bị tạm dừng cũng được tính. Khoảng thời gian giữa lúc mất kết nối và lúc tiếp tục thì không.
- **Tính theo từng phút đã bắt đầu, tối thiểu 1.** Phút đầu tiên bị tính khi kết nối đầu tiên được chấp nhận, và mỗi phút sau bị tính khi phút đó bắt đầu.
- **Nhanh hơn thời gian thực thì tính theo thời lượng âm thanh.** Nếu âm thanh bạn gửi dài hơn thời gian kết nối, hệ thống tính thời lượng âm thanh.
- **Hệ số lấy từ API.** Một phút đã bắt đầu tốn `rate` phút số dư, làm tròn lên. Hệ số thường là 1 và có thể là số thập phân. Hãy đọc hệ số trực tiếp từ `rates.live` trong `GET /v1/account`, từ `rate` trong `GET /v1/live/capabilities` và từ `rate` của phiên bạn tạo. Hệ số của một phiên được cố định sau khi phiên được tạo.
- **Dịch miễn phí.**
- **Chưa từng kết nối thì không bị tính phí.** Phiên không được kết nối trong 5 phút sẽ kết thúc với `not_connected`.
- **Lỗi được hoàn phí.** Nếu phiên kết thúc với `upstream_error` hoặc `internal_error` trước khi tạo ra dòng nào, mọi khoản đã tính đều được hoàn lại. Phiên rỗng do im lặng vẫn bị tính phí và được lưu rỗng.
- **Khi hết số dư** và [tự động nạp](https://transcript1.com/vi/docs/billing-and-limits#auto-top-up) đang bật, transcript1 trừ tiền thẻ đã lưu trong lúc phiên vẫn tiếp tục phát trực tiếp, và phiên được tiếp tục. Nếu tự động nạp tắt hoặc bị tạm dừng, hoặc việc trừ tiền thất bại, phiên được chốt, lưu và kết thúc với `balance_exhausted` ở ranh giới phút. Cảnh báo `balance_low` đến trước, trừ khi tự động nạp đang bật.

Tạo phiên trả về `402 insufficient_minutes` khi số dư dùng được thấp hơn một phút đã bắt đầu theo hệ số hiện tại. Nếu tự động nạp đang bật, transcript1 sẽ thử nạp trước. Xem [Thanh toán và giới hạn](https://transcript1.com/vi/docs/billing-and-limits#live-transcription).

## Giới hạn

| | Free | Monthly | Annual |
|---|---|---|---|
| Phiên trực tiếp cùng lúc | 1 | 2 | 3 |

- Một phiên được tính khi ở trạng thái `waiting`, `connected` hoặc `disconnected` (trong khoảng 2 phút chờ tiếp tục). Vượt giới hạn, `POST /v1/live/sessions` trả về `429 too_many_live_sessions` kèm `Retry-After: 30`.
- Phiên trực tiếp không tính vào giới hạn job đang chạy của bạn.
- Một phiên kéo dài tối đa 300 phút thời gian kết nối. Sau đó phiên kết thúc với `max_duration`.
- Phiên không có âm thanh trong 5 phút kết thúc với `idle_timeout`; bạn nhận `idle_timeout_soon` sau 4 phút. Luồng bị tạm dừng vẫn bị tính thời gian kết nối và được coi là nhàn rỗi, nên hãy kết thúc phiên nếu tạm dừng lâu.
- transcript1 giới hạn tổng của tất cả phiên trực tiếp. Khi đã dùng hết, `POST /v1/live/sessions` trả về `503 live_capacity`, lỗi có thể thử lại. Hãy chờ `Retry-After` (30 giây) rồi thử lại. Giới hạn này chỉ được kiểm tra khi tạo phiên, không bao giờ kiểm tra giữa chừng.
- Việc tạo phiên và lấy token được tính vào giới hạn request mỗi phút của bạn.
- Âm thanh chỉ là PCM thô: `pcm_s16le`, mono, từ 8000 đến 48000 Hz.

## Khả năng hỗ trợ

`GET /v1/live/capabilities` mô tả những gì chế độ trực tiếp đang hỗ trợ, để client không phải viết cứng quy tắc của nhà cung cấp. Quyền cần có là `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
}
```

Các danh sách ở đây đã được rút gọn. Hãy đọc đầy đủ từ API.

- `translation.pairs` là `"any"` (từ mọi ngôn ngữ được nhận diện sang mọi ngôn ngữ đích), hoặc một danh sách object có `from` và `to`.
- `translation.requires_language` là `true` khi việc dịch cần `language` cụ thể thay vì `auto`.
- Khi chế độ trực tiếp chưa được cấu hình, response là `available: false` chỉ kèm `rate`, và việc tạo phiên trả về `503 live_unavailable`.

Với SDK: `await t1.live.capabilities()`.

## Xem một phiên

`GET /v1/live/sessions/:id` trả về trạng thái của một phiên. Quyền cần có là `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` là `waiting`, `connected`, `disconnected` hoặc `ended`. `end_reason` là `null` cho đến khi phiên kết thúc. Với SDK: `await t1.live.get(id)`.

## Lỗi

Lỗi request dùng [cấu trúc lỗi](https://transcript1.com/vi/docs/errors) quen thuộc. Đây là các lỗi mà `POST /v1/live/sessions` và các endpoint liên quan trả về:

| Mã HTTP | `code` | Có thể thử lại | Khi nào |
|---|---|---|---|
| 422 | `invalid_request` | Không | Một trường bị thiếu hoặc không hợp lệ, ví dụ `sample_rate` ngoài khoảng 8000 đến 48000. |
| 422 | `unsupported_language` | Không | `language` không được chế độ trực tiếp hỗ trợ. |
| 422 | `unsupported_translation` | Không | Ngôn ngữ đích, hoặc cặp ngôn ngữ, không được hỗ trợ. |
| 402 | `insufficient_minutes` | Không | Số dư dùng được thấp hơn một phút đã bắt đầu theo hệ số hiện tại. |
| 403 | `email_not_verified` | Không | Hãy xác minh email trước. |
| 429 | `too_many_live_sessions` | Có | Số phiên trực tiếp cùng lúc của gói bạn đã dùng hết. Chờ theo `Retry-After`. |
| 503 | `live_capacity` | Có | Toàn bộ dung lượng trực tiếp đang được dùng. Chờ theo `Retry-After` (30 giây). |
| 503 | `live_unavailable` | Không | Chế độ trực tiếp hiện không khả dụng. |
| 409 | `session_ended` | Không | `POST /v1/live/sessions/:id/token` cho một phiên đã kết thúc. |

Sự cố trên socket đến dưới dạng sự kiện `error` và [mã đóng](https://transcript1.com/vi/docs/live#close-codes).

## Transcript đã lưu

Với `save: true` (mặc định), phiên được lưu thành một transcription thông thường khi kết thúc. Âm thanh không bao giờ được lưu.

- `transcription_id` được trả về khi bạn tạo phiên. Trạng thái của nó là `live` trong lúc phiên chạy và `completed` sau khi phiên kết thúc. Phiên chưa từng kết nối để lại một transcription ở trạng thái `canceled`.
- Transcription có `source: { "platform": "live", "session_id": "lv_…" }`. Không có link hay file. Bảng điều khiển liệt kê nó như một phiên trực tiếp.
- Mỗi dòng trở thành một segment, có cùng chỉ số với `id` của dòng. Đọc transcript dưới dạng JSON, văn bản, SRT hoặc VTT như mọi transcription khác: xem [Tài liệu API](https://transcript1.com/vi/docs/api-reference).
- Bản dịch trực tiếp được lưu cùng. Đọc nó bằng tham số `translation`, như mô tả trong [Dịch](https://transcript1.com/vi/docs/translation#read-the-translation).
- Có thể thêm ngôn ngữ dịch khác sau bằng `POST /v1/transcriptions/:id/translations` khi transcription đã `completed`. Xem [Dịch](https://transcript1.com/vi/docs/translation#translate-an-existing-transcript).

Với `save: false`, không có gì được giữ lại sau khi phiên kết thúc: không có transcription và `transcription_id` là `null`. Bạn vẫn nhận mọi dòng qua socket và vẫn bị tính phí như bình thường.

## Bảo mật

- Token bạn nhận khi tạo phiên chỉ dùng được một lần và hết hạn sau 60 giây. Nó được gửi dưới dạng subprotocol WebSocket, không nằm trong URL, nên không lọt vào log truy cập. Token bị lộ sẽ vô dụng sau khi đã dùng hoặc hết hạn.
- Chỉ dùng khóa API từ server do bạn kiểm soát. Đừng bao giờ đặt khóa API trong trang web hay tiện ích mở rộng.
- Trong trình duyệt hoặc tiện ích mở rộng, hãy cho người dùng đăng nhập bằng OAuth và truyền OAuth access token vào `getAccessToken` của `createLiveClient`. Token cần quyền `transcriptions:write` để bắt đầu phiên. OAuth access token hết hạn sau 1 giờ, và mỗi lần tiếp tục phiên đều gọi `getAccessToken` để lấy token mới.
- Server của bạn cũng có thể tạo phiên bằng khóa API rồi chỉ chuyển `websocket_url` và `token` cho trình duyệt. Khi đó client kết nối trực tiếp. Client không thể tiếp tục phiên nếu không có token mới từ bạn.
