Skip to main content

스트리밍 STT - WebSocket

본 문서는 스트리밍 STT 중에서 WebSocket로 구현하는 방식에 대한 가이드를 제공합니다.

연동예제

본 문서의 예제는 로컬 오디오 파일로부터 스트리밍 음성인식을 수행하는 방법을 설명해줍니다. 마이크와 같은 스트리밍 입력 장치로 API를 이용하고 싶은 경우, 파일로 읽어오는 코드 부분을 장치 인식을 수행하는 코드로 변경함으로써 사용하실 수 있습니다.

인증 토큰 발급

스트리밍 STT API를 사용하기 위해서는 인증 토큰 발급 가이드를 통해 토큰을 발급받아야 합니다.

Parameter

NameTypeDescriptionRequired
sample_rateinteger범위: 8000 ~ 48000, 단위: HzO
encodingstring인코딩 타입 (참고. 지원 인코딩)O
model_namestring사용할 언어 모델 (default: sommers_ko, 참고: 한국어 - sommers_ko, 일본어 - sommers_ja)X
use_itnboolean영어/숫자/단위 변환 사용 여부 (default: true, 참고: 영어/숫자/단위 변환)X
use_disfluency_filterboolean간투어 필터기능 사용 여부 (default: false, 참고: 간투어 필터)X
use_profanity_filterboolean비속어 필터기능 사용 여부 (default: false, 참고: 비속어 필터)X
keywordsstring아래 키워드 부스팅 참고X

키워드 부스팅 (keywords)

키워드 부스팅에 사용할 단어와 그에 해당하는 스코어를 지정합니다.
개별 키워드는 다음과 같은 형식의 문자열(string)으로 제공되어야 합니다: "단어" 또는 "단어:스코어"
2개 이상의 키워드를 지정할 경우, 개별 키워드는 쉼표(,)로 구분되어야 합니다: "단어1,단어2:스코어2,단어3:스코어3"

  • 단어: 인식률을 높이고 싶은 단어를 지정해야 하고, 반드시 한글발음과 띄어쓰기로만 이루어져야 합니다.
  • 스코어:
    • 스코어를 지정하지 않는 경우 기본값인 2.0으로 설정됩니다.
    • 스코어를 지정할 경우 각 단어와 스코어는 반드시 콜론(:)으로 구분되어야 합니다.
    • 스코어는 -5.0부터 5.0까지의 범위 내에 있는 실수 값을 지정해야 합니다. 0보다 큰 경우 해당 단어를 잘 인식하고, 0보다 작은 경우 덜 인식하도록 작동합니다.
    • 0을 설정하는 경우 해당 단어에 대해 키워드 부스팅을 사용하지 않는 것과 동일하게 작동합니다.
  • keywords 예시:
    "부스팅"  // 스코어를 지정하지 않은 경우 2.0을 사용합니다.
    "부스팅:3.5"
    "음성인식,리턴제로:3.5,에스티티:-1"
caution
  • 스코어 값을 지정할 경우 반드시 -5.0 이상 5.0 이하여야 합니다.
  • 단어는 한글 발음대로 적어야 합니다. <예시: STT (X), 에스티티 (O)>
  • 단어는 한글과 공백으로만 구성되어야 하며 음절이어야 합니다. <예시: 에스TT (X), 에스ㅌㅌ (X), 에스티티2 (X), 에스티티 (O)>
  • 각 단어의 길이는 20자 이하로 제한되며, keywords는 최대 100개까지 지원합니다.
  • 유효하지 않은 형식의 단어나 스코어가 포함된 경우, 요청은 거부될 수 있습니다.

Response

{
// 문장의 발화 id
seq: integer
// 스트리밍 시작 기준 문장의 발화 시점 (단위: msec)
start_at: integer
// final이 true 일 경우 문자의 발화 시간, final 이 false일 경우 0 (단위: msec)
duration: integer
// 문장의 종료 여부
final: boolean

// 대체 텍스트, 첫번째 값이 정확도가 가장 높은 결과
alternatives: [
{
// 문장의 텍스트
text: string
// 문장의 정확도 (beta)
confidence: float
// 단어(토큰)의 정보, final 이 true 일 경우만 제공
words?: [
{
// 단어(토큰)의 텍스트, `|` 로 띄어쓰기 구분
text: string
// 문장의 시작 기준 단어(토큰)의 발화 시점 (단위: msec)
start_at: integer
// 단어(토큰)의 발화 시간 (단위: msec)
duration: integer
// 단어(토큰)의 정확도 (미지원)
confidence: float
}
]
}
]
}

샘플 코드

package main

import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/gorilla/websocket"
"github.com/xfrr/goffmpeg/transcoder"
)

const ServerHost = "openapi.vito.ai"
const ClientId = "{YOUR_CLIENT_ID}"
const ClientSecret = "{YOUR_CLIENT_SECRET}"

const SAMPLE_RATE int = 8000
const BYTES_PER_SAMPLE int = 2

/*
본 예제에서는 스트리밍 입력을 음성파일을 읽어서 시뮬레이션 합니다.
실제사용시에는 마이크 입력 등의 실시간 음성 스트림이 들어와야합니다.
*/
type FileStreamer struct {
file *os.File
}

func (fs *FileStreamer) Read(p []byte) (int, error) {
byteSize := len(p)
maxSize := 1024 * 1024
if byteSize > maxSize {
byteSize = maxSize
}

defer time.Sleep(time.Duration(byteSize/((SAMPLE_RATE*BYTES_PER_SAMPLE)/1000)) * time.Millisecond)
return fs.file.Read(p[:byteSize])
}

func (fs *FileStreamer) Close() error {
defer os.Remove(fs.file.Name())
return fs.file.Close()
}

func OpenAudioFile(audioFile string) (io.ReadCloser, error) {
fileName := filepath.Base(audioFile)
i := strings.LastIndex(fileName, ".")
audioFileName8K := filepath.Join(os.TempDir(), fileName[:i]) + fmt.Sprintf("_%d.%s", SAMPLE_RATE, "wav")
trans := new(transcoder.Transcoder)
if err := trans.Initialize(audioFile, audioFileName8K); err != nil {
log.Fatal(err)
}

trans.MediaFile().SetAudioRate(SAMPLE_RATE)
trans.MediaFile().SetAudioChannels(1)
trans.MediaFile().SetSkipVideo(true)
trans.MediaFile().SetAudioFilter("aresample=resampler=soxr")

err := <-trans.Run(false)
if err != nil {
return nil, fmt.Errorf("transcode audio file failed: %w", err)
}

file, err := os.Open(audioFileName8K)
if err != nil {
return nil, fmt.Errorf("open audio file failed: %w", err)
}

return &FileStreamer{file: file}, nil
}

func main() {
flag.Parse()
log.SetFlags(0)

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

u := url.URL{Scheme: "wss", Host: ServerHost, Path: "/v1/transcribe:streaming"}

query := u.Query()
query.Set("sample_rate", strconv.Itoa(SAMPLE_RATE))
query.Set("encoding", "LINEAR16")
query.Set("use_itn", "true")
query.Set("use_disfluency_filter", "true")
query.Set("use_profanity_filter", "false")
u.RawQuery = query.Encode()

log.Printf("connecting to %s", u.String())

audioFile := flag.Arg(0)
streamingFile, err := OpenAudioFile(audioFile)
if err != nil {
log.Fatal(err)
}
defer streamingFile.Close()

data := map[string][]string{
"client_id": []string{ClientId},
"client_secret": []string{ClientSecret},
}
resp, _ := http.PostForm("https://openapi.vito.ai/v1/authenticate", data)
if resp.StatusCode != 200 {
panic("Failed to authenticate")
}

bytes, _ := io.ReadAll(resp.Body)
var result struct {
Token string `json:"access_token"`
}
json.Unmarshal(bytes, &result)
requestHeader := http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", result.Token)},
}
c, res, err := websocket.DefaultDialer.Dial(u.String(), requestHeader)
if err != nil {
log.Printf("status: %s, body: %v", res.Status, res.Body)
log.Fatal("dial:", err)
}
defer c.Close()

start := time.Now()
go func() {
buf := make([]byte, 1024)
for {
n, err := streamingFile.Read(buf)
if err == io.EOF {
// Nothing else to pipe, close the stream.
log.Println("send EOS")
if err := c.WriteMessage(websocket.TextMessage, []byte("EOS")); err != nil {
log.Fatalf("Could not close stream: %v", err)
}
return
}
if err != nil {
log.Printf("Could not read from %s: %v", audioFile, err)
continue
}
err = c.WriteMessage(websocket.BinaryMessage, buf[:n])
if err != nil {
log.Println("write:", err)
}

select {
case <-interrupt:
log.Println("interrupt")
if err := c.WriteMessage(websocket.TextMessage, []byte("EOS")); err != nil {
log.Fatalf("Could not close: %v", err)
}
return
default:

}
}
}()

start2 := time.Now()
for {
_, message, err := c.ReadMessage()
if err != nil {
log.Printf("elapsed[%v]\n", time.Since(start))
log.Println("read:", err)
return
}
log.Printf("[%v]recv: %s", time.Since(start2), message)
start2 = time.Now()
}

}

오류 코드

스트리밍 STT - WebSocket 의 오류 처리는 Dial시 응답값의 StatusCode로 오류를 처리 합니다.

HttpStatusCodeNotes
400H0001잘못된 파라미터 요청
401H0002인증실패
429A0001사용량 초과
500E500서버 오류

참고사항

오디오 파일을 텍스트로 변환할 경우, 스트리밍 STT API를 이용하여 처리할 수도 있지만 일반 STT 가이드 문서에서 기술된 것처럼 일반 STT API로 변환 작업을 수행하는 것이 더 편리합니다.