# Расшифровка в реальном времени

> Отправляйте звук с микрофона или вкладки в transcript1 по WebSocket и получайте строки субтитров, при желании с переводом, прямо во время речи.

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

Расшифровка в реальном времени превращает аудиопоток в строки субтитров, пока люди ещё говорят. Клиент отправляет по WebSocket сырой PCM-звук, а transcript1 возвращает строки: сначала это черновой вариант, затем окончательный текст. Добавьте `translate_to`, и у каждой строки появится перевод. После завершения сессии результат сохраняется как обычная транскрибация.

Отдельных тарифов для live нет: расшифровка в реальном времени списывает минуты с того же баланса, что и всё остальное. Как считается время подключения, читайте в разделе [«Оплата»](https://transcript1.com/ru/docs/live#billing).

## Когда нужен live

| Выбирайте live, если | Выбирайте [обычную транскрибацию](https://transcript1.com/ru/docs/getting-started), если |
|---|---|
| Звук ещё создаётся: звонок, доклад, трансляция, видео, которое вы смотрите. | У вас есть ссылка или готовый файл. |
| Субтитры нужны через секунду-две после слов. | Важна максимальная точность, а результат можно подождать. |
| Рядом с каждой строкой нужен перевод в реальном времени. | Нужен перевод готового транскрипта, в том числе на несколько языков. |

Live принимает только звук в виде сырого PCM. Ссылки на видео, загруженные файлы, WebM, Opus и MP3 не поддерживаются: запишите звук на стороне клиента и сначала преобразуйте его в PCM. Как это сделать, показано в разделе [«Захват звука в браузере»](https://transcript1.com/ru/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()` завершает сессию, дожидается последних строк и сохраняет транскрипт. Все методы и события описаны в разделе [«SDK для Node.js»](https://transcript1.com/ru/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/ru/docs/live#billing).

В теле запроса можно передать такие поля. Все они необязательны, нужное разрешение — `transcriptions:write`:

| Поле | По умолчанию | Правило |
|---|---|---|
| `language` | `auto` | `auto` или двухбуквенный код языка, который поддерживает live. |
| `translate_to` | нет | Двухбуквенный код языка. На сессию приходится один язык перевода. См. [«Перевод в реальном времени»](https://transcript1.com/ru/docs/live#translate-live). |
| `diarize` | `false` | Подписывать говорящих. В некоторых конфигурациях разметка недоступна, например вместе с `language: "auto"`, и API отвечает `422`. Проверьте [возможности](https://transcript1.com/ru/docs/live#capabilities). |
| `audio_format` | `pcm_s16le` | Единственное значение. Поле нужно, чтобы позже можно было добавить другие форматы. |
| `sample_rate` | `16000` | Целое число от 8000 до 48000. Только моно. |
| `save` | `true` | При `false` после завершения сессии ничего не хранится. См. [«Сохранённые транскрипты»](https://transcript1.com/ru/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/ru/docs/live#close-codes).

## Захват звука в браузере

Звук для live должен быть знаковым 16-битным моно PCM с порядком байтов little-endian и частотой `sample_rate` сессии. Браузер отдаёт сэмплы с плавающей точкой, поэтому преобразуйте их в `AudioWorklet`. Пример ниже захватывает микрофон как PCM 16 кГц блоками по 100 мс и отправляет его через браузерную точку входа SDK. `getAccessToken` возвращает OAuth-токен доступа; никогда не кладите API-ключ в браузер. См. [«Безопасность»](https://transcript1.com/ru/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();
}
```

Скрипт AudioWorklet рассчитан на 16 кГц. Если контекст работает на другой частоте, блок перестаёт быть равным 100 мс, но PCM остаётся корректным, потому что `sampleRate` совпадает с контекстом. Чтобы захватить звук вкладки или видео вместо микрофона, создайте `MediaStream` через `getDisplayMedia` или из `captureStream()` медиаэлемента и подключите его так же.

## Перевод в реальном времени

Добавьте в сессию `translate_to`, и transcript1 будет переводить каждую строку по мере произнесения.

- На сессию приходится один язык перевода. Другие языки можно добавить к сохранённому транскрипту позже через [эндпоинт перевода](https://transcript1.com/ru/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/ru/docs/live#capabilities), а не зашивайте правила в код. Неподдерживаемый язык перевода вернёт `422 unsupported_translation`. Например, поставщик может требовать явный `language` вместо `auto` или разрешать только пары с английским.

## Протокол WebSocket

### Подключение

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

Токен передаётся как субпротокол WebSocket. Он не попадает ни в URL, ни в журналы доступа. Заголовок `Authorization` не отправляйте: учётными данными служит сам токен. Сервер шлёт ping каждые 30 секунд; браузеры и пакет `ws` отвечают автоматически. Сокет, который не ответил pong за 60 секунд, отключается.

Корректное подключение к сессии, у которой уже есть сокет, заменяет старый. См. [«Возобновление и перехват»](https://transcript1.com/ru/docs/live#resume-and-takeover).

### Сообщения клиента

| Сообщение | Значение |
|---|---|
| Бинарный кадр | Звук: PCM s16le, моно, с `sample_rate` сессии. Длина кадра чётная, размер не больше 256 KB. Хороший размер — около 100 мс звука на кадр. |
| `{"type":"finalize"}` | Закрыть текущую строку сейчас, например при паузе видео. |
| `{"type":"end"}` | Завершить сессию и сохранить её. Сервер отвечает `session.ended` и закрывает сокет. |
| Любое другое | Нефатальное событие `error` с кодом `invalid_message`. |

Кадр нечётной длины или слишком большого размера считается нарушением протокола и закрывает сокет с кодом `4008`.

Начинайте отправлять звук после `session.started`. Если клиент отправляет звук быстрее реального времени, оплачивается длительность звука, а не время подключения (см. [«Оплата»](https://transcript1.com/ru/docs/live#billing)), поэтому отправляйте звук с той скоростью, с какой он создаётся.

### События сервера

Каждое событие — это текстовый JSON-кадр с полем `type`.

| Событие | Когда приходит |
|---|---|
| `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`. См. [«Объект строки»](https://transcript1.com/ru/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/ru/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
}
```

### Объект строки

| Поле | Значение |
|---|---|
| `id` | Число с нуля, которое растёт на единицу с каждой строкой. Это индекс сегмента в сохранённом транскрипте. |
| `start_ms`, `end_ms` | Начало и конец во времени звука сессии. Это время продолжается после переподключений. |
| `speaker` | Метка говорящего, если включён `diarize`, иначе `null`. Метки такие же, как в обычных транскриптах. |
| `language` | Определённый или заданный язык строки либо `null`, если он ещё неизвестен. |
| `text` | Текст оригинала на данный момент. |
| `final` | `true`, когда текст оригинала больше не изменится. |
| `translation` | `null` без `translate_to`, иначе `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` | Ошибка на нашей стороне. |
| `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. Придёт `session.started` с `resumed: true`. `offset_ms` показывает, сколько звука сессия уже обработала: продолжайте с этого места. Идентификаторы строк продолжают нумерацию.

Строки, которые уже были финальными, сохраняются. Незавершённый хвост на момент обрыва теряется, поэтому, если вы буферизуете звук, отправьте заново несколько секунд с `offset_ms`. Время переподключения не оплачивается.

Перехват — это тот же вызов. Если сервер ещё считает сокет открытым, новое корректное подключение заменяет его, а старый сокет закрывается с `4010` после сохранения его строк. Лимит одновременных сессий возобновлению не мешает.

SDK делает всё это за вас: при обрыве или событии `reconnect` он получает токен и переподключается с задержкой от 0.5 до 8 секунд в течение 2 минут, а затем повторно отправляет последние 10 секунд звука, отправленного за время обрыва.

## Оплата

Live списывает минуты с того же баланса, что и обычные транскрибации.

- **Оплачивается время подключения** с момента принятия сокета до его закрытия. Тишина и видео на паузе тоже считаются. Время между обрывом и возобновлением не считается.
- **За каждую начатую минуту, минимум одна.** Первая минута списывается при первом принятом подключении, каждая следующая — когда начинается.
- **Быстрее реального времени оплачивается как длительность звука.** Если отправленный звук длиннее времени подключения, оплачивается длительность звука.
- **Тариф берите из API.** Начатая минута стоит `rate` минут баланса с округлением вверх. Обычно значение равно 1, но может быть дробным. Текущее значение читайте из `rates.live` в `GET /v1/account`, из `rate` в `GET /v1/live/capabilities` и из `rate` созданной сессии. После создания сессии её значение не меняется.
- **Перевод бесплатный.**
- **Без подключения нет списания.** Сессия, к которой не подключились за 5 минут, завершается как `not_connected`.
- **При сбоях деньги возвращаются.** Если сессия завершилась с `upstream_error` или `internal_error`, не выдав ни одной строки, все списания возвращаются. Пустая сессия из-за тишины оплачивается и сохраняется пустой.
- **Когда баланс заканчивается** и включено [автопополнение](https://transcript1.com/ru/docs/billing-and-limits#auto-top-up), transcript1 списывает оплату с сохранённой карты, пока сессия продолжает стримить, и сессия продолжается. Если автопополнение выключено или приостановлено либо списание не прошло, сессия завершается, сохраняется и закрывается с `balance_exhausted` на границе минуты. Сначала приходит предупреждение `balance_low`, если только не включено автопополнение.

Если доступного баланса меньше одной начатой минуты по текущему тарифу, создание сессии вернёт `402 insufficient_minutes`. Если включено автопополнение, transcript1 сначала пробует пополнить баланс. См. [«Тарифы и лимиты»](https://transcript1.com/ru/docs/billing-and-limits#live-transcription).

## Лимиты

| | Free | Monthly | Annual |
|---|---|---|---|
| Одновременных live-сессий | 1 | 2 | 3 |

- Сессия учитывается, пока она в статусе `waiting`, `connected` или `disconnected` (в пределах двухминутного окна). При превышении лимита `POST /v1/live/sessions` возвращает `429 too_many_live_sessions` с `Retry-After: 30`.
- Live-сессии не входят в лимит активных задач.
- Сессия длится не больше 300 минут времени подключения, после чего завершается с `max_duration`.
- Сессия без звука 5 минут завершается с `idle_timeout`; предупреждение `idle_timeout_soon` приходит через 4 минуты. Поток на паузе по-прежнему оплачивается как время подключения и считается простоем, поэтому при долгой паузе завершайте сессию.
- transcript1 ограничивает все live-сессии в сумме. Когда ёмкость исчерпана, `POST /v1/live/sessions` возвращает `503 live_capacity` — такую ошибку можно повторить. Подождите `Retry-After` (30 секунд) и повторите запрос. Проверка выполняется только при создании сессии, но не посреди неё.
- Создание сессий и получение токенов входят в ваш лимит запросов в минуту.
- Звук принимается только в виде сырого PCM: `pcm_s16le`, моно, от 8000 до 48000 Гц.

## Возможности

`GET /v1/live/capabilities` описывает, что live поддерживает прямо сейчас, чтобы клиентам не приходилось зашивать правила поставщика. Нужное разрешение — `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`.
- Когда live не настроен, ответ содержит `available: false` и только `rate`, а создание сессии возвращает `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/ru/docs/errors). Вот те, что возвращают `POST /v1/live/sessions` и соседние эндпоинты:

| HTTP-статус | `code` | Можно повторить | Когда |
|---|---|---|---|
| 422 | `invalid_request` | Нет | Поле пропущено или неверно, например `sample_rate` вне диапазона от 8000 до 48000. |
| 422 | `unsupported_language` | Нет | `language` не поддерживается в live. |
| 422 | `unsupported_translation` | Нет | Язык перевода или пара языков не поддерживается. |
| 402 | `insufficient_minutes` | Нет | Доступный баланс меньше одной начатой минуты по текущему тарифу. |
| 403 | `email_not_verified` | Нет | Сначала подтвердите почту. |
| 429 | `too_many_live_sessions` | Да | Заняты все одновременные live-сессии вашего тарифа. Подождите `Retry-After`. |
| 503 | `live_capacity` | Да | Вся ёмкость live занята. Подождите `Retry-After` (30 секунд). |
| 503 | `live_unavailable` | Нет | Расшифровка в реальном времени сейчас недоступна. |
| 409 | `session_ended` | Нет | `POST /v1/live/sessions/:id/token` для завершённой сессии. |

Проблемы на сокете приходят как события `error` и [коды закрытия](https://transcript1.com/ru/docs/live#close-codes).

## Сохранённые транскрипты

При `save: true` (по умолчанию) после завершения сессия сохраняется как обычная транскрибация. Сам звук не хранится никогда.

- `transcription_id` возвращается при создании сессии. Пока сессия идёт, у неё статус `live`, после завершения — `completed`. Если к сессии так и не подключились, остаётся транскрибация со статусом `canceled`.
- У транскрибации есть `source: { "platform": "live", "session_id": "lv_…" }`. Ссылки и файла нет. В личном кабинете она отображается как live-сессия.
- Каждая строка становится одним сегментом с тем же индексом, что и `id` строки. Транскрипт можно читать как JSON, текст, SRT или VTT, как любую другую транскрибацию: см. [справочник API](https://transcript1.com/ru/docs/api-reference).
- Перевод, сделанный в реальном времени, сохраняется вместе с ней. Читайте его с параметром `translation`, как описано в разделе [«Перевод»](https://transcript1.com/ru/docs/translation#read-the-translation).
- Когда транскрибация получит статус `completed`, другие языки перевода можно добавить позже через `POST /v1/transcriptions/:id/translations`. См. [«Перевод»](https://transcript1.com/ru/docs/translation#translate-an-existing-transcript).

При `save: false` после завершения сессии ничего не хранится: транскрибации нет, а `transcription_id` равен `null`. Все строки вы по-прежнему получаете по сокету, и списание остаётся обычным.

## Безопасность

- Токен, который вы получаете при создании сессии, одноразовый и действует 60 секунд. Он передаётся как субпротокол WebSocket, а не в URL, поэтому не попадает в журналы доступа. Утёкший токен бесполезен после использования или истечения срока.
- Используйте API-ключ только на сервере, который вы контролируете. Никогда не вставляйте API-ключ в веб-страницу или расширение.
- В браузере или расширении входите через OAuth и передавайте OAuth-токен доступа в `getAccessToken` метода `createLiveClient`. Для запуска сессий токену нужно разрешение `transcriptions:write`. OAuth-токены доступа истекают через 1 час, и при каждом возобновлении `getAccessToken` запрашивается заново.
- Сессию может создать и ваш сервер с API-ключом, передав браузеру только `websocket_url` и `token`. Тогда клиент подключается напрямую. Без нового токена от вас он не сможет возобновить сессию.
