# Transkripsi langsung

> Kirim audio mikrofon atau tab ke transcript1 lewat WebSocket dan terima baris subtitle, dengan terjemahan real-time opsional, saat orang masih berbicara.

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

Transkripsi langsung mengubah aliran audio menjadi baris subtitle saat orang masih berbicara. Klien Anda mengirim audio PCM mentah lewat WebSocket, lalu transcript1 mengirim balik baris yang awalnya berupa dugaan sementara dan kemudian menjadi final. Tambahkan `translate_to` agar setiap baris juga membawa terjemahannya. Setelah sesi berakhir, hasilnya disimpan sebagai transkripsi biasa.

Tidak ada paket khusus untuk fitur langsung: transkripsi langsung memakai saldo menit yang sama dengan fitur lainnya. Lihat [Biaya](https://transcript1.com/id/docs/live#billing) untuk cara waktu terhubung dihitung.

## Kapan memakai transkripsi langsung

| Pakai transkripsi langsung jika | Pakai [transkripsi biasa](https://transcript1.com/id/docs/getting-started) jika |
|---|---|
| Audio masih berlangsung: panggilan, presentasi, siaran, atau video yang sedang Anda tonton. | Anda punya tautan atau file yang sudah jadi. |
| Anda butuh subtitle satu atau dua detik setelah ucapan. | Anda mengutamakan akurasi terbaik dan bisa menunggu hasilnya. |
| Anda ingin terjemahan real-time di samping setiap baris. | Anda ingin menerjemahkan transkrip yang sudah selesai, atau ke beberapa bahasa. |

Transkripsi langsung hanya menerima audio, dalam bentuk PCM mentah. Tautan video, unggahan file, WebM, Opus, dan MP3 tidak diterima. Tangkap audio di klien dan ubah ke PCM terlebih dahulu. [Menangkap audio di browser](https://transcript1.com/id/docs/live#capture-audio-in-a-browser) menunjukkan caranya.

## Mulai cepat dengan SDK

Pasang SDK 1.5.0 atau yang lebih baru: `npm i @transcript1/sdk@latest`. `t1.live.start` membuat sesi, terhubung, lalu selesai setelah sesi dimulai, sehingga Anda bisa langsung mengirim audio.

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

Baris diperbarui di tempat. Ganti baris yang sudah Anda punya ketika baris dengan `id` yang sama tiba, atau baca semuanya dari `live.lines`. `end()` menyelesaikan sesi, menunggu baris terakhir, dan menyimpan transkrip. Lihat [SDK Node.js](https://transcript1.com/id/docs/sdk#live-transcription) untuk semua metode dan event.

## Mulai cepat dengan WebSocket mentah

Tanpa SDK ada dua langkah: buat sesi lewat HTTPS, lalu buka WebSocket dengan token sekali pakai yang dikembalikan.

### Membuat sesi

```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 menjawab `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` bernilai `null` jika `save` adalah `false`. Token hanya berlaku sekali dan kedaluwarsa dalam 60 detik, jadi segera terhubung. `rate` adalah jumlah menit saldo yang ditagih per menit yang dimulai dalam sesi ini; lihat [Biaya](https://transcript1.com/id/docs/live#billing).

Body request menerima field berikut. Semuanya opsional, dan scope yang dibutuhkan adalah `transcriptions:write`:

| Field | Bawaan | Aturan |
|---|---|---|
| `language` | `auto` | `auto`, atau kode dua huruf yang didukung transkripsi langsung. |
| `translate_to` | tidak ada | Kode dua huruf. Satu bahasa tujuan per sesi. Lihat [Terjemahan langsung](https://transcript1.com/id/docs/live#translate-live). |
| `diarize` | `false` | Memberi label pembicara. Sebagian konfigurasi tidak bisa memberi label pembicara, misalnya bersama `language: "auto"`, dan menjawab `422`. Periksa [kemampuan](https://transcript1.com/id/docs/live#capabilities). |
| `audio_format` | `pcm_s16le` | Satu-satunya nilai. Field ini ada agar format lain bisa ditambahkan nanti. |
| `sample_rate` | `16000` | Bilangan bulat dari 8000 sampai 48000. Hanya mono. |
| `save` | `true` | `false` tidak menyimpan apa pun setelah sesi berakhir. Lihat [Transkrip tersimpan](https://transcript1.com/id/docs/live#saved-transcripts). |

### Terhubung dan streaming

Buka WebSocket dengan token sebagai subprotokol kedua. Cara ini berjalan di browser dan di Node 22+ tanpa header khusus. Tunggu `session.started` sebelum mengirim audio: frame biner yang dikirim lebih awal dibuang, dan Anda mendapat peringatan `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 tidak punya `WebSocket` bawaan: pakai paket `ws`, yang memiliki bentuk `new WebSocket(url, protocols)` yang sama.

Server menjawab token yang valid hanya dengan subprotokol `t1-live`. Token yang salah tidak pernah menggagalkan handshake: socket terbuka, Anda menerima event `error`, lalu socket ditutup dengan kode 4xxx, sehingga browser menampilkan alasan sebenarnya, bukan 1006 yang generik. Lihat [Kode penutupan](https://transcript1.com/id/docs/live#close-codes).

## Menangkap audio di browser

Audio untuk transkripsi langsung harus berupa PCM mono 16-bit bertanda little-endian dengan `sample_rate` sesi. Browser memberi sampel floating-point, jadi ubah di dalam `AudioWorklet`. Contoh ini menangkap mikrofon sebagai PCM 16 kHz dalam potongan 100 ms dan mengirimkannya lewat entri SDK untuk browser. `getAccessToken` mengembalikan access token OAuth; jangan pernah menaruh kunci API di browser. Lihat [Keamanan](https://transcript1.com/id/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 ini ditulis untuk 16 kHz. Jika konteks berjalan pada laju lain, potongan tidak lagi 100 ms, tetapi PCM tetap benar karena `sampleRate` sama dengan konteks. Untuk menangkap suara tab atau video, bukan mikrofon, buat `MediaStream` dengan `getDisplayMedia` atau dari `captureStream()` elemen media, lalu sambungkan dengan cara yang sama.

## Terjemahan langsung

Tambahkan `translate_to` ke sesi, dan transcript1 menerjemahkan setiap baris saat diucapkan.

- Satu bahasa tujuan per sesi. Tambahkan bahasa lain ke transkrip tersimpan setelahnya dengan [endpoint terjemahan](https://transcript1.com/id/docs/live#saved-transcripts).
- Setiap `line` punya teks asli (`text`, `final`) dan objek `translation` (`language`, `text`, `final`). `translation` bernilai `null` jika `translate_to` tidak diatur.
- Teks asli dan terjemahan menjadi final secara terpisah: satu baris bisa punya teks asli yang final sementara terjemahannya masih berubah. Tampilkan baris sebagai final setelah keduanya final.
- Baris yang sudah berbahasa tujuan memakai teksnya sendiri sebagai terjemahan, dan menjadi final saat baris ditutup.
- Baris ditutup saat pembicara berhenti sejenak, pembicara berganti, jeda antarkata lebih dari 0.9 detik, baris mencapai dua baris subtitle (84 karakter) atau 15 detik, atau Anda mengirim `finalize`. Saat terjemahan aktif, penutupan baris bisa memakan sekitar 3 detik lebih lama sampai terjemahannya tiba. Dalam hal itu terjemahan bisa tetap terbuka (`final: false`) dan terus bertambah setelah teks asli final.
- Terjemahan gratis. Tidak memakai menit.

Bahasa tujuan dan pasangan bahasa yang tersedia bergantung pada penyedia ucapan langsung. Baca [kemampuan](https://transcript1.com/id/docs/live#capabilities) alih-alih menuliskan aturan secara permanen di kode. Tujuan yang tidak didukung menghasilkan `422 unsupported_translation`. Misalnya, penyedia bisa mewajibkan `language` yang eksplisit alih-alih `auto`, atau hanya mengizinkan pasangan yang menyertakan bahasa Inggris.

## Protokol WebSocket

### Terhubung

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

Token dikirim sebagai subprotokol WebSocket. Token tidak pernah muncul di URL atau log akses. Jangan kirim header `Authorization`: token itulah kredensialnya. Server mengirim ping setiap 30 detik; browser dan paket `ws` menjawab otomatis. Socket yang tidak mengirim pong selama 60 detik diputus.

Koneksi valid ke sesi yang sudah punya socket akan menggantikan socket lama. Lihat [Melanjutkan dan mengambil alih](https://transcript1.com/id/docs/live#resume-and-takeover).

### Pesan dari klien

| Pesan | Arti |
|---|---|
| Frame biner | Audio: PCM s16le mono dengan `sample_rate` sesi. Setiap frame berukuran genap dan paling besar 256 KB. Sekitar 100 ms per frame adalah ukuran yang baik. |
| `{"type":"finalize"}` | Menutup baris saat ini sekarang juga, misalnya saat video dijeda. |
| `{"type":"end"}` | Mengakhiri sesi dan menyimpannya. Server menjawab `session.ended` lalu menutup socket. |
| Selain itu | Event `error` yang tidak fatal dengan kode `invalid_message`. |

Frame berukuran ganjil atau terlalu besar adalah pelanggaran protokol dan menutup socket dengan `4008`.

Mulai kirim audio setelah `session.started`. Klien yang mengirim audio lebih cepat dari waktu nyata ditagih berdasarkan durasi audio, bukan waktu terhubung (lihat [Biaya](https://transcript1.com/id/docs/live#billing)), jadi kirim audio sesuai kecepatan audio itu dihasilkan.

### Event dari server

Setiap event adalah frame teks JSON dengan `type`.

| Event | Dikirim saat |
|---|---|
| `session.started` | Koneksi diterima. Kirim audio mulai sekarang. |
| `line` | Baris subtitle dibuat atau berubah. |
| `usage` | Setelah setiap penagihan. |
| `warning` | Ada hal yang perlu diperhatikan, tetapi sesi berlanjut. |
| `error` | Masalah yang tidak fatal, atau alasan penutupan seketika. |
| `reconnect` | Server akan segera restart. Lanjutkan sesi. |
| `session.ended` | Sesi selesai dan tersimpan. Event terakhir sebelum penutupan normal. |

`session.started` menyatakan apakah ini kelanjutan sesi dan berapa banyak audio yang sudah diproses sesi:

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

`line` membawa keadaan lengkap terkini dari satu baris, jadi ganti baris yang Anda punya dengan `id` yang sama. Lihat [Objek baris](https://transcript1.com/id/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` dikirim setelah setiap penagihan, berisi waktu terhubung, durasi audio, menit saldo yang sudah ditagih, dan menit yang tersisa:

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

`warning` memiliki `code`:

| `code` | Arti |
|---|---|
| `balance_low` | Penagihan berikutnya akan gagal. Sesi berakhir dengan `balance_exhausted` saat waktunya tiba. Tidak dikirim saat [top-up otomatis](https://transcript1.com/id/docs/billing-and-limits#auto-top-up) aktif. |
| `audio_dropped` | Audio tiba sebelum sesi siap, atau terlalu banyak audio tiba saat penyedia ucapan sedang terhubung. Audio terlama dibuang. |
| `idle_timeout_soon` | Tidak ada audio selama 4 menit. Sesi berakhir setelah 5 menit tanpa audio. Kirim audio agar sesi tetap berjalan. |

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

`error` memiliki `code` dan `message`. Kode berikut bisa tiba di socket langsung:

| `code` | Fatal | Arti |
|---|---|---|
| `invalid_message` | Tidak | Frame teks yang bukan `finalize` atau `end`. |
| `invalid_token` | Ya | Token tidak valid, kedaluwarsa, atau sudah dipakai. Socket ditutup dengan `4001`. |
| `not_found` | Ya | Sesi tidak ada. Socket ditutup dengan `4004`. |
| `session_ended` | Ya | Sesi sudah berakhir. Socket ditutup dengan `4004`. |

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

`reconnect` datang sebelum server menutup socket dengan `1012`, misalnya saat deploy. Ambil token baru dan lanjutkan sesi.

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

`session.ended` adalah event terakhir setiap sesi. Angkanya adalah total akhir:

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

### Objek baris

| Field | Arti |
|---|---|
| `id` | Nomor berbasis 0 yang bertambah satu untuk setiap baris. Ini adalah indeks segmen di transkrip tersimpan. |
| `start_ms`, `end_ms` | Awal dan akhir dalam waktu audio sesi. Waktu ini berlanjut melewati sambungan ulang. |
| `speaker` | Label pembicara jika `diarize` aktif, selain itu `null`. Labelnya sama dengan transkrip biasa. |
| `language` | Bahasa baris yang terdeteksi atau ditentukan, atau `null` jika belum diketahui. |
| `text` | Teks asli sejauh ini. |
| `final` | `true` setelah teks asli tidak akan berubah lagi. |
| `translation` | `null` tanpa `translate_to`, selain itu `language`, `text`, dan `final`. |

Tampilkan baris dengan `final: false` dengan gaya yang lebih pudar: teksnya masih bisa berubah. Teks baris dengan `final: true` tidak pernah berubah lagi, dan baris final tidak pernah dihapus.

SDK memakai `camelCase` untuk field baris: `startMs`, `endMs`.

### Alasan berakhir

`session.ended` dan `GET /v1/live/sessions/:id` memberikan `end_reason`:

| Alasan | Arti |
|---|---|
| `client_ended` | Anda mengirim `end`. |
| `balance_exhausted` | Penagihan berikutnya gagal. Sesi berakhir pada batas menit dan disimpan. |
| `idle_timeout` | Tidak ada audio selama 5 menit. |
| `max_duration` | Sesi terhubung selama 300 menit. Mulai sesi baru. |
| `disconnected` | Klien menghilang lebih dari 2 menit tanpa melanjutkan sesi. |
| `upstream_error` | Penyedia ucapan gagal. |
| `internal_error` | Terjadi masalah di sisi kami. |
| `not_connected` | Sesi tidak pernah terhubung dalam 5 menit. Sesi ini tidak pernah ditagih dan hanya terlihat lewat `GET`. |

### Kode penutupan

| Kode | Arti |
|---|---|
| `1000` | Penutupan normal. Selalu mengikuti `session.ended`. |
| `1011` | Error server yang tidak terduga. Sesi dipulihkan oleh server: lanjutkan dengan token baru. |
| `1012` | Server restart. Mengikuti `reconnect`: lanjutkan dengan token baru. |
| `4001` | Token tidak valid, kedaluwarsa, atau sudah dipakai. |
| `4004` | Sesi tidak ditemukan atau sudah berakhir. |
| `4008` | Pelanggaran protokol: frame biner berukuran ganjil atau terlalu besar. |
| `4010` | Digantikan oleh koneksi yang lebih baru ke sesi yang sama. |

Selama API restart, request WebSocket baru bisa dijawab dengan HTTP `503`. Coba lagi sebentar lagi.

## Melanjutkan dan mengambil alih

Sesi bertahan saat jaringan terputus, saat API di-deploy, dan saat terjadi crash. Selama 2 menit setelah terputus, sesi menunggu Anda; setelah itu sesi berakhir sebagai `disconnected` dan disimpan.

1. Panggil `POST /v1/live/sessions/:id/token`. Responsnya berisi `token`, `token_expires_at`, dan `websocket_url`, field yang sama seperti saat Anda membuat sesi. Panggilan ini gagal dengan `409 session_ended` setelah sesi berakhir.
2. Buka WebSocket baru dengan token itu.
3. `session.started` tiba dengan `resumed: true`. `offset_ms` adalah jumlah audio yang sudah diproses sesi: lanjutkan audio dari titik itu. Id baris berlanjut.

Baris yang sudah final tetap disimpan. Ekor yang sedang berjalan saat koneksi terputus hilang, jadi jika Anda menyimpan buffer audio, kirim ulang beberapa detik audio mulai dari `offset_ms`. Jeda saat menyambung ulang tidak ditagih.

Pengambilalihan menggunakan panggilan yang sama. Jika server masih menganggap sebuah socket terbuka, koneksi valid yang baru menggantikannya, dan socket lama ditutup dengan `4010` setelah barisnya disimpan. Melanjutkan sesi tidak pernah terhalang oleh batas sesi bersamaan.

SDK melakukan semua ini untuk Anda: saat koneksi terputus atau event `reconnect` tiba, SDK mengambil token dan menyambung ulang dengan jeda bertahap 0.5 sampai 8 detik hingga 2 menit, lalu mengirim ulang 10 detik terakhir audio yang Anda kirim selama jeda.

## Biaya

Transkripsi langsung memakai saldo menit yang sama dengan transkripsi batch.

- **Waktu terhubung yang ditagih**, sejak socket diterima sampai ditutup. Keheningan dan video yang dijeda ikut dihitung. Waktu antara koneksi terputus dan dilanjutkan tidak dihitung.
- **Per menit yang dimulai, minimum 1.** Menit pertama ditagih saat koneksi pertama diterima, dan setiap menit berikutnya saat menit itu dimulai.
- **Lebih cepat dari waktu nyata ditagih sebagai durasi audio.** Jika audio yang Anda kirim lebih panjang daripada waktu terhubung, durasi audio yang ditagih.
- **Tarif dibaca dari API.** Satu menit yang dimulai bernilai `rate` menit saldo, dibulatkan ke atas. Tarif biasanya 1 dan bisa berupa desimal. Baca tarif langsung dari `rates.live` di `GET /v1/account`, dari `rate` di `GET /v1/live/capabilities`, dan dari `rate` pada sesi yang Anda buat. Tarif sebuah sesi tetap setelah sesi dibuat.
- **Terjemahan gratis.**
- **Tidak pernah terhubung berarti tidak ditagih.** Sesi yang tidak terhubung dalam 5 menit berakhir sebagai `not_connected`.
- **Kegagalan dikembalikan.** Jika sesi berakhir dengan `upstream_error` atau `internal_error` sebelum menghasilkan satu baris pun, semua tagihan dikembalikan. Sesi kosong akibat keheningan tetap ditagih dan disimpan kosong.
- **Saat saldo habis** dan [top-up otomatis](https://transcript1.com/id/docs/billing-and-limits#auto-top-up) aktif, transcript1 menagih kartu tersimpan Anda selagi sesi tetap streaming, dan sesi berlanjut. Jika top-up otomatis mati atau dijeda, atau penagihan gagal, sesi diselesaikan, disimpan, dan berakhir dengan `balance_exhausted` pada batas menit. Peringatan `balance_low` datang lebih dulu, kecuali top-up otomatis aktif.

Membuat sesi menghasilkan `402 insufficient_minutes` jika saldo yang bisa dipakai kurang dari satu menit yang dimulai pada tarif saat ini. Jika top-up otomatis aktif, transcript1 mencoba top-up lebih dulu. Lihat [Biaya dan batas penggunaan](https://transcript1.com/id/docs/billing-and-limits#live-transcription).

## Batas

| | Free | Monthly | Annual |
|---|---|---|---|
| Sesi langsung bersamaan | 1 | 2 | 3 |

- Sesi dihitung selama berstatus `waiting`, `connected`, atau `disconnected` (dalam jendela 2 menitnya). Jika melewati batas, `POST /v1/live/sessions` menghasilkan `429 too_many_live_sessions` dengan `Retry-After: 30`.
- Sesi langsung tidak dihitung dalam batas transkripsi aktif Anda.
- Satu sesi berlangsung paling lama 300 menit waktu terhubung. Setelah itu sesi berakhir dengan `max_duration`.
- Sesi tanpa audio selama 5 menit berakhir dengan `idle_timeout`; Anda mendapat `idle_timeout_soon` setelah 4 menit. Aliran yang dijeda tetap menagih waktu terhubung dan dihitung sebagai menganggur, jadi akhiri sesi jika jedanya lama.
- transcript1 membatasi semua sesi langsung secara bersama-sama. Jika semuanya terpakai, `POST /v1/live/sessions` menghasilkan `503 live_capacity`, yang bisa dicoba ulang. Tunggu `Retry-After` (30 detik) lalu coba lagi. Batas ini diperiksa hanya saat sesi dibuat, tidak pernah di tengah sesi.
- Membuat sesi dan mengambil token dihitung dalam batas request per menit Anda.
- Audio hanya berupa PCM mentah: `pcm_s16le`, mono, 8000 sampai 48000 Hz.

## Kemampuan

`GET /v1/live/capabilities` menjelaskan apa yang saat ini didukung transkripsi langsung, sehingga klien tidak perlu menuliskan aturan penyedia di kode. Scope yang dibutuhkan adalah `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
}
```

Daftar di atas dipersingkat. Baca daftar lengkapnya dari API.

- `translation.pairs` bernilai `"any"` (bahasa terdeteksi apa pun ke tujuan apa pun), atau daftar objek dengan `from` dan `to`.
- `translation.requires_language` bernilai `true` jika terjemahan memerlukan `language` yang eksplisit alih-alih `auto`.
- Jika transkripsi langsung belum dikonfigurasi, responsnya `available: false` dengan hanya `rate`, dan pembuatan sesi menghasilkan `503 live_unavailable`.

Dengan SDK: `await t1.live.capabilities()`.

## Membaca sesi

`GET /v1/live/sessions/:id` mengembalikan keadaan sebuah sesi. Scope yang dibutuhkan adalah `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` bernilai `waiting`, `connected`, `disconnected`, atau `ended`. `end_reason` bernilai `null` sampai sesi berakhir. Dengan SDK: `await t1.live.get(id)`.

## Error

Error request memakai [struktur error](https://transcript1.com/id/docs/errors) yang biasa. Berikut error yang dikembalikan `POST /v1/live/sessions` dan endpoint pendampingnya:

| Status HTTP | `code` | Bisa dicoba ulang | Kapan |
|---|---|---|---|
| 422 | `invalid_request` | Tidak | Ada field yang hilang atau tidak valid, misalnya `sample_rate` di luar 8000 sampai 48000. |
| 422 | `unsupported_language` | Tidak | `language` tidak didukung untuk transkripsi langsung. |
| 422 | `unsupported_translation` | Tidak | Bahasa tujuan, atau pasangan bahasanya, tidak didukung. |
| 402 | `insufficient_minutes` | Tidak | Saldo yang bisa dipakai kurang dari satu menit yang dimulai pada tarif saat ini. |
| 403 | `email_not_verified` | Tidak | Verifikasi alamat email Anda terlebih dahulu. |
| 429 | `too_many_live_sessions` | Ya | Semua sesi langsung bersamaan pada paket Anda sedang terpakai. Tunggu sesuai `Retry-After`. |
| 503 | `live_capacity` | Ya | Seluruh kapasitas langsung sedang terpakai. Tunggu sesuai `Retry-After` (30 detik). |
| 503 | `live_unavailable` | Tidak | Transkripsi langsung sedang tidak tersedia. |
| 409 | `session_ended` | Tidak | `POST /v1/live/sessions/:id/token` untuk sesi yang sudah berakhir. |

Masalah di socket datang sebagai event `error` dan [kode penutupan](https://transcript1.com/id/docs/live#close-codes).

## Transkrip tersimpan

Dengan `save: true` (bawaan), sesi disimpan sebagai transkripsi biasa saat berakhir. Audio tidak pernah disimpan.

- `transcription_id` dikembalikan saat Anda membuat sesi. Statusnya `live` selama sesi berjalan dan `completed` setelah berakhir. Sesi yang tidak pernah terhubung meninggalkan transkripsi berstatus `canceled`.
- Transkripsi memiliki `source: { "platform": "live", "session_id": "lv_…" }`. Tidak ada tautan atau file. Dasbor menampilkannya sebagai sesi langsung.
- Setiap baris menjadi satu segmen, dengan indeks yang sama dengan `id` baris. Baca transkrip sebagai JSON, teks, SRT, atau VTT seperti transkripsi lain: lihat [referensi API](https://transcript1.com/id/docs/api-reference).
- Terjemahan langsung ikut tersimpan. Bacalah dengan parameter `translation`, seperti dijelaskan di [Terjemahan](https://transcript1.com/id/docs/translation#read-the-translation).
- Tambahkan bahasa terjemahan lain nanti dengan `POST /v1/transcriptions/:id/translations` setelah transkripsi berstatus `completed`. Lihat [Terjemahan](https://transcript1.com/id/docs/translation#translate-an-existing-transcript).

Dengan `save: false`, tidak ada yang disimpan setelah sesi berakhir: tidak ada transkripsi dan `transcription_id` bernilai `null`. Anda tetap menerima setiap baris lewat socket dan tagihan biasa tetap berlaku.

## Keamanan

- Token yang Anda terima saat membuat sesi hanya berlaku sekali dan kedaluwarsa dalam 60 detik. Token dikirim sebagai subprotokol WebSocket, bukan di URL, sehingga tidak masuk log akses. Token yang bocor tidak berguna setelah dipakai atau kedaluwarsa.
- Gunakan kunci API hanya dari server yang Anda kendalikan. Jangan pernah menaruh kunci API di halaman web atau ekstensi.
- Di browser atau ekstensi, masukkan pengguna dengan OAuth dan teruskan access token OAuth ke `getAccessToken` pada `createLiveClient`. Token memerlukan scope `transcriptions:write` untuk memulai sesi. Access token OAuth kedaluwarsa setelah 1 jam, dan setiap kelanjutan sesi meminta token baru dari `getAccessToken`.
- Server Anda juga bisa membuat sesi dengan kunci API lalu hanya memberikan `websocket_url` dan `token` ke browser. Klien kemudian terhubung langsung. Klien tidak bisa melanjutkan sesi tanpa token baru dari Anda.
