# 일반 STT 일반 STT API는 음성 파일을 텍스트로 변환하는 HTTP 기반 REST API입니다. ## 지원 포맷[​](#supported-formats "지원 포맷에 대한 직접 링크") 팁 일반 STT API는 mp4, m4a, mp3, amr, flac, wav 음성 파일 형식을 지원합니다. ## 인증 토큰 발급[​](#authentication-token "인증 토큰 발급에 대한 직접 링크") 일반 STT API는 [인증 가이드](/docs/authentications.md)를 통해 토큰을 발급한 후 사용할 수 있습니다.
# API 목록 | Method | URL | Description | | ------ | ------------------------------------------------------------------- | ------------------- | | POST | [/v1/transcribe](#1-post-v1transcribe) | 파일 전사 요청 | | GET | [/v1/transcribe/{TRANSCRIBE\_ID}](#2-get-v1transcribetranscribe_id) | 파일 전사 결과 조회 | ## 1) \[POST] /v1/transcribe[​](#1-post-v1transcribe "1) \[POST] /v1/transcribe에 대한 직접 링크") 저장된 음성 파일에 대해 전사를 요청하는 API입니다. ### HTTP 요청[​](#http-request "HTTP 요청에 대한 직접 링크") ```less POST https://openapi.vito.ai/v1/transcribe ``` ### 요청 헤더[​](#request-headers "요청 헤더에 대한 직접 링크") ```less Authorization: Bearer {YOUR_JWT_TOKEN} ``` * scheme: bearer * bearerFormat: JWT ### 요청 바디 (Request body)[​](#request-body "요청 바디 (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 | [음성 인식 모델](/docs/stt-file/model.md) | string | optional | `sommers`, `whisper` | `sommers` | | language | [음성 인식 언어](/docs/stt-file/language.md). `whisper` 모델 사용 시 필수. `sommers` 지원 언어는 `ko`, `ja` | string | optional | `ko`, `ja`, (`whisper`) `detect`, `multi` | `ko` | | language\_candidates | [언어 감지 후보군](/docs/stt-file/language.md#language_candidates-option). `language`가 `detect` 또는 `multi`일 때만 적용 | array | optional | | \["ko", "ja", "zh", "en"] | | use\_diarization | [화자 분리](/docs/stt-file/diarization.md) 사용 여부 | boolean | optional | | false | | diarization.spk\_count | 화자 수. `use_diarization`이 `true`일 때만 적용 | integer | optional | 0 이상의 정수 | 0 (화자 수 예측) | | use\_itn | [영어/숫자/단위 표기 변환](/docs/stt-file/itn.md) 사용 여부 | boolean | optional | | true | | use\_disfluency\_filter | [간투어 필터](/docs/stt-file/disfluency.md) 사용 여부 | boolean | optional | | true | | use\_profanity\_filter | [비속어 필터](/docs/stt-file/profanity.md) 사용 여부 | boolean | optional | | false | | use\_paragraph\_splitter | [문단 나누기](/docs/stt-file/paragraph-splitter.md) 사용 여부 | boolean | optional | | true | | paragraph\_splitter.max | 문단 최대 글자 수. `use_paragraph_splitter`이 `true`일 때만 적용 | integer | optional | 1 이상의 정수 | 50 | | domain | 음성 파일 유형 ([도메인](/docs/stt-file/domain.md)) | string | optional | `GENERAL`, `CALL` | `GENERAL` | | use\_word\_timestamp | [단어 단위 타임스탬프](/docs/stt-file/word_timestamp.md) 사용 여부 | boolean | optional | | false | | keywords | [키워드 부스팅](/docs/stt-file/keywords.md)용 단어 목록 | array | optional | | | 주의 일반 STT API에는 다음과 같은 제약이 있습니다.
1. POST API 동시처리 제한: 동시처리 가능한 파일 개수는 [처리량 제한 정책](/docs/rate_limit.md)을 따릅니다. POST API로 요청한 뒤 처리가 완료되기 전까지의 요청 개수를 의미하며, API 처리에 대한 완료는 아래 [GET API](#2-get-v1transcribetranscribe_id)를 통해 확인 가능합니다.
2. 최대 인식파일 크기: 2GB, 최대 인식가능 시간: 4시간.
3. 전사 작업은 요청 순서에 따라 순차적으로 처리됩니다. 최대 인식 가능 시간인 4시간 분량의 음성 파일도 일반적으로 5분 이내에 작업이 시작되지만, 요청이 많은 시간대에는 작업 시작까지 최대 30분 이상 소요될 수 있습니다. ### 샘플 코드 1[​](#sample-code-1 "샘플 코드 1에 대한 직접 링크") * cURL * Python * Java transcribe.sh ```bash 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={}' ``` transcribe.py ```python 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()) ``` transcribe.java ```java 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[​](#sample-code-2 "샘플 코드 2에 대한 직접 링크") * cURL * Python * Java transcribe.sh ```bash 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 } }' ``` transcribe.py ```python 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()) ``` transcribe.java ```java 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)[​](#response-body "응답 바디 (Response Body)에 대한 직접 링크") 응답이 성공한 경우 HTTP Status 200과 함께 아래와 같은 응답을 내려줍니다. ```json { "id": "{TRANSCRIBE_ID}" } ``` ### 오류 코드[​](#error-codes "오류 코드에 대한 직접 링크") | HTTP Status | Code | Notes | | ----------- | ----- | ----------------------- | | 400 | H0001 | 잘못된 파라미터 요청 | | 400 | H0010 | 지원하지 않는 파일 포맷 | | 401 | H0002 | 유효하지 않은 토큰 | | 413 | H0005 | 파일 사이즈 초과 | | 413 | H0006 | 파일 길이 초과 | | 429 | A0001 | 사용량 초과 | | 429 | A0002 | 동시 처리 제한 초과 | | 500 | E500 | 서버 오류 | 아래는 실패 응답 예시입니다. ```json { "code": "H0001", "msg": "unexpected end of JSON input" } ``` ## 2) \[GET] /v1/transcribe/{TRANSCRIBE\_ID}[​](#2-get-v1transcribetranscribe_id "2) \[GET] /v1/transcribe/{TRANSCRIBE_ID}에 대한 직접 링크") * 음성 파일의 전사 결과를 조회하는 API입니다. [전사 요청 API](#response-body)에서 응답받은 `TRANSCRIBE_ID`를 사용하여 전사 결과를 조회할 수 있습니다. ### HTTP 요청[​](#http-request-1 "HTTP 요청에 대한 직접 링크") ```less GET https://openapi.vito.ai/v1/transcribe/{TRANSCRIBE_ID} ``` ### 요청 헤더[​](#request-headers-1 "요청 헤더에 대한 직접 링크") ```less Authorization: Bearer {YOUR_JWT_TOKEN} ``` * scheme: bearer * bearerFormat: JWT ### 샘플 코드[​](#sample-code "샘플 코드에 대한 직접 링크") * cURL * Python * Java get\_transcript.sh ```bash curl -X "GET" \ "https://openapi.vito.ai/v1/transcribe/${TRANSCRIBE_ID}" \ -H "accept: application/json" \ -H "Authorization: Bearer ${YOUR_JWT_TOKEN}" ``` get\_transcript.py ```python 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()) ``` get\_transcript.java ```java 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)[​](#response-body-1 "응답 바디 (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** ```json { "id": "{TRANSCRIBE_ID}", "status": "transcribing" } ``` **status: completed** ```json { "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** 요청은 성공했지만 전사가 실패한 경우 아래와 같은 응답이 반환됩니다. ```json { "id": "{TRANSCRIBE_ID}", "status": "failed", "error": { "code": "{ERROR_CODE}", "message": "{MESSAGE}" } } ``` 아래는 예시입니다. ```json { "id": "ZbOOQftrS1ywK_T3ikuveA", "status": "failed", "error": { "code": "E500", "message": "internal server error" } } ``` 위와 같은 오류가 지속되면 문의해 주시기 바랍니다. ### 오류 코드[​](#error-codes-1 "오류 코드에 대한 직접 링크") | HttpStatus | Code | Notes | | ---------- | ----- | -------------------- | | 400 | H0001 | 잘못된 파라미터 요청 | | 401 | H0002 | 유효하지 않은 토큰 | | 403 | H0003 | 권한 없음 | | 404 | H0004 | 전사 결과 없음 | | 410 | H0007 | 전사 결과 만료됨 | | 429 | A0003 | 요청 제한 초과 | | 500 | E500 | 서버 오류 | 아래는 응답이 실패한 경우 가운데 하나의 예시입니다. ```json { "code": "H0004", "msg": "not found" } ``` ### 샘플 코드 (단일 예제 + 프리셋)[​](#unified-example "샘플 코드 (단일 예제 + 프리셋)에 대한 직접 링크") 아래 단일 스크립트에서 `PRESET` 환경 변수로 원하는 설정을 선택할 수 있습니다. 기본값은 `sommers_basic`입니다. * Python (recommended) transcribe.py ```python 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}, }, "sommers_ja": { # 3) sommers japanese without diarization "model_name": "sommers", "use_diarization": False, "domain": "GENERAL", "language": "ja", }, "whisper_en_diarization": { # 4) 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` | `sommers` | — | `GENERAL` | off | — | | `sommers_call_diarization` | `sommers` | — | `CALL` | on (`diarization.spk_count=2`) | — | | `whisper_en_diarization` | `whisper` | `en` | — | on | — | | `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` | `whisper` | `multi` | — | — | `language_candidates=["ko", "en", "ja"]` | 전체 설정 JSON 보기 (선택) ```json { "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"] } } ```