일반 STT
일반 STT API는 음성 파일을 텍스트로 변환하는 HTTP 기반의 REST API입니다.
지원 포맷
일반 STT API는 음성 파일 포맷 mp4, m4a, mp3, amr, flac, wav을 지원하고 있습니다.
인증 토큰 발급
일반 STT API는 인증 가이드를 통해 토큰을 발급받은 뒤 사용하실 수 있습니다.
API 목록
Method | URL | Description |
---|---|---|
POST | /v1/transcribe | 파일 전사 요청 |
GET | /v1/transcribe/{TRANSCRIBE_ID} | 파일 전사 결과 조회 |
1) [POST] /v1/transcribe
저장된 음성 파일에 대해 전사를 요청하는 API입니다.
HTTP 요청
POST https://openapi.vito.ai/v1/transcribe
요청 헤더
Authorization: Bearer {YOUR_JWT_TOKEN}
- scheme: bearer
- bearerFormat: JWT
요청 바디 (Request body)
content-type: multipart/form-data
Field | Type | Required |
---|---|---|
config | RequestConfig | required |
file | Binary | required |
RequestConfig
Name | Desc | Type | Required | Value | Default |
---|---|---|---|---|---|
model_name | 음성인식 모델 | string | optional | sommers, whisper | sommers |
language | 음성인식 언어, whisper 전용 | string | optional | ko, detect, multi 등 | ko |
language_candidates | 언어 감지 후보군, language 가 detect 또는 multi일 때만 적용 | array | optional | ["ko","ja","zh","en"] | |
use_diarization | 화자 분리 사용 여부 | boolean | optional | false | |
diarization.spk_count | 화자수, use_diarization 이 true일 때만 적용 | integer | optional | 0 이상의 정수 | 0 (화자수 예측) |
use_itn | 영어/숫자/단위 변환 여부 | boolean | optional | true | |
use_disfluency_filter | 간투어 필터 사용 여부 | boolean | optional | true | |
use_profanity_filter | 비속어 필터 사용 여부 | boolean | optional | false | |
use_paragraph_splitter | 문단 나누기 사용 여부 | boolean | optional | true | |
paragraph_splitter.max | 문단의 최대 문자 길이, use_paragraph_splitter 이 true일 때만 적용 | integer | optional | 1 이상의 정수 | 50 |
domain | 음성파일의 종류 (도메인) | string | optional | GENERAL, CALL | GENERAL |
use_word_timestamp | 단어별 Timestamp 사용 여부 | boolean | optional | false | |
keywords | 키워드 부스팅용 단어 리스트 | array | optional |
일반 STT API의 경우 아래와 같은 제약 사항이 있습니다.
샘플 코드 1
- cURL
- Python
- Java
curl -X "POST" \
"https://openapi.vito.ai/v1/transcribe" \
-H "accept: application/json" \
-H "Authorization: Bearer ${YOUR_JWT_TOKEN}" \
-H "Content-Type: multipart/form-data" \
-F "file=@sample.wav" \
-F 'config={}'
import json
import requests
import os
# Read JWT token from environment
jwt_token = os.getenv("YOUR_JWT_TOKEN")
if not jwt_token:
raise ValueError("Environment variable 'YOUR_JWT_TOKEN' is not set")
config = {}
resp = requests.post(
"https://openapi.vito.ai/v1/transcribe",
headers={"Authorization": f"Bearer {jwt_token}"},
data={"config": json.dumps(config)},
files={"file": open("sample.wav", "rb")},
)
resp.raise_for_status()
print(resp.json())
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Scanner;
public class PostTranscribeSample {
public static void main(String[] args) throws Exception {
URL url = new URL("https://openapi.vito.ai/v1/transcribe");
HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
httpConn.setRequestMethod("POST");
httpConn.setRequestProperty("accept", "application/json");
httpConn.setRequestProperty("Authorization", "Bearer "+ "{YOUR_JWT_TOKEN}");
httpConn.setRequestProperty("Content-Type", "multipart/form-data;boundary=authsample");
httpConn.setDoOutput(true);
File file = new File("sample.wav");
try (DataOutputStream outputStream = new DataOutputStream(httpConn.getOutputStream());
FileInputStream in = new FileInputStream(file)) {
// File part
outputStream.writeBytes("--authsample\r\n");
outputStream.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + file.getName() + "\"\r\n");
outputStream.writeBytes("Content-Type: " + URLConnection.guessContentTypeFromName(file.getName()) + "\r\n");
outputStream.writeBytes("Content-Transfer-Encoding: binary\r\n\r\n");
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.writeBytes("\r\n");
// Config part
outputStream.writeBytes("--authsample\r\n");
outputStream.writeBytes("Content-Disposition: form-data; name=\"config\"\r\n");
outputStream.writeBytes("Content-Type: application/json\r\n\r\n");
outputStream.writeBytes("{}\r\n");
// End boundary
outputStream.writeBytes("--authsample--\r\n");
outputStream.flush();
}
InputStream responseStream = httpConn.getResponseCode() / 100 == 2
? httpConn.getInputStream()
: httpConn.getErrorStream();
Scanner s = new Scanner(responseStream).useDelimiter("\\A");
String response = s.hasNext() ? s.next() : "";
s.close();
System.out.println(response);
}
}
샘플 코드 2
- cURL
- Python
- Java
curl -X "POST" \
"https://openapi.vito.ai/v1/transcribe" \
-H "accept: application/json" \
-H "Authorization: Bearer ${YOUR_JWT_TOKEN}" \
-H "Content-Type: multipart/form-data" \
-F "file=@sample.wav" \
-F 'config={
"use_diarization": true,
"diarization": {
"spk_count": 2
},
"use_itn": false,
"use_disfluency_filter": false,
"use_profanity_filter": false,
"use_paragraph_splitter": true,
"paragraph_splitter": {
"max": 50
}
}'
import json
import requests
import os
# Read JWT token from environment
jwt_token = os.getenv("YOUR_JWT_TOKEN")
if not jwt_token:
raise ValueError("Environment variable 'YOUR_JWT_TOKEN' is not set")
config = {
"use_diarization": True,
"diarization": {"spk_count": 2},
"use_itn": False,
"use_disfluency_filter": False,
"use_profanity_filter": False,
"use_paragraph_splitter": True,
"paragraph_splitter": {"max": 50},
}
resp = requests.post(
"https://openapi.vito.ai/v1/transcribe",
headers={"Authorization": f"Bearer {jwt_token}"},
data={"config": json.dumps(config)},
files={"file": open("sample.wav", "rb")},
)
resp.raise_for_status()
print(resp.json())
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Scanner;
public class PostTranscribeSample {
public static void main(String[] args) throws Exception {
URL url = new URL("https://openapi.vito.ai/v1/transcribe");
HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
httpConn.setRequestMethod("POST");
httpConn.setRequestProperty("accept", "application/json");
httpConn.setRequestProperty("Authorization", "Bearer "+ "{YOUR_JWT_TOKEN}");
httpConn.setRequestProperty("Content-Type", "multipart/form-data;boundary=authsample");
httpConn.setDoOutput(true);
File file = new File("sample.wav");
try (DataOutputStream outputStream = new DataOutputStream(httpConn.getOutputStream());
FileInputStream in = new FileInputStream(file)) {
// File part
outputStream.writeBytes("--authsample\r\n");
outputStream.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + file.getName() + "\"\r\n");
outputStream.writeBytes("Content-Type: " + URLConnection.guessContentTypeFromName(file.getName()) + "\r\n");
outputStream.writeBytes("Content-Transfer-Encoding: binary\r\n\r\n");
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.writeBytes("\r\n");
// Config part
outputStream.writeBytes("--authsample\r\n");
outputStream.writeBytes("Content-Disposition: form-data; name=\"config\"\r\n");
outputStream.writeBytes("Content-Type: application/json\r\n\r\n");
outputStream.writeBytes(
"{\n" +
" \"use_diarization\": true,\n" +
" \"diarization\": {\"spk_count\": 2},\n" +
" \"use_itn\": false,\n" +
" \"use_disfluency_filter\": false,\n" +
" \"use_profanity_filter\": false,\n" +
" \"use_paragraph_splitter\": true,\n" +
" \"paragraph_splitter\": {\"max\": 50}\n" +
"}\r\n"
);
// End boundary
outputStream.writeBytes("--authsample--\r\n");
outputStream.flush();
}
InputStream responseStream = httpConn.getResponseCode() / 100 == 2
? httpConn.getInputStream()
: httpConn.getErrorStream();
Scanner s = new Scanner(responseStream).useDelimiter("\\A");
String response = s.hasNext() ? s.next() : "";
s.close();
System.out.println(response);
}
}
응답 바디 (Response Body)
응답이 성공한 경우 HTTP Status 200과 함께 아래와 같은 응답을 내려줍니다.
{
"id": "{TRANSCRIBE_ID}"
}
오류 코드
HTTP Status | Code | Notes |
---|---|---|
400 | H0001 | 잘못된 파라미터 요청 |
400 | H0010 | 지원하지 않는 파일 포맷 |
401 | H0002 | 유효하지 않은 토큰 |
413 | H0005 | 파일 사이즈 초과 |
413 | H0006 | 파일 길이 초과 |
429 | A0001 | 사용량 초과 |
429 | A0002 | 동시 처리 제한 초과 |
500 | E500 | 서버 오류 |
아래는 응답이 실패한 경우 가운데 하나의 예시입니다.
{
"code": "H0001",
"msg": "unexpected end of JSON input"
}
2) [GET] /v1/transcribe/{TRANSCRIBE_ID}
- 음성 파일의 전사 결과를 조회하는 API입니다. 전사 요청 API에서 응답받은
TRANSCRIBE_ID
를 사용하여 전사 결과를 조회할 수 있습니다.
HTTP 요청
GET https://openapi.vito.ai/v1/transcribe/{TRANSCRIBE_ID}
요청 헤더
Authorization: Bearer {YOUR_JWT_TOKEN}
- scheme: bearer
- bearerFormat: JWT
샘플 코드
- cURL
- Python
- Java
curl -X "GET" \
"https://openapi.vito.ai/v1/transcribe/${TRANSCRIBE_ID}" \
-H "accept: application/json" \
-H "Authorization: Bearer ${YOUR_JWT_TOKEN}"
import requests
resp = requests.get(
"https://openapi.vito.ai/v1/transcribe/" + "{TRANSCRIBE_ID}",
headers={"Authorization": "bearer " + "{YOUR_JWT_TOKEN}"},
)
resp.raise_for_status()
print(resp.json())
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;
public class GetTranscribeSample {
public static void main(String[] args) throws Exception {
URL url = new URL("https://openapi.vito.ai/v1/transcribe/"+"{TRANSCRIBE_ID}");
HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
httpConn.setRequestMethod("GET");
httpConn.setRequestProperty("accept", "application/json");
httpConn.setRequestProperty("Authorization", "Bearer {YOUR_JWT_TOKEN}");
InputStream responseStream = httpConn.getResponseCode() / 100 == 2
? httpConn.getInputStream()
: httpConn.getErrorStream();
Scanner s = new Scanner(responseStream).useDelimiter("\\A");
String response = s.hasNext() ? s.next() : "";
s.close();
System.out.println(response);
}
}
응답 바디 (Response Body)
응답이 성공한 경우 HTTP Status 200과 함께 아래와 같은 응답을 내려줍니다.
Name | Desc | Type | Value |
---|---|---|---|
id | transcribe id | string | |
status | 전사 결과 상태 | string | transcribing , completed , failed |
results.utterances | 발화 정보 | array | |
results.utterances.start_at | 발화 시작 시각 (ms) | integer | |
results.utterances.duration | 발화 지속 시간 (ms) | integer | |
results.utterances.msg | 발화 텍스트 | string | |
results.utterances.spk | 화자/채널 ID | integer | |
results.utterances.lang | language 로 설정한 언어, 또는 detect/multi인 경우에는 모델이 예측한 언어 | string | ISO 639-1 language code |
일반 STT API의 경우, 긴 음성 파일도 지원하기 위하여 Polling 방식으로 구현되어 있습니다. 전사 요청 API에서 응답받은 {TRANSCRIBE_ID}의 상태 값이 transcribing
인 경우, 최종 상태(completed
또는 failed
)가 될 때까지 주기적으로 조회하여 변환 결과를 확인할 수 있습니다. 권장하는 Polling 주기는 5초입니다.
(Polling 주기가 너무 짧을 경우 HTTP Status 429로 요청 제한 초과 응답이 내려갈 수 있습니다.)
status: transcribing
{
"id": "{TRANSCRIBE_ID}",
"status": "transcribing"
}
status: completed
{
"id": "{TRANSCRIBE_ID}",
"status": "completed",
"results": {
"utterances": [
{
"start_at": 4737,
"duration": 2360,
"msg": "안녕하세요.",
"spk": 0,
"lang": "ko"
},
{
"start_at": 8197,
"duration": 3280,
"msg": "네, 안녕하세요? 반갑습니다.",
"spk": 1,
"lang": "ko"
}
]
}
}
status: failed
응답은 성공했지만 전사가 실패한 경우 아래와 같은 응답을 내려줍니다.
{
"id": "{TRANSCRIBE_ID}",
"status": "failed",
"error": {
"code": "{ERROR_CODE}",
"message": "{MESSAGE}"
}
}
아래는 예시입니다.
{
"id": "ZbOOQftrS1ywK_T3ikuveA",
"status": "failed",
"error": {
"code": "E500",
"message": "internal server error"
}
}
위와 같은 경우가 지속적으로 발생할 경우, 문의해 주시기 바랍니다.
오류 코드
HttpStatus | Code | Notes |
---|---|---|
400 | H0001 | 잘못된 파라미터 요청 |
401 | H0002 | 유효하지 않은 토큰 |
403 | H0003 | 권한 없음 |
404 | H0004 | 전사 결과 없음 |
410 | H0007 | 전사 결과 만료됨 |
429 | A0003 | 요청 제한 초과 |
500 | E500 | 서버 오류 |
아래는 응답이 실패한 경우 가운데 하나의 예시입니다.
{
"code": "H0004",
"msg": "not found"
}
샘플 코드 (단일 예제 + 프리셋)
아래 하나의 예제 스크립트에서 PRESET
환경변수로 원하는 설정을 조합할 수 있습니다. 기본은 sommers_basic
입니다.
- Python (recommended)
import json
import os
import time
from typing import Any, Dict, Optional
import requests
class RTZROpenAPIClient:
"""Minimal client for RTZR OpenAPI (auth + STT file).
- Fetches JWT via /v1/authenticate using client_id/client_secret
- Submits a file transcription job via /v1/transcribe
- Polls /v1/transcribe/{id} every few seconds until completed/failed
"""
def __init__(
self,
client_id: Optional[str] = None,
client_secret: Optional[str] = None,
base_url: str = "https://openapi.vito.ai",
) -> None:
self.base_url = base_url.rstrip("/")
self.client_id = client_id or os.getenv("RTZR_CLIENT_ID")
self.client_secret = client_secret or os.getenv("RTZR_CLIENT_SECRET")
if not self.client_id or not self.client_secret:
raise ValueError(
"Missing credentials. Set RTZR_CLIENT_ID and RTZR_CLIENT_SECRET "
"environment variables, or pass client_id/client_secret to RTZROpenAPIClient."
)
self._sess = requests.Session()
self._token: Optional[Dict[str, Any]] = None
@property
def token(self) -> str:
# Renew if missing or expiring within 30 minutes
if self._token is None or self._token.get("expire_at", 0) < time.time() - 1800:
resp = self._sess.post(
f"{self.base_url}/v1/authenticate",
data={"client_id": self.client_id, "client_secret": self.client_secret},
)
resp.raise_for_status()
self._token = resp.json()
access = self._token.get("access_token")
if not access:
raise RuntimeError("authenticate: 'access_token' not found in response")
return access
def _auth_headers(self) -> Dict[str, str]:
return {"Authorization": f"Bearer {self.token}"}
def transcribe_file(self, file_path: str, config: Dict[str, Any]) -> Dict[str, Any]:
url = f"{self.base_url}/v1/transcribe"
with open(file_path, "rb") as f:
files = {"file": (os.path.basename(file_path), f)}
data = {"config": json.dumps(config)}
resp = self._sess.post(url, headers=self._auth_headers(), files=files, data=data)
resp.raise_for_status()
return resp.json()
def get_transcription(self, transcribe_id: str) -> Dict[str, Any]:
url = f"{self.base_url}/v1/transcribe/{transcribe_id}"
resp = self._sess.get(url, headers=self._auth_headers())
resp.raise_for_status()
return resp.json()
def wait_for_result(
self,
transcribe_id: str,
poll_interval_sec: int = 5,
timeout_sec: int = 3600,
) -> Dict[str, Any]:
deadline = time.time() + timeout_sec
while True:
if time.time() > deadline:
raise TimeoutError("Timed out waiting for transcription result")
result = self.get_transcription(transcribe_id)
status = result.get("status")
if status in ("completed", "failed"):
return result
time.sleep(poll_interval_sec)
# Preset configurations
PRESETS: Dict[str, Dict[str, Any]] = {
"sommers_basic": { # 1) sommers without diarization
"model_name": "sommers",
"use_diarization": False,
"domain": "GENERAL",
},
"sommers_call_diarization": { # 2) sommers + diarization + CALL, spk_count=2
"model_name": "sommers",
"domain": "CALL",
"use_diarization": True,
"diarization": {"spk_count": 2},
},
"whisper_en_diarization": { # 3) whisper + diarization, language=en
"model_name": "whisper",
"language": "en",
"use_diarization": True,
},
# Additional commonly requested options
"paragraph_split_80": {"use_paragraph_splitter": True, "paragraph_splitter": {"max": 80}},
"keywords_example": {"keywords": ["stt", "returnzero", "api"]},
"with_word_timestamps": {"use_word_timestamp": True},
"disfluency_on": {"use_disfluency_filter": True},
"profanity_on": {"use_profanity_filter": True},
"whisper_detect_multi": {
"model_name": "whisper",
"language": "multi",
"language_candidates": ["ko", "en", "ja"],
},
}
def main():
audio_path = os.getenv("AUDIO_PATH", "sample.wav")
preset_name = os.getenv("PRESET", "sommers_basic")
if preset_name not in PRESETS:
raise ValueError(f"Unknown PRESET '{preset_name}'. Available: {sorted(PRESETS.keys())}")
config = PRESETS[preset_name]
client = RTZROpenAPIClient()
submit = client.transcribe_file(audio_path, config)
transcribe_id = submit.get("id")
result = client.wait_for_result(transcribe_id, poll_interval_sec=5)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
사용 가능한 프리셋 목록은 다음과 같습니다.
- sommers_basic: model_name=sommers, use_diarization=false, domain=GENERAL
- sommers_call_diarization: model_name=sommers, domain=CALL, use_diarization=true, diarization.spk_count=2
- whisper_en_diarization: model_name=whisper, language=en, use_diarization=true
- paragraph_split_80: use_paragraph_splitter=true, paragraph_splitter.max=80
- keywords_example: keywords=["stt","returnzero","api"]
- with_word_timestamps: use_word_timestamp=true
- disfluency_on: use_disfluency_filter=true
- profanity_on: use_profanity_filter=true
- whisper_detect_multi: model_name=whisper, language=multi, language_candidates=["ko","en","ja"]