Skip to main content

스트리밍 STT - GRPC

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

연동예제

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

인증 토큰 발급

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

DecoderConfig

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

키워드 부스팅 (keywords)

키워드 부스팅에 사용할 단어와 그에 해당하는 스코어를 지정합니다.
keywords의 개별 element는 다음과 같은 형식의 문자열(string)으로 제공되어야 합니다: "단어" 또는 "단어:스코어"

  • 단어: 인식률을 높이고 싶은 단어를 지정해야 하고, 반드시 한글발음과 띄어쓰기로만 이루어져야 합니다.
  • 스코어:
    • 스코어를 지정하지 않는 경우 기본값인 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개까지 지원합니다.
  • 유효하지 않은 형식의 단어나 스코어가 포함된 경우, 요청은 거부될 수 있습니다.

StreamingRecognitionResult

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

샘플 코드

package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"

"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
pb "github.com/vito-ai/go-genproto/vito-openapi/stt"
"github.com/xfrr/goffmpeg/transcoder"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
)

const ServerHost = "grpc-openapi.vito.ai:443"
const ClientId = "{YOUR_CLIENT_ID}"
const ClientSecret = "{YOUR_CLIENT_SECRET}"

const SAMPLE_RATE int = 8000
const BYTES_PER_SAMPLE int = 2

var False = false
var True = true

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

func (fs *FileStreamer) Read(p []byte) (int, error) {
byteSize := len(p)
maxSize := 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.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s <AUDIOFILE>\n", filepath.Base(os.Args[0]))
fmt.Fprintf(os.Stderr, "<AUDIOFILE> must be a path to a local audio file. Audio file must be a 16-bit signed little-endian encoded with a sample rate of 16000.\n")

}
flag.Parse()
if len(flag.Args()) != 1 {
log.Fatal("Please pass path to your local audio file as a command line argument")
}
audioFile := flag.Arg(0)

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)

var dialOpts []grpc.DialOption
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")))
dialOpts = append(dialOpts, grpc.WithBlock())
dialOpts = append(dialOpts, grpc.WithTimeout(10*time.Second))
conn, err := grpc.Dial(ServerHost, dialOpts...)
if err != nil {
log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

md := metadata.Pairs("authorization", fmt.Sprintf("%s %v", "bearer", result.Token))
ctx := context.Background()
nCtx := metautils.NiceMD(md).ToOutgoing(ctx)
client := pb.NewOnlineDecoderClient(conn)
stream, err := client.Decode(nCtx)
if err != nil {
log.Printf("Failed to create stream: %v\n", err)
log.Fatal(err)
}

// Send the initial configuration message.
if err := stream.Send(&pb.DecoderRequest{
StreamingRequest: &pb.DecoderRequest_StreamingConfig{
StreamingConfig: &pb.DecoderConfig{
SampleRate: int32(SAMPLE_RATE),
Encoding: pb.DecoderConfig_LINEAR16,
UseItn: &True,
UseDisfluencyFilter: &False,
UseProfanityFilter: &False,
},
},
}); err != nil {
log.Fatal(err)
}

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

var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
buf := make([]byte, 1024)
for {
n, err := streamingFile.Read(buf)
if n > 0 {
if err := stream.Send(&pb.DecoderRequest{
StreamingRequest: &pb.DecoderRequest_AudioContent{
AudioContent: buf[:n],
},
}); err != nil {
log.Printf("Could not send audio: %v", err)
}
}
if err == io.EOF {
// Nothing else to pipe, close the stream.
if err := stream.CloseSend(); 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 = stream.Recv()
if err != nil {
log.Fatalf("failed to recv: %v", err)
}

for {
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Printf("Cannot stream results: %v", err)
break
}

if err := resp.Error; err {
log.Printf("Could not recognize: %v", err)
break
}
for _, result := range resp.Results {
if result.IsFinal {
fmt.Printf("final: %v\n", result.Alternatives[0].Text)
} else {
fmt.Printf("%v\n", result.Alternatives[0].Text)
}
}
}
wg.Wait()
}

오류 코드

스트리밍 STT - GRPC 의 오류 처리는 grpc error code를 이용하여 처리합니다.

CodeDescriptionNotes
16Unauthenticated인증 실패
3InvalidArgument잘못된 파라미터 요청
8ResourceExhausted사용량 초과 또는 카드 등록 필요
13Internal서버 오류

참고사항

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