문서실시간 변환

실시간 변환

마이크나 탭 오디오를 WebSocket으로 transcript1에 스트리밍하면, 말하는 동안 자막 줄을 받아요. 실시간 번역도 함께 쓸 수 있어요.

실시간 변환은 사람이 말하는 동안 오디오 스트림을 자막 줄로 바꿔 줘요. 클라이언트가 WebSocket으로 원시 PCM 오디오를 보내면, transcript1이 처음에는 임시 결과였다가 나중에 확정되는 줄을 돌려줘요. translate_to를 추가하면 모든 줄에 번역도 함께 담겨요. 세션이 끝나면 결과는 일반 변환 작업으로 저장돼요.

실시간 변환 전용 요금제는 따로 없어요. 다른 기능과 같은 분량 잔액을 사용해요. 연결 시간을 어떻게 세는지는 과금을 확인하세요.

실시간 변환이 맞는 경우

이럴 때 실시간 변환이럴 때 일반 변환
통화, 강연, 스트림, 지금 보고 있는 영상처럼 오디오가 아직 만들어지는 중이에요.링크나 완성된 파일이 있어요.
말하고 나서 1~2초 안에 자막이 필요해요.결과를 기다릴 수 있고 가장 높은 정확도를 원해요.
각 줄 옆에 실시간 번역이 필요해요.완성된 스크립트의 번역이나 여러 언어 번역이 필요해요.

실시간 변환은 원시 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는 이 세션에서 시작한 1분마다 차감되는 잔액 분량이에요. 과금을 참고하세요.

요청 본문에는 다음 필드를 쓸 수 있어요. 모두 선택 사항이고, 필요한 스코프는 transcriptions:write예요.

필드기본값규칙
languageautoauto 또는 실시간 변환이 지원하는 두 글자 언어 코드예요.
translate_to없음두 글자 언어 코드예요. 세션당 번역 대상 언어는 하나예요. 실시간으로 번역하기를 참고하세요.
diarizefalse화자에 라벨을 붙여요. language: "auto"와 함께 쓰는 경우처럼 화자 라벨을 지원하지 않는 조합은 422로 응답해요. 지원 범위를 확인하세요.
audio_formatpcm_s16le유일한 값이에요. 나중에 형식을 추가할 수 있도록 필드를 만들어 뒀어요.
sample_rate160008000에서 48000 사이의 정수예요. 모노만 지원해요.
savetruefalse이면 세션이 끝난 뒤 아무것도 남기지 않아요. 저장된 스크립트를 참고하세요.

연결하고 스트리밍하기

토큰을 두 번째 서브프로토콜로 넣어 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이 없어요. 같은 new WebSocket(url, protocols) 형태를 쓰는 ws 패키지를 사용하세요.

유효한 토큰이면 서버는 t1-live 서브프로토콜만 응답해요. 잘못된 토큰이어도 핸드셰이크가 실패하지는 않아요. 소켓이 열리고 error 이벤트가 온 뒤 4xxx 코드로 닫혀요. 그래서 브라우저에서도 막연한 1006 대신 실제 이유를 볼 수 있어요. 종료 코드를 참고하세요.

브라우저에서 오디오 캡처하기

실시간 오디오는 세션의 sample_rate로 맞춘 16비트 부호 있는 리틀 엔디언 모노 PCM이어야 해요. 브라우저는 부동 소수점 샘플을 주기 때문에 AudioWorklet에서 변환해야 해요. 아래 코드는 마이크를 16 kHz PCM으로 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();
}

이 워크릿은 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을 반환해요. 예를 들어 제공자에 따라 auto 대신 language를 명시해야 하거나, 영어가 포함된 쌍만 허용할 수 있어요.

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로 응답하고 소켓을 닫아요.
그 외코드가 invalid_message인, 치명적이지 않은 error 이벤트가 와요.

홀수 길이이거나 너무 큰 프레임은 프로토콜 위반이라 소켓이 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_soon4분 동안 오디오가 없었어요. 오디오 없이 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줄마다 1씩 늘어나는 0부터 시작하는 번호예요. 저장된 스크립트의 세그먼트 인덱스이기도 해요.
start_ms, end_ms세션 오디오 시간 기준의 시작과 끝이에요. 이 시간은 재연결 후에도 이어져요.
speakerdiarize가 켜져 있으면 화자 라벨, 아니면 null이에요. 라벨은 일반 스크립트와 같아요.
language감지했거나 지정한 줄의 언어예요. 아직 모르면 null이에요.
text지금까지의 원문이에요.
final원문이 더 이상 바뀌지 않으면 true예요.
translationtranslate_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_connected5분 안에 한 번도 연결되지 않았어요. 과금되지 않고 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분 단위이고 최소 1분이에요. 첫 1분은 첫 연결이 수락될 때, 이후 각 분은 시작될 때 과금돼요.
  • 실시간보다 빠르면 오디오 길이로 과금해요. 보낸 오디오가 연결 시간보다 길면 오디오 길이를 기준으로 과금해요.
  • 요율은 API에서 읽어요. 시작한 1분은 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 경고가 먼저 오지만, 자동 충전이 켜져 있으면 오지 않아요.

사용 가능한 잔액이 현재 요율로 시작한 1분보다 적으면 세션 생성이 402 insufficient_minutes를 반환해요. 자동 충전이 켜져 있으면 transcript1이 먼저 충전을 시도해요. 요금제와 한도를 참고하세요.

한도

FreeMonthlyAnnual
동시 실시간 세션 수123
  • 세션은 waiting, connected, disconnected(2분의 재개 유예 안) 상태인 동안 집계돼요. 한도를 넘으면 POST /v1/live/sessions가 Retry-After: 30과 함께 429 too_many_live_sessions를 반환해요.
  • 실시간 세션은 동시 작업 수에 포함되지 않아요.
  • 세션 하나는 연결 시간으로 최대 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이면 번역에 auto 대신 명시적인 language가 필요해요.
  • 실시간 변환이 설정되어 있지 않으면 응답은 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불가사용 가능한 잔액이 현재 요율로 시작한 1분보다 적어요.
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초 뒤에 만료돼요. URL이 아니라 WebSocket 서브프로토콜로 전달하기 때문에 액세스 로그에 남지 않아요. 유출된 토큰도 한 번 쓰거나 만료되면 쓸모가 없어요.
  • API 키는 직접 관리하는 서버에서만 사용하세요. 웹 페이지나 확장 프로그램에는 API 키를 절대 넣지 마세요.
  • 브라우저나 확장 프로그램에서는 사용자를 OAuth로 로그인시키고, OAuth 액세스 토큰을 createLiveClient의 getAccessToken에 전달하세요. 세션을 시작하려면 토큰에 transcriptions:write 스코프가 필요해요. OAuth 액세스 토큰은 1시간 뒤에 만료되며, 재개할 때마다 getAccessToken에서 새 토큰을 받아 와요.
  • 서버가 API 키로 세션을 만들고 websocket_url과 token만 브라우저에 넘길 수도 있어요. 그러면 클라이언트가 직접 연결해요. 이 경우 클라이언트는 서버에서 새 토큰을 받지 않으면 재개할 수 없어요.

최근 업데이트 2026년 11월 3일