본문으로 건너뛰기

스트리밍 STT - WebSocket

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

스트리밍 STT 공통 가이드로 돌아가기

연동예제​

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

인증 토큰 발급​

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

Parameter​

WebSocket 연동 시 사용되는 Parameter에 대한 상세 정보는 공통 DecoderConfig/Parameter 정보에서 확인할 수 있습니다.

FINALIZE request​

{ "type": "Finalize" }
websocket.send(JSON.stringify({ type: "Finalize" }));

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"

var ClientId = os.Getenv("RTZR_CLIENT_ID")
var ClientSecret = os.Getenv("RTZR_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로 변환 작업을 수행하는 것이 더 편리합니다.