wsUrl로 여는 WebSocket 연결의 규칙을 다뤄요. 세션 생성, 조회, 목록, 종료 REST API는 API 레퍼런스에 있어요.
세션과 연결은 어떻게 구분되나요?
세션은 REST로 만든 전사 단위이고, 연결은 그 세션에 붙는 WebSocket 하나예요. 세션은OPEN과 ENDED 두 상태만 가져요. 연결은 세션이 OPEN인 동안 끊겼다가 다시 붙을 수 있어요.
- 세션이
ENDED가 되면 되돌릴 수 없어요. 계속 전사하려면 새 세션을 만드세요. - 연결이 끊겨도 세션은
OPEN으로 남아요. 같은wsUrl과streamToken으로 다시 연결하세요. - 세션이 끝나는 사유는
endReason으로 받아요. 값은session.ended에 정리했어요.
연결
POST /v1/external/realtime-transcriptions 응답의 wsUrl에 Authorization: Bearer <streamToken> 헤더를 넣어 연결해요.
wsUrl은 받은 그대로 쓰세요. 지금은wss://<host>/v1/external/realtime-transcriptions/{id}/stream모양이지만 호스트와 경로는 바뀔 수 있어요.- 토큰은
Authorization헤더로만 받아요. 쿼리 문자열, 쿠키, API Key로는 연결할 수 없어요. 그래서 헤더를 넣을 수 없는 브라우저 WebSocket API로는 연결할 수 없어요. - Origin 헤더는 검사하지 않아요.
streamToken은 그 세션에만 쓸 수 있고, 세션이OPEN인 동안 다시 연결할 때도 같은 토큰을 써요. 세션 생성 응답에서만 받을 수 있고 다시 발급하는 API는 없어요. 토큰을 잃어버렸다면 세션을 닫고 새 세션을 만드세요.
error 메시지를 보낸 뒤 연결을 닫아요. HTTP 상태로 거절하는 경우는 아래 세 가지뿐이에요.
연결을 수락하면 Tiro는 토큰, 세션 상태, 토큰을 발급한 API Key, 워크스페이스 정책을 확인해요. 세션 도중 정책이 꺼지거나 API Key가 삭제되면 Tiro는 열려 있는 연결을 끊지 않고 다음 연결부터 거절해요.
오디오 프레임
오디오는 바이너리 프레임으로, 제어 메시지는 텍스트 프레임으로 보내요. 텍스트 프레임 하나에는 UTF-8 JSON 객체 하나를 담아요.session.started를 받기 전에 오디오를 보내도 돼요. Tiro는 전사 준비가 끝나기 전에 받은 오디오를 보관했다가 이어서 처리해요.
오디오 선행량은 어떻게 계산하나요?
오디오 선행량은 보낸 오디오 길이가 실제 경과 시간보다 얼마나 앞서 있는지를 뜻해요. Tiro는 이 값을 연결마다 재고, 60초를 넘으면 연결을 닫아요.- 쉬는 동안에는 여유가 쌓이지 않아요. 1분 동안 아무것도 보내지 않았다가 2분 분량을 한 번에 보내도 60초를 넘어요.
- 재접속하면 직전 연결의 선행량을 이어받아요. 재접속으로 한도를 다시 받을 수는 없어요.
- 60초를 넘으면
error(audio_rate_exceeded)의retryAfterMs에 넘친 양이 담겨요. 그 시간 이상 기다린 뒤 다시 연결하고 실시간 속도로 보내세요.
메시지
모든 텍스트 메시지에는type 필드가 있어요. 길이와 오프셋은 이름이 Ms로 끝나는 정수 밀리초이고, 시각은 이름이 At으로 끝나는 ISO-8601 UTC 문자열이에요. 로케일은 ko_KR 같은 지원 로케일 표기를 써요.
순서. 같은
segmentId의 transcript와 같은 translationId의 translation은 보낸 순서대로 도착해요. isFinal: false가 0번 이상 온 뒤 isFinal: true가 한 번 와요. 서로 다른 segment 사이, 그리고 transcript와 translation 사이의 순서는 보장하지 않아요.
호환성. Tiro는 메시지 종류와 필드를 추가할 수 있어요. 필드를 지우거나 이름, 타입, 의미를 바꾸지는 않아요. 클라이언트는 모르는 type, 필드, reason, endReason, status 값을 무시하거나 기본값으로 처리하세요.
session.end
클라이언트가 세션을 끝낼 때 보내요. 여러 번 보내도 두 번째부터는 무시해요. 이후 절차는 종료를 보세요.
session.started
연결마다 첫 메시지로 와요. 보통 1초 안팎에 오고, 드물게 수십 초 걸려요.
transcript
전사 결과예요.
- 같은
segmentId의isFinal: false메시지가 오면 그 segment의 텍스트를 통째로 바꾸세요. isFinal: true는 segment마다 정확히 한 번 와요. 그 뒤로 그 segment의 메시지는 오지 않아요.isFinal: false없이isFinal: true만 올 수도 있어요.- segment 크기는 Tiro가 정하고 약속하지 않아요. 지금은 발화가 끝나는 지점에서 나뉘어요.
translation
번역 결과예요. 세션에 translationLocale을 지정했을 때만 와요.
- 교체 규칙은
transcript와 같아요. 같은translationId의isFinal: false는 이전 텍스트를 통째로 바꾸고,isFinal: true는 한 번만 와요. - 번역 조각은 원문 segment와 1:1로 짝지어지지 않아요. 조각 하나가 여러 segment를 덮거나, segment 하나가 여러 조각으로 나뉠 수 있어요.
- 확정된 조각의
text를 도착한 순서대로 구분자 없이 이어 붙이면 번역문이 돼요. 조각은 단어 중간에서 나뉠 수 있으니 조각 사이에 공백을 넣지 마세요. 공백만 있는 확정 조각은 띄어쓰기라서 그대로 이어 붙이세요. - 번역은 원문 확정보다 늦게 올 수도, 원문의
isFinal: false보다 먼저 올 수도 있어요. - 자동 감지 세션에서
transcript.locale이translationLocale과 같은 segment는 번역하지 않아요. 언어를 지정한 세션에서도 번역 대상 언어로 말한 구간은 번역이 빠질 수 있어요.
session.ended
세션이 끝났다는 메시지예요. 세션의 마지막 메시지이고, Tiro는 이 메시지를 보낸 뒤 close 1000으로 연결을 닫아요. 이 메시지를 받으면 다시 연결하지 마세요.
연결이 없을 때 끝난 세션은 이 메시지를 보낼 곳이 없어요. 그때는
GET /v1/external/realtime-transcriptions/{id}로 상태를 확인하세요. 끝난 세션에 연결하면 error(session_ended)를 받아요.
error
오류를 알리는 메시지예요.
종료
session.end를 보내야 마지막 결과까지 모두 받을 수 있어요.
1
session.end 보내기
마지막 오디오 프레임을 보낸 뒤
{"type": "session.end"}를 보내요. 이후 보낸 오디오는 무시해요.2
남은 결과 받기
Tiro가 남은
transcript와 translation을 모두 isFinal: true로 보내요. 아직 확정되지 않은 segment와 번역 조각은 마지막 텍스트로 확정해서 보내요.3
session.ended 받기
Tiro가
session.ended를 보내고 close 1000으로 연결을 닫아요.session.ended를 받을 때까지 연결을 유지하세요. 남은 결과를 보내는 시간은 번역을 포함해 최대 30초예요. 30초 안에 끝나지 않으면 Tiro는 그때까지 보낸 결과로 session.ended를 보내고 incomplete: true를 붙여요.
REST로 세션을 닫거나 타임아웃에 걸려도 연결이 있으면 같은 순서로 끝나요. 이때 Tiro에 도착했지만 아직 전사를 시작하지 않은 오디오는 결과에 빠질 수 있어요.
WebSocket 연결만 닫으면 세션은 OPEN으로 남고 워크스페이스 동시 세션 한 자리를 차지해요. 연결이 그냥 끊기면 그 순간 확정되지 않은 결과도 받지 못해요.
타임아웃
- 텍스트 메시지는 오디오 없음 타이머를 초기화하지 않아요.
- 음소거하는 동안 연결을 유지한 채 전송을 멈춰도 돼요. 15분 안에 다시 보내면 세션이 이어져요.
- Tiro는 타임아웃 시점보다 늦게 세션을 끝낼 수 있어요. 연결이 있는 세션은 최대 1분, 연결이 없는 세션은 최대 3분쯤 늦어요.
재접속
세션이OPEN이면 연결이 끊겨도 같은 wsUrl과 streamToken으로 다시 연결해 이어 갈 수 있어요. 다시 연결할지는 마지막으로 받은 메시지로 정하고, close code로 분기하지 마세요.
session.ended를 받았으면 세션이 끝난 거예요. 다시 연결하지 마세요.- 연결을 닫은
error(fatal: true)가retryable: false면 다시 연결하지 마세요. 연결이 유지되는 오류(fatal: false, 예:invalid_message)는 이 판단에 넣지 마세요. - 그 밖의 경우에는 다시 연결하세요.
retryable: true인error를 받았거나 메시지 없이 끊긴 경우예요. close1012는 서버 재시작이라 바로 다시 연결하고, 나머지는 백오프하세요.
retryAfterMs가 있으면 그 시간 이상 기다리세요. 세션이 OPEN인 동안에는 재접속 횟수 제한이 없어요.
재접속에 성공하면 session.started가 resumed: true로 와요. 세션이 이미 끝났으면 error(session_ended, retryable: false)를 받아요.
끊긴 동안의 오디오는 어떻게 다시 보내나요?
Tiro는 오디오를 다시 요청하지 않아요. 보내지 못한 오디오는 세션에 남지 않으니, 클라이언트가 직접 모아 두었다가 다시 보내야 해요.- 연결이 끊긴 동안 최근 오디오를 최대 60초까지 모아 두세요. 그보다 오래된 앞부분은 버리세요.
- 재접속하면
session.started의audioOffsetMs를 확인하세요. 이 값은 Tiro가 처리를 마친 오디오 길이라서, 직전 연결로 실제 보낸 길이보다 짧을 수 있어요. Tiro가 직전 연결이 끊긴 것을 알기 전에 새 연결이 기존 연결을 대체하면0일 수도 있어요. - 세션 타임라인에서
audioOffsetMs이후에 해당하는 오디오를 버퍼에서 먼저 보내고, 이어서 실시간 오디오를 보내세요. 이렇게 보내도 오디오 선행량 60초 안이에요. - Tiro는 다시 보낸 오디오가 이미 받은 오디오인지 판단하지 않아요.
- close
1012는 메시지 없이 닫혀서 직전에 보낸 프레임 일부가 Tiro에 닿지 않았을 수 있어요. 이때도audioOffsetMs이후 오디오를 다시 보내세요. - 끊기기 직전까지
isFinal: false였던 segment와 번역 조각은 버리세요. Tiro가 새 연결에서 다시 보내지 않아요.
같은 세션에 두 번 연결하면 어떻게 되나요?
세션 하나에는 연결이 하나만 붙어요. 같은 토큰으로 새 연결이 들어오면 Tiro는 새 연결을 쓰고 기존 연결을 닫아요.- 기존 연결은
error(connection_replaced,retryable: false)를 받고 close4409로 닫혀요. 기존 연결의 클라이언트는 다시 연결하지 마세요. - 새 연결은
session.started를resumed: true로 받고 세션을 이어 가요.
WebSocket 에러
reason은 늘어날 수 있어요. 모르는 reason을 받아도 fatal과 retryable 값을 따르세요.
close code
아래 표는 로그를 볼 때 참고하세요. 재접속 여부는 재접속 규칙으로 정하고 close code로 분기하지 마세요. close code는 늘어날 수 있어요.REST 에러
세션 REST API는 에러 처리의 공통 형식으로 에러를 돌려줘요. 분기에는detail.reason을 쓰세요. detail.reason이 없는 응답은 HTTP 상태로 분기하세요. 엔드포인트별 에러는 API 레퍼런스에 있어요.