Speech to Text API 호출 가이드

페이지 이동경로

Custom STT API 사용 가이드

카카오 i 클라우드의 Custom STT 서비스는 웹소켓 API(Websocket API)를 제공합니다. 사용자는 웹소켓 기반으로 실시간 음성 인식 및 파일 업로드 음성 인식 서비스를 구현할 수 있습니다. 이 문서는 클라이언트에서 API 서버로 음성 스트림을 전송하는 방법, API 서버와 교환하는 메시지 프로토콜, 웹소켓 연결 방법 등을 안내합니다.

웹소켓 연결하기

클라이언트에서 Custom STT API를 사용하려면 웹소켓 연결을 요청해야 합니다. 이를 웹소켓 핸드셰이크(Websocket Handshake)라고 하며, 자세한 설명은 부록. WebSocket Handshake를 참고하시기 바랍니다.

안내
클라이언트가 구현해야 하는 언어별 웹소켓 연결 예제는 언어별 예제 코드를 참고하시기 바랍니다.

웹소켓 연결을 요청하기 위해서는 URI에 API Endpoint URL을 입력해야 합니다. API Endpoint URL은 Custom STT 도메인 목록에서 [인증 정보] 버튼을 클릭 후, API 인증정보에서 확인할 수 있습니다.
콘솔에서 제공하는 API Endpoint URL에는 기본적으로 장문(이어 말하기) 모드의 /ws/long? 파라미터가 삽입되어 있으며, 단문 음성 인식을 위해서는 파라미터를 /ws?로 변경해야 합니다.

음성 인식 모드 파라미터 URL 예시
장문(이어 말하기) /ws/long? wss://sample-host.api.kr-central-1.kakaoi.io/ai/speech-to-text/ws/long?signature={signature}&x-api-key={API Key}
단문 /ws? wss://sample-host.api.kr-central-1.kakaoi.io/ai/speech-to-text/ws?signature={signature}&x-api-key={API Key}

음성 인식하기

실제로 음성을 인식하려면 클라이언트는 음성 인식 요청을 Custom STT 서버로 전송해야 합니다. 음성 인식 요청에는 메시지와 음성 스트림이 포함됩니다. 서버는 요청을 받아서 해당 음성을 인식하고, 그 결과를 메시지로 전송합니다. Custom STT 서버-클라이언트 간 음성 인식 플로우는 다음과 같습니다.

Custom STT 서버-클라이언트 간 음성 인식 플로우

이미지 Custom STT 서버-클라이언트 간 웹소켓 시퀀스 다이어그램 이미지

  1. 음성 인식 요청: 웹소켓이 연결된 상태에서 클라이언트는 음성 인식 요청을 위해 recogStart 메시지를 보냅니다.
  2. 음성 인식 준비 응답: 서버는 음성 인식 세션을 시작하고, 음성 인식 준비 상태가 되면 ready 메시지로 응답합니다.
  3. 음성 스트림 전송: 클라이언트는 ready 메시지를 수신한 후 음성 스트림을 서버에 전송하기 시작합니다.
  4. 발화 시작 감지: 서버가 클라이언트가 전송한 음성 스트림에서 말소리의 시작을 감지하면 beginPointDetection 메시지를 보내고 음성 인식을 시작합니다.
  5. 중간 음성 인식 결과 전송: 서버는 중간 음성 인식 결과를 partialResult 메시지로 지속적으로 전송합니다.
  6. 발화 중단 감지: 서버가 수신 중인 음성 스트림에서 말소리의 중단을 감지하면 endPointDetection 메시지를 전송합니다.
  7. 최종 음성 인식 결과 전송: 서버는 말소리의 시작에서 종료 구간까지의 최종 인식 결과를 finalResult 메시지로 전송합니다.
    • 단문 인식의 경우 발화 구간 하나를 인식하고 나면 음성 인식 서버는 세션을 종료합니다.
    • 장문(이어 말하기) 인식의 경우, 발화 구간 하나를 인식한 후에도 음성 인식 서버는 세션을 유지합니다. 클라이언트는 음성 스트림을 계속해서 전송할 수 있습니다.
  8. 음성 인식 종료 요청: 장문(이어 말하기) 인식의 경우, 음성 인식 세션을 종료하기 위해서는 클라이언트가 recogEnd 메시지를 전송하여야 합니다.
  9. 음성 인식 종료 응답: 장문(이어 말하기) 인식에 대한 응답으로 서버는 endLongRecognition 메시지를 응답합니다.

음성 스트림 전송

오디오 포맷 확인

General STT API에서 지원하는 오디오 포맷 스펙은 아래와 같습니다.

  • 비트 뎁스: 16bit
  • 채널: 1ch (mono)
  • 코덱: RAWPCM, MP3
  • 샘플레이트: 8kHz, 16kHz

안내
음성 인식을 원하는 음성 파일의 샘플레이트를 사전에 확인하시기 바랍니다.
- 보유한 음원의 샘플레이트가 16kHz 이상이라면 16kHz로 리샘플링하여, 16kHz 샘플레이트로 요청하시는 것이 좋습니다.
- 콜센터 전산망을 통해 획득된 음원은 대부분 16kHz에서 8kHz로 변환하여 저장됩니다. 따라서 8kHz 샘플레이트로 요청하시는 것이 적합합니다.

음성 스트림 전송 가이드

General STT API 서버에 음성 바이너리를 전송하기 위해서는, 바이너리를 청크(Chunk)로 분할하여 전송하여야 합니다. 음성을 바이너리를 청크로 분할하여 전송하는 것은 API 서버의 Read Timeout 발생을 방지하는 목적입니다. API 서버의 Read Timeout 발생 조건은 10초입니다. 따라서 안정적인 API 이용을 위해서는 아래의 가이드에 따라 청크 사이즈 및 전송 간격을 설정하시기 바랍니다.

  • 청크 사이즈: 16kHz의 경우 640 byte, 8kHz의 경우 320 byte
  • 청크의 전송 간격: 20 ms

안내
웹소켓 라이브러리를 사용하는 언어별 예제 코드는 본 문서의 언어별 예제 코드를 참고하시기 바랍니다. 언어별 예제 코드는 JavaScript, Python, Java, Go를 포함합니다.

메시지 프로토콜

서버와 클라이언트가 주고받는 모든 메시지는 json 형식으로 구성됩니다. 모든 메시지는 메시지 유형을 규정하는 “type” : "{message-type}" 필드로 시작하며, 메시지 유형에 따라서 추가가 필요하거나 추가될 수 있는 필드가 존재합니다.
클라이언트 요청 메시지 유형서버 응답 메시지 유형은 아래를 참고하시기 바랍니다.

클라이언트 요청 메시지 유형

클라이언트가 서버로 요청하는 메시지 유형은 다음과 같습니다.

  • recogStart : 연결된 웹소켓 세션에서 음성 인식 요청을 시작하고자 할 때 사용합니다.
  • recogStop : 진행 중인 음성 인식을 중단하고 연결된 세션을 종료하고자 할 때 선택적으로 사용합니다.
  • recogEnd : 음성 인식 대기 상태에서 연결된 세션을 종료하고자 할 때 선택적으로 사용합니다. 장문(이어 말하기) 모드에서만 사용합니다.

recogStart

recogStart 메시지는 연결된 웹소켓 세션에서 음성 인식 요청을 시작하고자 할 때 사용합니다.

{
    "type"                 : "recogStart"
    "service"              : "DICTATION"
    "showFinalOnly"        : {Boolean}
    "showExtraInfo"        : {Boolean}
    "audioFormat"          : "{codec}/{bit depth}/{sample rate}/{channel 개수}/_/_"
    "recogLongMaxWaitTime" : {int}
    "requestId"            : "{your-unique-id}"
}
파라미터 타입 필수여부 설명
type String 필수 메시지의 유형
- recogStart 로 기입
service String 필수 서비스 종류
- 고정값: DICTATION
showFinalOnly Boolean 선택 서버 응답 메시지로 finalResult만을 받을지 선택
- false (기본값): finalResult 외에도 beginPointDetection, partialResult, endPointDetection 을 모두 받음
- true: finalResult 만을 받음
showExtraInfo Boolean 선택 서버 응답 메시지 중 finalResult에 부가 정보를 받을지 선택
- false (기본값): 부가 정보를 받지 않음
- true: 부가 정보를 받음
audioFormat String 선택 전송할 음성 스트림의 코덱, 비트 뎁스, 샘플 레이트, 채널 개수 정보
- 형식: {codec}/{bit depth}/{sample rate}/{channel 개수}/_/_
- 기본값: RAWPCM/16/16000/1/_/_
{codec} String 선택 음성 스트림의 코덱
- 기본값: RAWPCM
- 변경 가능한 옵션: MP3
{bit depth} String 선택 음성 스트림의 비트 뎁스
- 고정값: 16
{sample rate} String 선택 음성 스트림의 샘플 레이트
- 기본값: 16000
- 변경 가능한 옵션: 8000
{channel 개수} String 선택 음성 스트림의 채널 수
- 고정값: 1
recogLongMaxWaitTime Integer 선택 장문(이어 말하기) 묵음 구간 허용 시간 (ms)
- finalResult 메시지 전송 이후, 입력된 {int} ms 이내로 다음 beginPointDetection 메시지를 보낼 수 없는 경우 서버에서 18번 에러 발생
requestId String 선택 클라이언트의 로그 추적을 위해서 사용하고자 하는 고유 ID
- 사용자가 임의로 지정하여 사용

주의
audioFormat 에서 전송하는 값이 선택한 도메인의 베이스 모델 및 실제로 전송하는 음성 스트림의 포맷과 일치하지 않으면 인식 결과가 정상적으로 출력되지 않습니다.

코드 예제 recogStart

아래 예제는 RAWPCM 8kHz 음성 스트림에 대한 음성 인식을 요청합니다. 응답으로 기본 정보가 담긴 finalResult 만을 받습니다. 묵음 구간 허용 시간은 설정하지 않았으며, 요청 ID로 demo_001을 설정한 예제입니다.

{
    "type"          : "recogStart"
    "service"       : "DICTATION"
    "showFinalOnly" : true
    "showExtraInfo" : false
    "audioFormat"   : "RAWPCM/16/8000/1/_/_"
    "requestId"     : "demo_001"
}

recogStop

recogStop 메시지는 진행 중인 음성 인식을 중단하고 연결된 세션을 종료하고자 할 때 선택적으로 사용합니다.

{
    "type" : "recogStop"
}

recogEnd

recogEnd 메시지는 음성 인식 대기 상태에서 연결된 세션을 종료하고자 할 때 선택적으로 사용합니다. 장문(이어 말하기) 모드에서만 사용합니다.

{
    "type" : "recogEnd"
}

서버 응답 메시지 유형

서버에서 클라이언트로 전송하는 서버 응답 메시지 유형은 다음과 같습니다.

  • ready : recogStart 메시지를 받아 음성 인식이 준비되었을 때 전송됩니다. 서버와의 음성 인식 세션이 연결되어 음성 스트림을 받아서 처리할 수 있는 상태를 의미합니다.
  • beginPointDetection : 발화의 시작이 감지되었을 때 전송됩니다. 서버에 전송된 음성 스트림에서 말소리의 시작이 감지되어, 화자가 말하기 시작한 것으로 판단되었음을 의미합니다.
  • partialResult : 중간 음성 인식 결과를 전송합니다. 서버에서 음성 인식 처리가 진행 중이며, 말소리가 아직 끝나지 않은 것으로 판단되었음을 의미합니다. 이 메시지는 여러 번 발생할 수 있습니다.
  • endPointDetection : 발화의 종료가 감지되었을 때 전송됩니다. 서버에 전송된 음성 스트림에서 묵음 구간이 감지되어, 화자가 말을 끝낸 것으로 판단되었음을 의미합니다.
  • finalResult : 발화 시작 구간에서 발화 종료 구간까지의 전체 음성 인식 결과를 최종 음성 인식 결과로 전송합니다.
  • endLongRecognition : recogEnd 메시지를 받아, 음성 인식을 종료할 때 전송합니다. 장문(이어 말하기) 모드에서만 사용합니다.
  • errorCalled : 음성 인식에 실패하였을 때 전송됩니다.

ready

ready 메시지는 클라이언트의 recogStart 메시지를 받아, 서버에서 음성 인식이 준비되었을 때 전송됩니다. 서버와의 음성 인식 세션이 연결되어 음성 스트림을 받아서 처리할 수 있는 상태를 의미합니다. 음성 인식 세션 ID를 sessionId로 전송합니다.

{
    "type"      : "ready"
    "sessionId" : {string}
}

beginPointDetection

beginPointDetection 메시지는 발화의 시작이 감지되었을 때 전송됩니다. 서버에 전송된 음성 스트림에서 말소리의 시작이 감지되어, 화자가 말하기 시작한 것으로 판단되었음을 의미합니다.

{
    "type"  : "beginPointDetection"
    "value" : "BPD"
}

partialResult

partialResult 메시지는 중간 음성 인식 결과를 전송합니다. 서버에서 음성 인식 처리가 진행 중이며, 말소리가 아직 끝나지 않은 것으로 판단되었음을 의미합니다. partialResult는 여러 번 발생할 수 있습니다.

{
    "type"  : "partialResult"
    "value" : {중간 인식 결과}
}

endPointDetection

endPointDetection 메시지는 발화의 종료가 감지되었을 때 전송됩니다. 서버에 전송된 음성 스트림에서 묵음 구간이 감지되어, 화자가 말을 끝낸 것으로 판단되었음을 의미합니다.

{
    "type"  : "endPointDetection"
    "value" : "EPD"
}

finalResult

finalResult 메시지는 endPointDetection 메시지 전송 후, 발화 시작 구간에서 발화 종료 구간까지의 전체 음성 인식 결과를 최종 음성 인식 결과로 전송합니다. finalResult 메시지에는 인식 결과 후보와 신뢰도 값, finalResult가 발생하기까지 들어온 음성의 전체 길이(ms), 피치로 판단한 의문문 점수, 시작 또는 이전 finalResult부터 다음 finalResult까지의 오디오 길이(s)가 포함됩니다. recogStart 에서 “showExtraInfo” : true 로 요청한 경우, resultInfo 에 단어별 타임스탬프를 포함합니다.

{
    "type"             : "finalResult"
    "value"            : {인식 결과}
    "nBest"            : [{object}]
    "voiceProfile"     : {object}       | 추후 지원 예정
    "durationMS"       : {int}
    "qmarkScore"       : {int}
    "gender"           : {int}          | 추후 지원 예정
    "x-metering-count" : {int}
    "resultInfo"       : [{object}]
}
항목 설명
nBest 인식 결과 후보와 신뢰도 값 (0~100)
voiceProfile 음성 프로필 기능 사용 시 인증 여부
- 추후 지원 예정
durationMS 이 finalResult가 발생하기까지 들어온 음성의 총 길이 (ms)
qmarkScore 음성 피치로 판단한 의문문 점수 (0~100)
gender 음성의 성별
- 추후 지원 예정
x-metering-count 시작 또는 이전 finalResult 부터 다음 finalResult 까지의 오디오 길이 (s)
- 음성인식 미터링 단위
resultInfo 단어별 타임스탬프
- recogStart 에서 “showExtraInfo” : true로 요청한 경우에만 포함됨

endLongRecognition

endLongRecognition 메시지는 클라이언트의 recogEnd 메시지를 받아, 서버가 음성 인식을 종료할 때 전송합니다. recogEnd와 마찬가지로 장문(이어 말하기) 모드에서만 사용됩니다.

{
    "type"  : "endLongRecognition"
    "value" : "ELR"
}

errorCalled

errorCalled 메시지는 음성 인식에 실패했을 때 전송합니다. 에러 메시지에는 에러 코드와 메시지가 포함됩니다.

{
    "type"  : "errorCalled"
    "value" : "Error {에러 코드} {에러 메시지}"
}

에러 코드 및 메시지

에러 코드 에러 타입 에러 메시지 설명
0 ERROR_NONE - 오류 아님
2 ERROR_NETWORK Client - can’t send packet 네트워크 관련 오류
3 ERROR_NETWORK_TIMEOUT Client - receive packet timeout 네트워크 타임아웃
4 ERROR_NO_RESULT Received Nack - no result 결과 없음
6 ERROR_SERVER_INTERNAL Received Nack - Server internal 서버 내부 오류
7 ERROR_SERVER_TIMEOUT - Received Nack - Server session timeout
- Received Nack - Server socket read timeout
서버 타임아웃 (10초)
8 ERROR_SERVER_AUTHENTICATION Received Nack - Server authentication fail 인증 실패
11 ERROR_SERVER_UNSUPPORT_SERVICE Received Nack - Server unsupport service 음성서버에서 지원하지 않는 서비스 코드
13 ERROR_SERVER_ALLOWED_REQUESTS_EXCESS Received Nack - Allowed Request Excess 음성 서비스 최대 허용 횟수 초과
17 ERROR_SERVER_OBSOLETE_SERVICE Received Nack - Service do not support 더 이상 서비스하지 않음
18 ERROR_LEPD_RESULT_TIMEOUT Received Nack - recogLongMaxWaitTime is over 장문(이어 말하기) 인식에서 일정 시간 정상 응답이 없을 때 타임아웃
50 ERROR_NEWTONE recogStart: read timeout recogStart 메시지 대기 타임아웃 (5초)
    recogStart: invalid type 잘못된 인식 시작 요청 메시지
- 웹소켓 세션 연결 후 recogStart 외의 메시지 전송
    invalid text message type 잘못된 웹소켓 텍스트 메시지 타입
- 음성 인식 세션 중간에 recogStop, recogEnd 외의 메시지 전송

서버 응답 메시지 예시

아래의 세 가지 응답 메시지는 동일한 음성 스트림을 전송하면서 recogStart 메시지 내 파라미터를 서로 다르게 설정한 경우를 보여줍니다.

예시 서버 응답 메시지 1 - 기본값

다음은 장문(이어 말하기) 모드에서 기본값을 사용한 예제입니다. showFinalOnly : false, showExtraInfo : false 와 동일합니다.

{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"첫"}
{"type":"partialResult","value":"첫 번째"}
{"type":"partialResult","value":"첫 번째 문장"}
{"type":"partialResult","value":"첫 번째 문장입니다"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"첫 번째 문장입니다","durationMS":2380,"x-metering-count":2,"nBest":[{"value":"첫 번째 문장입니다","resultInfo":null,"score":12}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"두"}
{"type":"partialResult","value":"두 번째"}
{"type":"partialResult","value":"두 번째 문장"}
{"type":"partialResult","value":"두 번째 문장입니다"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"두 번째 문장입니다","durationMS":4980,"x-metering-count":2,"nBest":[{"value":"두 번째 문장입니다","resultInfo":null,"score":5}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"한"}
{"type":"partialResult","value":"안녕"}
{"type":"partialResult","value":"안녕하세요"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"안녕하세요","durationMS":7380,"x-metering-count":3,"nBest":[{"value":"안녕하세요","resultInfo":null,"score":17}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"어"}
{"type":"partialResult","value":"어반"}
{"type":"partialResult","value":"어"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"어 반갑다","durationMS":9580,"x-metering-count":2,"nBest":[{"value":"어 반갑다","resultInfo":null,"score":71},{"value":"허 반갑다","resultInfo":null,"score":0},{"value":"a반 갑다","resultInfo":null,"score":0}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"잘"}
{"type":"partialResult","value":"잘 지내"}
{"type":"partialResult","value":"잘 지냈어요"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"잘 지냈어요","durationMS":12080,"x-metering-count":3,"nBest":[{"value":"잘 지냈어요","resultInfo":null,"score":73},{"value":"잘 지냈어여","resultInfo":null,"score":0},{"value":"잘 지냈었어요","resultInfo":null,"score":0},{"value":"잘 지 내 써요","resultInfo":null,"score":0},{"value":"잘 지냈어 효","resultInfo":null,"score":0},{"value":"아 잘 지냈어요","resultInfo":null,"score":0}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"난"}
{"type":"partialResult","value":"난 잘"}
{"type":"partialResult","value":"난 잘 지냈지"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"난 잘 지냈지","durationMS":14580,"x-metering-count":2,"nBest":[{"value":"난 잘 지냈지","resultInfo":null,"score":76},{"value":"아 난 잘 지냈지","resultInfo":null,"score":0}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"너"}
{"type":"partialResult","value":"너는"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"너는","durationMS":15980,"x-metering-count":1,"nBest":[{"value":"너는","resultInfo":null,"score":95}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"저"}
{"type":"partialResult","value":"저도"}
{"type":"partialResult","value":"저도 잘"}
{"type":"partialResult","value":"저도 잘 지냈어요"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"저도 잘 지냈어요","durationMS":18360,"x-metering-count":3,"nBest":[{"value":"저도 잘 지냈어요","resultInfo":null,"score":70},{"value":"아 저도 잘 지냈어요","resultInfo":null,"score":0},{"value":"예 저도 잘 지냈어요","resultInfo":null,"score":0},{"value":"와 저도 잘 지냈어요","resultInfo":null,"score":0}],"voiceProfile":{"authenticated":false}}
{"type":"endLongRecognition","value":"ELR"}

예시 서버 응답 메시지 2

다음은 장문(이어 말하기) 모드에서 showFinalOnlytrue, showExtraInfofalse로 설정한 예제입니다.

{"type":"finalResult","value":"첫 번째 문장입니다","durationMS":2280,"x-metering-count":2,"nBest":[{"value":"첫 번째 문장입니다","resultInfo":null,"score":12}],"voiceProfile":{"authenticated":false}}
{"type":"finalResult","value":"두 번째 문장입니다","durationMS":4980,"x-metering-count":2,"nBest":[{"value":"두 번째 문장입니다","resultInfo":null,"score":6}],"voiceProfile":{"authenticated":false}}
{"type":"finalResult","value":"안녕하세요","durationMS":7280,"x-metering-count":3,"nBest":[{"value":"안녕하세요","resultInfo":null,"score":17}],"voiceProfile":{"authenticated":false}}
{"type":"finalResult","value":"어 반갑다","durationMS":9480,"x-metering-count":2,"nBest":[{"value":"어 반갑다","resultInfo":null,"score":79}],"voiceProfile":{"authenticated":false}}
{"type":"finalResult","value":"잘 지냈어요","durationMS":11980,"x-metering-count":2,"nBest":[{"value":"잘 지냈어요","resultInfo":null,"score":67},{"value":"잘 지냈었어요","resultInfo":null,"score":0},{"value":"잘 지 내 써요","resultInfo":null,"score":0},{"value":"잘 지냈어 효","resultInfo":null,"score":0},{"value":"아 잘 지냈어요","resultInfo":null,"score":0}],"voiceProfile":{"authenticated":false}}
{"type":"finalResult","value":"난 잘 지냈지","durationMS":14580,"x-metering-count":3,"nBest":[{"value":"난 잘 지냈지","resultInfo":null,"score":76},{"value":"아 난 잘 지냈지","resultInfo":null,"score":0}],"voiceProfile":{"authenticated":false}}
{"type":"finalResult","value":"너는","durationMS":15980,"x-metering-count":1,"nBest":[{"value":"너는","resultInfo":null,"score":95}],"voiceProfile":{"authenticated":false}}
{"type":"finalResult","value":"저도 잘 지냈어요","durationMS":18360,"x-metering-count":3,"nBest":[{"value":"저도 잘 지냈어요","resultInfo":null,"score":70},{"value":"아 저도 잘 지냈어요","resultInfo":null,"score":0},{"value":"예 저도 잘 지냈어요","resultInfo":null,"score":0},{"value":"와 저도 잘 지냈어요","resultInfo":null,"score":0}],"voiceProfile":{"authenticated":false}}
{"type":"endLongRecognition","value":"ELR"}

예시 서버 응답 메시지 3

다음은 장문(이어 말하기) 모드에서 showExtraInfotrue로 설정한 예제입니다.

{"type":"ready","sessionId":"4aec8c70921c1a0fc3033a05b6e1861275d5b8e7"}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"첫"}
{"type":"partialResult","value":"첫 번째"}
{"type":"partialResult","value":"첫 번째 문장"}
{"type":"partialResult","value":"첫 번째 문장입니다"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"첫 번째 문장입니다","resultInfo":[{"start":0.15,"end":0.39,"space":true,"word":"첫"},{"start":0.42,"end":0.72,"space":true,"word":"번째"},{"start":0.75,"end":1.08,"space":false,"word":"문장"},{"start":1.11,"end":1.62,"space":true,"word":"입니다"}],"durationMS":2280,"x-metering-count":2,"nBest":[{"value":"첫 번째 문장입니다","resultInfo":[{"start":0.15,"end":0.39,"space":true,"word":"첫"},{"start":0.42,"end":0.72,"space":true,"word":"번째"},{"start":0.75,"end":1.08,"space":false,"word":"문장"},{"start":1.11,"end":1.62,"space":true,"word":"입니다"}],"score":12}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"두"}
{"type":"partialResult","value":"두 번째"}
{"type":"partialResult","value":"두 번째 문장"}
{"type":"partialResult","value":"두 번째 문장입니다"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"두 번째 문장입니다","resultInfo":[{"start":3.01,"end":3.19,"space":true,"word":"두"},{"start":3.22,"end":3.49,"space":true,"word":"번째"},{"start":3.52,"end":3.85,"space":false,"word":"문장"},{"start":3.88,"end":4.39,"space":true,"word":"입니다"}],"durationMS":4980,"x-metering-count":2,"nBest":[{"value":"두 번째 문장입니다","resultInfo":[{"start":3.01,"end":3.19,"space":true,"word":"두"},{"start":3.22,"end":3.49,"space":true,"word":"번째"},{"start":3.52,"end":3.85,"space":false,"word":"문장"},{"start":3.88,"end":4.39,"space":true,"word":"입니다"}],"score":5}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"한"}
{"type":"partialResult","value":"안녕"}
{"type":"partialResult","value":"안녕하세요"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"안녕하세요","resultInfo":[{"start":5.68,"end":6.04,"space":false,"word":"안녕"},{"start":6.07,"end":6.61,"space":true,"word":"하세요"}],"durationMS":7380,"x-metering-count":3,"nBest":[{"value":"안녕하세요","resultInfo":[{"start":5.68,"end":6.04,"space":false,"word":"안녕"},{"start":6.07,"end":6.61,"space":true,"word":"하세요"}],"score":17}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"어"}
{"type":"partialResult","value":"어반"}
{"type":"partialResult","value":"어"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"어 반갑다","resultInfo":[{"start":8.00,"end":8.24,"space":true,"word":"어"},{"start":8.27,"end":8.93,"space":true,"word":"반갑다"}],"durationMS":9480,"x-metering-count":2,"nBest":[{"value":"어 반갑다","resultInfo":[{"start":8,"end":8.24,"space":true,"word":"어"},{"start":8.27,"end":8.93,"space":true,"word":"반갑다"}],"score":71},{"value":"허 반갑다","resultInfo":[{"start":8,"end":8.24,"space":true,"word":"허"},{"start":8.27,"end":8.93,"space":true,"word":"반갑다"}],"score":0},{"value":"a반 갑다","resultInfo":[{"start":8,"end":8.24,"space":false,"word":"a"},{"start":8.27,"end":8.42,"space":true,"word":"반"},{"start":8.45,"end":8.6,"space":false,"word":"갑"},{"start":8.63,"end":8.93,"space":true,"word":"다"}],"score":0}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"잘"}
{"type":"partialResult","value":"잘 지내"}
{"type":"partialResult","value":"잘 지냈어요"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"잘 지냈어요","resultInfo":[{"start":10.31,"end":10.61,"space":true,"word":"잘"},{"start":10.64,"end":11.39,"space":true,"word":"지냈어요"}],"durationMS":11980,"x-metering-count":2,"nBest":[{"value":"잘 지냈어요","resultInfo":[{"start":10.31,"end":10.61,"space":true,"word":"잘"},{"start":10.64,"end":11.39,"space":true,"word":"지냈어요"}],"score":73},{"value":"잘 지냈어여","resultInfo":[{"start":10.31,"end":10.61,"space":true,"word":"잘"},{"start":10.64,"end":11.39,"space":true,"word":"지냈어여"}],"score":0},{"value":"잘 지냈었어요","resultInfo":[{"start":10.31,"end":10.61,"space":true,"word":"잘"},{"start":10.64,"end":11.39,"space":true,"word":"지냈었어요"}],"score":0},{"value":"잘 지 내 써요","resultInfo":[{"start":10.31,"end":10.61,"space":true,"word":"잘"},{"start":10.64,"end":10.73,"space":true,"word":"지"},{"start":10.76,"end":10.88,"space":true,"word":"내"},{"start":10.91,"end":11.39,"space":true,"word":"써요"}],"score":0},{"value":"잘 지냈어 효","resultInfo":[{"start":10.31,"end":10.61,"space":true,"word":"잘"},{"start":10.64,"end":11.12,"space":true,"word":"지냈어"},{"start":11.15,"end":11.39,"space":true,"word":"효"}],"score":0},{"value":"아 잘 지냈어요","resultInfo":[{"start":8.87,"end":10.25,"space":true,"word":"아"},{"start":10.31,"end":10.61,"space":true,"word":"잘"},{"start":10.64,"end":11.39,"space":true,"word":"지냈어요"}],"score":0}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"난"}
{"type":"partialResult","value":"난 잘"}
{"type":"partialResult","value":"난 잘 지냈지"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"난 잘 지냈지","resultInfo":[{"start":12.79,"end":13.06,"space":true,"word":"난"},{"start":13.09,"end":13.24,"space":true,"word":"잘"},{"start":13.27,"end":13.90,"space":true,"word":"지냈지"}],"durationMS":14480,"x-metering-count":3,"nBest":[{"value":"난 잘 지냈지","resultInfo":[{"start":12.79,"end":13.06,"space":true,"word":"난"},{"start":13.09,"end":13.24,"space":true,"word":"잘"},{"start":13.27,"end":13.9,"space":true,"word":"지냈지"}],"score":76},{"value":"아 난 잘 지냈지","resultInfo":[{"start":11.35,"end":12.73,"space":true,"word":"아"},{"start":12.79,"end":13.06,"space":true,"word":"난"},{"start":13.09,"end":13.24,"space":true,"word":"잘"},{"start":13.27,"end":13.9,"space":true,"word":"지냈지"}],"score":0}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"너"}
{"type":"partialResult","value":"너는"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"너는","resultInfo":[{"start":14.44,"end":14.65,"space":false,"word":"너"},{"start":14.68,"end":15.01,"space":true,"word":"는"}],"durationMS":15980,"x-metering-count":1,"nBest":[{"value":"너는","resultInfo":[{"start":14.44,"end":14.65,"space":false,"word":"너"},{"start":14.68,"end":15.01,"space":true,"word":"는"}],"score":95}],"voiceProfile":{"authenticated":false}}
{"type":"beginPointDetection","value":"BPD"}
{"type":"partialResult","value":"저"}
{"type":"partialResult","value":"저도"}
{"type":"partialResult","value":"저도 잘"}
{"type":"partialResult","value":"저도 잘 지냈어요"}
{"type":"endPointDetection","value":"EPD"}
{"type":"finalResult","value":"저도 잘 지냈어요","resultInfo":[{"start":16.52,"end":16.94,"space":true,"word":"저도"},{"start":16.97,"end":17.24,"space":true,"word":"잘"},{"start":17.27,"end":18.11,"space":true,"word":"지냈어요"}],"durationMS":18360,"x-metering-count":3,"nBest":[{"value":"저도 잘 지냈어요","resultInfo":[{"start":16.52,"end":16.94,"space":true,"word":"저도"},{"start":16.97,"end":17.24,"space":true,"word":"잘"},{"start":17.27,"end":18.11,"space":true,"word":"지냈어요"}],"score":70},{"value":"아 저도 잘 지냈어요","resultInfo":[{"start":14.87,"end":16.43,"space":true,"word":"아"},{"start":16.52,"end":16.94,"space":true,"word":"저도"},{"start":16.97,"end":17.24,"space":true,"word":"잘"},{"start":17.27,"end":18.11,"space":true,"word":"지냈어요"}],"score":0},{"value":"예 저도 잘 지냈어요","resultInfo":[{"start":14.87,"end":16.43,"space":true,"word":"예"},{"start":16.52,"end":16.94,"space":true,"word":"저도"},{"start":16.97,"end":17.24,"space":true,"word":"잘"},{"start":17.27,"end":18.11,"space":true,"word":"지냈어요"}],"score":0},{"value":"와 저도 잘 지냈어요","resultInfo":[{"start":14.87,"end":16.43,"space":true,"word":"와"},{"start":16.52,"end":16.94,"space":true,"word":"저도"},{"start":16.97,"end":17.24,"space":true,"word":"잘"},{"start":17.27,"end":18.11,"space":true,"word":"지냈어요"}],"score":0}],"voiceProfile":{"authenticated":false}}
{"type":"endLongRecognition","value":"ELR"}

언어별 예제 코드

다음은 웹소켓 연결부터 음성 인식 시작, 음성 전송, 응답 메시지 등 Custom STT API의 모든 과정을 포함하는 언어별 예제 코드입니다.

주의
아래 예제 코드는 16kHz 음성 전송에 맞추어져 있습니다. 8kHz 음성 전송을 위해서는 청크(Chunk) 사이즈를 절반으로 수정하여 사용하시기 바랍니다.

HTML / JavaScript

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>File Upload Client</title>

    <script type="text/javascript">

        let ws = null;

        function connector(){
            // must modify endpoint
            var uri = '{API Endpoint URL}';
            alert(uri)

            ws = new WebSocket(uri);

            ws.binaryType="arraybuffer";

            // 1. send recogStart message (RAWPCM/16/16000/1/_/_)
            ws.onopen=function(){
                const now = Date.now()
                ws.send(`{"type":"recogStart","service":"DICTATION","showFinalOnly":false,"showExtraInfo":false,"requestId":"GNTWSC-${now}","audioFormat":"RAWPCM/16/16000/1/_/_"}`)
                alert("연결 완료");
            };

            // 2. read server message
            ws.onmessage = function(e){
                let out = document.getElementById('output');
                out.innerHTML += e.data + '<br>';
            };

            setTimeout(function() {
                ws.close();
            }, 3600000);

            ws.onclose = function() {
                alert("연결 종료");
            };
            ws.onerror = function(e) {
                alert("onerror: " + e.msg);
            }
        }

        function sleep(ms) {
            return new Promise((r) => setTimeout(r, ms))
        }

        function sendFile(){
            let file = document.getElementById('file').files[0];
            alert('start');

            let reader = new FileReader();
            let rawData;

            reader.loadend = function() {

            }

            // 3. async send audio binary message (file) and control sending speed
            reader.onload = async function(e) {
                rawData = e.target.result;

                const chunkLength = 640;
                const sleepDuration = 20;

                let begin = 0;
                let end = chunkLength;

                while(end < rawData.byteLength) {
                    await ws.send(rawData.slice(begin, end));
                    await sleep(sleepDuration);
                    begin = end;
                    end = begin + chunkLength;
                }
                await ws.send(rawData.slice(begin, rawData.byteLength));

                // 4. send recogEnd message
                await ws.send('{"type":"recogEnd"}');

                alert("파일 전송이 완료 되었습니다.");
            }

            reader.readAsArrayBuffer(file);
        }

        function addEvent(){
            document.getElementById("connect").addEventListener("click", connector, false);
            document.getElementById("send").addEventListener("click", sendFile, false);
        }

        window.addEventListener("load", addEvent, false);
    </script>

</head>
<body>

<input id="file" type="file" >
<input id="connect" type="button" value="connect">
<input id="send" type="button" value="send">

<p id="output"></p>

</body>
</html>

Go

package main

import (
    "bufio"
    "encoding/json"
    "errors"
    "io"
    "log"
    "net/url"
    "os"
    "time"

    "github.com/gorilla/websocket"
    "github.com/labstack/gommon/random"
)

type RecogStart struct {
    Type          string `json:"type"`
    Service       string `json:"service"`
    ShowFinalOnly bool   `json:"showFinalOnly"`
    ShowExtraInfo bool   `json:"showExtraInfo"`
    AudioFormat   string `json:"audioFormat"`
    RequestId     string `json:"requestId"`
}

type RecogEnd struct {
    Type string `json:"type"`
}

type ResponseMessage struct {
    Type  string `json:"type"`
    Value string `json:"value"`
}

func main() {
    log.SetFlags(log.Lmicroseconds)

    idxstr := random.String(15)
    requestId := "GNT-" + idxstr

    // must modify endpoint
    endPointURI := "{API Endpoint URL}"
    endPoint, err := url.Parse(endPointURI)
    if err != nil {
        log.Fatalf("[%s] url parse err: %s", requestId, err.Error())
    }

    // must modify file path
    filePath := "{File Path}"
    f, err := os.Open(filePath)
    if err != nil {
        log.Fatalf("[%s] file open err: %s", requestId, err.Error())
    }
    defer f.Close()

    recognize(endPoint, f, requestId)
}

func recognize(u *url.URL, r io.Reader, requestId string) {
    log.Printf("[%s] connecting to %s", requestId, u.String())

    c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    if err != nil {
        log.Fatalf("[%s] dial: %s", requestId, err.Error())
    }
    defer c.Close()

    // 1. send recogStart message
    startMsg := RecogStart{
        Type:          "recogStart",
        Service:       "DICTATION",
        ShowFinalOnly: false,
        ShowExtraInfo: false,
        AudioFormat:   "RAWPCM/16/16000/1/_/_",
        RequestId:     requestId,
    }
    if err := c.WriteJSON(startMsg); err != nil {
        log.Printf("[%s] start err: %s", requestId, err.Error())
        return
    }
    log.Printf("[%s] send: %+v", requestId, startMsg)

    // 2. async read server message
    ticker := time.NewTicker(65 * time.Minute)
    defer ticker.Stop()

    exit := make(chan bool, 1)

    go func() {
        defer func() {
            exit <- true
        }()
        for {
            select {
            case <-ticker.C:
                log.Printf("[%s] exit by ticker", requestId)
                return
            default:
                _, message, err := c.ReadMessage()
                if err != nil {
                    log.Printf("[%s] ReadMessage: %s", requestId, err.Error())
                    return
                }
                log.Printf("[%s] recv: %s", requestId, message)

                var respMsg ResponseMessage
                json.Unmarshal(message, &respMsg)
                if respMsg.Type == "ELR" {
                    return
                }
            }
        }
    }()

    // 3. send audio binary message (file)
    br := bufio.NewReader(r)

    data := make([]byte, 640)

    cumulativeReadSize := 0
    readStartTime := time.Now()
    for {
        rbytes, err := br.Read(data)
        if err != nil {
            if errors.Is(err, io.EOF) {
                log.Printf("[%s] EOF", requestId)
                break
            } else {
                log.Printf("[%s] read err: %s", requestId, err.Error())
                return
            }
        }
        cumulativeReadSize += rbytes
        log.Printf("[%s] cumulative read size: %d", requestId, cumulativeReadSize)

        if err := c.WriteMessage(websocket.BinaryMessage, data[:rbytes]); err != nil {
            log.Printf("[%s] write: %s", requestId, err.Error())
            return
        }

        // control sending speed (640Bytes / 20ms)
        for cumulativeReadSize > 32*int(time.Now().Sub(readStartTime).Milliseconds()) {
            time.Sleep(20 * time.Millisecond)
        }
    }

    // 4. send recogEnd message
    endMsg := RecogEnd{Type: "recogEnd"}
    if err := c.WriteJSON(endMsg); err != nil {
        log.Printf("[%s] end err: %s", requestId, err.Error())
        return
    }
    log.Printf("[%s] send: %+v", requestId, endMsg)

    <-exit
}

Java

package newtone;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.time.LocalDate;
import java.time.LocalTime;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.apache.commons.cli.*;

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.enums.Opcode;
import org.java_websocket.handshake.ServerHandshake;

public class ExampleClient extends WebSocketClient{
    static final int frameSize = 640;

    public ExampleClient(URI serverUri) {
        super(serverUri);
    }

    // 1. send recogStart message
    @Override
    public void onOpen(ServerHandshake handshakedata) {
        JSONObject jo = new JSONObject();

        jo.put("type", "recogStart");
        jo.put("service", "DICTATION");
        jo.put("showFinalOnly", false);
        jo.put("showExtraInfo", false);
        jo.put("requestId", "GNT-" + LocalDate.now().atTime(LocalTime.MIDNIGHT));
        jo.put("audioFormat", "RAWPCM/16/16000/1/_/_");
        // jo.put("audioFormat", "RAWPCM/16/8000/1/_/_");

        send(jo.toJSONString());
        System.out.println("opened connection : " + jo);
        // if you plan to refuse connection based on ip or httpfields overload: onWebsocketHandshakeReceivedAsClient
    }
  
    // 2. async read server message
    @Override
    public void onMessage(String message) {
        System.out.println("received: " + message);

        try {
            JSONObject jo = (JSONObject) new JSONParser().parse(message);

            if (jo.get("type").equals("ready")){
                //todo ready
            } else if (jo.get("type").equals("beginPointDetection")){
                //todo beginPointDetection
            } else if (jo.get("type").equals("endPointDetection")){
                //todo endPointDetection
            } else if (jo.get("type").equals("partialResult")){
                //todo partialResult
            } else if (jo.get("type").equals("finalResult")){
                //todo finalResult
            } else if (jo.get("type").equals("endLongRecognition")){
                //todo endLongRecognition
            }
        } catch (org.json.simple.parser.ParseException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
  
    @Override
    public void onClose(int code, String reason, boolean remote) {
        // The close codes are documented in class org.java_websocket.framing.CloseFrame
        System.out.println(
        "Connection closed by " + (remote ? "remote peer" : "us") + " Code: " + code + " Reason: "
            + reason);
    }
  
    @Override
    public void onError(Exception ex) {
        ex.printStackTrace();
        // if the error is fatal then onClose will be called additionally
    }

    public static void main(String[] args) throws URISyntaxException, InterruptedException{
        Options options = new Options();
        
        Option file = new Option("f", "file", true, "audio file path.");
        file.setRequired(true);
        options.addOption(file);
        
        Option endPoint = new Option("u", "uri", true, "endpoint URI.");
        endPoint.setRequired(true);
        options.addOption(endPoint);

        CommandLineParser parser = new BasicParser();
        HelpFormatter formatter = new HelpFormatter();
        CommandLine cmd = null;

        try {
            cmd = parser.parse(options,args);
        } catch (ParseException e) {
            System.out.println(e.getMessage());
            formatter.printHelp("utility-name", options);

            System.exit(1);
        }

        String fileName = cmd.getOptionValue("file");
        String endPointURI = cmd.getOptionValue("endPoint");

        ExampleClient client = new ExampleClient(new URI(
        endPointURI)); // more about drafts here: http://github.com/TooTallNate/Java-WebSocket/wiki/Drafts
        
        if (!client.connectBlocking()) {
            System.err.println("Could not connect to the server.");
            System.exit(1);
        }

        // 3. send audio binary message (file)
        try (InputStream inputStream = new FileInputStream(fileName)) {
            int byteRead = -1;
            byte[] buffer = new byte[frameSize];

            while( (byteRead = inputStream.read(buffer)) != -1) {
                ByteBuffer bb = ByteBuffer.wrap(buffer, 0, byteRead);
                client.sendFragmentedFrame(Opcode.BINARY, bb, true);
                Thread.sleep(20);
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        // 4. send recogEnd message
        JSONObject jo = new JSONObject();
        jo.put("type", "recogEnd");
        client.send(jo.toJSONString());

        // waiting for connection to close
        while(client.isOpen()){
            Thread.sleep(1000);
        }
    }
}

Python

# python3 wss_guide.py
# Standard Python libraries
import wave
import asyncio
import datetime
import asyncio
import json
from os.path import exists
# OpenSources, need install
# https://pypi.org/project/websockets/
import websockets
# https://pypi.org/project/aiofile/
from aiofile import AIOFile, Reader

class WebSocketClient():
    # Custom class for handling websocket client
    def __init__(self, url, onStartMessage, bits_per_seconds):
        self.url=url
        # chunk size is depend on sendfile duration, which is now 0.02s(20ms)
        # set chunk size as byte unit
        self.chunksize=bits_per_seconds*0.02/8
        self.onStartMessage = onStartMessage
        pass

    async def connect(self):
        self.connection = await websockets.connect(self.url)
        if self.connection.open:
            await self.connection.send(json.dumps(self.onStartMessage))
            return self.connection

    async def receiveMessage(self, connection):
        while True:
            try:
                message = await connection.recv()
                print(message)
            except websockets.exceptions.ConnectionClosed as e:
                print('Connection with server closed')
                break
            except Exception as e:
                print(e)

    async def sendfile(self, connection, filepath):
        try:
            async with AIOFile(filepath, 'rb') as afp:
                reader = Reader(afp, chunk_size=self.chunksize)
                async for chunk in reader:
                    await connection.send(chunk)
                    await asyncio.sleep(0.02)
        except Exception as e:
            print(e)

def argsChecks(args):
    # Check given arguments are valid
    # Plase check guide for more details
    if not exists(args["filepath"]):
        raise "Please give exist filepath in filepath args"
    
    filepath = args["filepath"]
    onStartMessage = {
        "type": "recogStart",
        "service": "DICTATION",
        "requestId": "GNTWSC-{}".format(datetime.datetime.now().strftime('%Y%m%d%H%M%S')),
        "showFinalOnly": args["showFinalOnly"],
        "showExtraInfo": args["showExtraInfo"],
    }
        if filepath.endswith(".wav"):
        with wave.open(filepath, 'rb') as wf:
            bit_depth = wf.getsampwidth() * 8
            samplerate = wf.getframerate()
            channels = wf.getnchannels()
            onStartMessage["audioFormat"] = "RAWPCM/{bitDepth}/{sampleRate}/{channel}/_/_".format(bitDepth=bit_depth, sampleRate=samplerate, channel=channels)
            bits_per_seconds = bit_depth * samplerate * channels
        else:
            # If file is PCM data
        onStartMessage["audioFormat"] = "RAWPCM/16/16000/1/_/_"
            bits_per_seconds = 256000
    return args["url"], filepath, onStartMessage, bits_per_seconds

if __name__ == '__main__':
    args = {
        "url": "{API ENPOINT URL}",
        "filepath": "{FILE PATH}",
        "showFinalOnly": False,
        "showExtraInfo": False,
    }
    
    url, filepath, onStartMessage, bits_per_seconds = argsChecks(args)

    # Creating client object
    client = WebSocketClient(url, onStartMessage, bits_per_seconds)
    loop = asyncio.get_event_loop()
    # Start connecting
    connection = loop.run_until_complete(client.connect())
    # Define async jobs
    tasks = [
        asyncio.ensure_future(client.sendfile(connection, filepath)),
        asyncio.ensure_future(client.receiveMessage(connection)),
    ]
    # Run async jobs
    loop.run_until_complete(asyncio.wait(tasks))

부록. WebSocket Handshake

클라이언트에서 Custom STT의 웹소켓 API를 사용하기 위해서는 API Endpoint URL을 사용해야 하며, 개발에 사용하는 웹소켓 프레임워크 혹은 라이브러리에서 규정하는 방식으로 새로운 웹소켓 연결을 요청해야 합니다. 이를 웹소켓 핸드셰이크(WebSocket Handshake)라고 합니다.

예시코드 JavaScript_장문(이어 말하기) 모드

let socket = new WebSocket("{API Endpoint URL}");

AI Endpoint URL 구성

웹소켓 연결 요청시 사용하는 AI Endpoint URL에는 웹소켓 연결에 필요한 API 호스트, 사용자 도메인 및 API Key 정보, 음성 인식 모드에 대한 파라미터가 포함됩니다.

예시 API Endpoint URL

wss://{호스트}/ai/speech-to-text/{음성 인식 모드}signature={signature}&x-api-key={API Key}
파라미터 설명
{호스트} API 호스트
- sample-host.api.kr-central-1.kakaoi.io 와 같은 형식으로 구성
{음성 인식 모드} 음성 인식 모드 설정
- /ws?: 단문 모드
- /ws/long? : 장문(이어 말하기) 모드
{API Key} API 호출 시 필요한 인증키

Request

웹소켓 핸드셰이크를 요청했을 때 실제로 전송되는 HTTP 요청 헤더는 아래와 같습니다.

코드 예제 Websocket Handshake Request: 장문(이어 말하기) 모드

GET /ai/speech-to-text/ws/long?signature={signature}&x-api-key={API Key}
Host: {호스트}
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: {random key}
Sec-WebSocket-Version: 13

안내
일반적으로, 웹소켓 핸드셰이크를 위한 HTTP 요청의 헤더는 웹소켓 프레임워크 혹은 라이브러리에서 자동으로 생성됩니다.

Response

Custom STT API 서버는 웹소켓 핸드셰이크 HTTP 요청에 대한 응답을 반환합니다. 상태 코드 101은 웹소켓 핸드셰이크 요청을 승인함을 의미합니다. 웹소켓 연결에 성공하면 Custom STT 서버의 프로토콜을 따라 음성 스트림과 메시지를 교환할 수 있습니다.

코드 예제 Websocket Handshake Response: 장문(이어 말하기) 모드

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade