Skip to main content

스트리밍 STT - gRPC

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

연동 예제

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

인증 토큰 발급

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

DecoderConfig

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

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"

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

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로 변환 작업을 수행하는 것이 더 편리합니다.