by Kevin
리턴제로의 음성인식 모델은 우수한 성능을 제공하여 STT 사용에 있어 우선적으로 고려되는 모델입니다. 리턴제로는 개발 가이드 Developer 사이트에서 우수한 음성인식 모델의 OpenAPI를 사용하는 방법을 제공합니다.
이번 튜토리얼에서는 Go 언어로 STT OpenAPI를 직접 사용하고, 복잡한 과정을 크게 줄인 RTZR STT SDK를 사용하여 2가지 경우(일반 STT, 스트리밍 STT)를 비교하겠습니다.
1. 프로젝트 세팅
시작하기에 앞서 프로젝트 환경은 다음과 같습니다.
- Ubuntu 22.04.4 LTS
- go1.23.0 linux/amd64
리턴제로 인증 키 발급
리턴제로의 STT API를 사용하기 위해선 Client 키를 발급받아야 합니다.
아래의 순서대로 진행하여 Client 키를 발급합니다.
- 리턴제로 Developer 사이트 회원가입
- MY 콘솔에 입장
- 애플리케이션 추가 후 API 연동에 필요한 SECRET 정보 발급
Go 프로젝트 만들기
이제 예제를 수행하기 위하여 Go 프로젝트를 만듭니다.
- 새로운 폴더를 만듭니다.
mkdir stt_sample && cd stt_sample
- Go 모듈을 시작합니다.
go mod init example.com/stt_sample
이제 프로젝트 세팅은 끝났습니다. 본격적으로 API를 사용해보겠습니다.
2. 일반 STT
리턴제로의 일반 STT API는 다음과 같은 순서로 진행 됩니다.
여기서 주의할 점은 2가지 입니다.
계속해서 사용을 하기 원하면 토큰을 갱신하는 코드가 필요합니다.
2. STT 변환까지의 시간이 소요됩니다.
변환 프로세싱 중에 ID 조회 시 결과를 얻을 수 없습니다.
API 사용
이제 코드를 작성해 보겠습니다. 코드는 크게 3가지 과정으로 나누어 설명하겠습니다.
클라이언트 인증 과정
ClientId와 ClientSecret을 바탕으로 서버에 인증을 보내어 result
에 JWT 값을 저장합니다.
이 때, ClientId 와 ClientSecret은 개별적으로 받은 값이어야합니다.
const ClientId = "YOUR_CLIEND_ID"
const ClientSecret = "YOUR_CLIENT_SECRET"
data := map[string][]string{
"client_id": {ClientId},
"client_secret": {ClientSecret},
}
resp, _ := http.PostForm("https://openapi.vito.ai/v1/authenticate", data)
if resp.StatusCode != 200 {
panic("Failed to authenticate")
}
// 결과값 중에서 access_token 값만을 result 에 할당
resByte, _ := io.ReadAll(resp.Body)
var result struct {
Token string `json:"access_token"`
}
json.Unmarshal(resByte, &result)
음성파일 업로드 과정
MakeReq
함수에서 음성 파일을 보낼 Request를 정의하고, 서버에 전달하여 STT 프로세싱을 시작합니다. 서버에서는 프로세싱 중인 Id를 반환하며, 이 Id는 추후 결과를 받아오기 위해 사용됩니다.
req, err := MakeReq(filePath, result.Token)
response, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("Error resposne")
}
defer response.Body.Close()
resByte, _ = io.ReadAll(response.Body)
var resultId struct {
Id string `json:"id"`
}
json.Unmarshal(resByte, &resultId)
MakeReq
함수는 다음과 같이 정의합니다.
func SendReq(filePath, token string) (*http.Request, error)
함수에 대해 자세히 알아보겠습니다.
- 파일을 업로드하기 위한
multipart.NewWriter
를 생성합니다.
var buf bytes.Buffer
// 파일 업로드를 위한 multipart writer 생성
writer := multipart.NewWriter(&buf)
- 음성 파일을
file
폼에 작성합니다.
// 읽을 파일 열기
audiofile, _ := os.Open(file)
defer audiofile.Close()
// 음성 파일 write
fw, _ := writer.CreateFormFile("file", audiofile.Name())
if _, err := io.Copy(fw, audiofile); err != nil {
log.Fatal(err)
}
- 음성 파일과 함께 보낼 Config를 작성합니다.
리턴제로의 STT API 에서는 STT를 위한 유용한 기능들을 제공합니다. 아래 링크에서 자세히 확인하실 수 있습니다.
이제 원하는 조건에 따라 Config를 작성하고합니다. 작성이 끝나면 반드시 writer.Close()
로 닫아주셔야합니다.
// 음성 파일 처리 config 처리
fw, _= writer.CreateFormField("config")
// config 작성
config := map[string]interface{}{
"model_name": "sommers",
"use_itn": true,
}
// json 변환
j, _ := json.Marshal(config)
if _, err := fw.Write(j); err != nil {
log.Fatal(err)
}
// multipart를 다 쓴 후 반드시 Close.
writer.Close()
- 마지막으로 헤더에 인증 토큰을 넣고 Request를 완성합니다.
// Server에 보낼 request 생성
req, err := http.NewRequest(http.MethodPost, "https://openapi.vito.ai/v1/transcribe", &buf)
req.Header.Add("Content-Type", writer.FormDataContentType())
req.Header.Add("Authorization", fmt.Sprintf("%s %v", "bearer", token))
if err != nil {
return nil, err
}
return req, err
결과 반환
결과 반환의 방식으로 서버에 일정한 주기로 요청을 보내는 Polling 방식을 사용합니다. 예제에서는 매 5초마다 서버에 요청을 하여 프로세싱이 성공적으로 끝났을 시 결과를 반환합니다.
// Polling 방식으로 결과값 반환할 때까지 요청
maxRetries := 10
for i := 0; i < maxRetries; i++ {
time.Sleep(5 * time.Second)
var buf bytes.Buffer
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%v", ServerHost, resultId.Id), &buf)
if err != nil {
return
}
req.Header.Set("Authorization", fmt.Sprintf("%s %v", "bearer", result.Token))
res, err := http.DefaultClient.Do(req)
if res.StatusCode != 200 {
continue
}
resByte, _ := io.ReadAll(res.Body)
result := new(SttResult)
json.Unmarshal(resByte, &result)
fmt.Println(result.Status)
if result.Status == "completed" {
fmt.Println(result.Results)
return
}
}
RTZR STT SDK 사용
Go에서 일반 STT를 사용하기 위해서는 일련의 과정이 필요하고, 그 과정을 위해 많은 코드들이 필요했습니다. 클라이언트들은 이러한 모든 과정을 알 필요가 없기 때문에 불필요한 과정을 최소화한 RTZR STT SDK를 사용하면 더 간단하게 STT API를 사용할 수 있습니다.
RTZR STT SDK 모듈 설치
go get -u "github.com/vito-ai/go-sdk"
환경 변수 설정
SDK를 사용하기 위해서는 환경변수를 설정해야 합니다. 아래의 명령어를 통해 환경변수를 설정합니다.
export RTZR_CLIENT_ID="YOUR_CLIENT_ID"
export RTZR_CLIENT_SECRET="YOUR_CLIENT_SECRET"
이렇게 하면 모든 세팅은 끝났습니다. 아래는 SDK로 사용 가능한 메소드입니다.
NewRestClient
일반 STT API를 사용할 Client를 선언합니다. 모든 일반 STT 작업은 Client를 바탕으로 수행합니다. 파라미터로는 *option.ClientOption
을 가집니다.
- *option.ClientOption - 선택적 파라미터입니다. 환경변수 설정이 끝났으면
nil
을 넘겨줍니다.ClientId
,ClientSecret
,Endpoint
,TokenURL
을 선택적으로 줄 수 있습니다.
client, err := speech.NewRestClient(nil)
if err != nil {
fmt.Println(err)
return
}
Recognize
동기적으로 STT 결과를 받아옵니다. 짧은 음성 파일에서 사용을 권장합니다. 파라미터로 context.Context
, RecognizeRequest
가 필요합니다.
- context.Context - 서버에 요청을 안전하게 처리하기 위하여 context를 이용하여 관리합니다.
- RecognizeRequest - 음성 파일과 보낼 Config를 정의합니다.
- Config - Config를 설정합니다.
- AudioSource - 서버에 보낼 음성 파일을 정의합니다.
FilePath
를 통해 로컬에 존재하는 음성 파일을 읽거나,Content
를 통해[]byte
데이터를 받을 수 있습니다. 두 파라미터가 동시에 들어오면 에러가 발생합니다.
결과로 RecognizeResponse
를 받아옵니다.
아래는 Recognize
사용 예제입니다.
var True = true
var False = false
filePath := "sample.wav" # 로컬 파일 위치
// STT 동기적으로 전송
resp, err := client.Recognize(ctx, &speech.RecognizeRequest{
Config: speech.RecognitionConfig{
ModelName: "somemrs",
UseItn: &True,
},
AudioSource: speech.RecognitionAudio{
FilePath: filePath,
},
})
if err != nil {
fmt.Println(err)
return
}
// 결과 출력
for _, utterance := range resp.Results.Utterances {
fmt.Println(utterance.Msg)
}
RecognizeAsync
비동기적으로 STT를 처리합니다. 용량이 큰 파일에서 사용을 권장합니다.
파라미터는 context.Context
, RecognizeRequest
로 동일합니다.
결과로 ResultId
가 주어집니다. 추후 결과를 받아오기 위해 사용됩니다.
resId, err := client.RecognizeAsync(ctx, &speech.RecognizeRequest{
Config: speech.RecognitionConfig{
ModelName: "somemrs",
UseItn: &True,
},
AudioSource: speech.RecognitionAudio{
FilePath: filePath,
},
})
ReceiveResult
RecognizeAsync에서 받아온 ResultId
를 통해 결과를 받아옵니다. 아직 결과가 생성이 되지 않았다면, ErrNotFinish
에러가 발생합니다.
파라미터로 context.Context
,ResultId
를 받습니다.
res, err := client.ReceiveResult(ctx, resId)
if err != nil && err != speech.ErrNotFinish {
return
}
for _, utterance := range res.Results.Utterances {
fmt.Println(utterance)
}
3. 스트리밍 STT
스트리밍 방식에서는 gRPC 방식 사용을 권장합니다. 스트리밍으로 데이터를 주고받는 방식은 다음과 같습니다.
gRPC
gRPC는 Google에서 개발한 Remote Procedure Call 프레임워크입니다. gRPC는 Protocol Buffer와 HTTP/2 기반의 전송방식을 이용하여 효율적으로 통신합니다.
클라이언트가 gRPC API를 이용하기 위해선, 함수명이나 인자, 반환 값 등에 대한 데이터형이 정의된 Protocol Buffer .proto
IDL 파일을 컴파일하여, “Stub”을 생성해야 합니다.
파일 스트리머 설정
예제를 위하여 실시간 음성을 받아올 수 없는 상황이기 때문에 음성 파일을 실시간처럼 만들어주는 FileStreamer를 정의합니다.
이를 위하여 ffmpeg 을 사용합니다.
import "github.com/xfrr/goffmpeg/transcoder"
const SAMPLE_RATE int = 8000
const BYTES_PER_SAMPLE int = 2
/*
예제를 위해 파일을 읽기 위한 Interface 만들기
*/
type FileStreamer struct {
file *os.File
}
// 정해진 byteSize (maxSize = 1024) 까지 파일을 읽어와서 전달
func (fs *FileStreamer) Read(p []byte) (int, error) {
byteSize := len(p)
maxSize := 1024
if byteSize > maxSize {
byteSize = maxSize
}
// buffer가 터지는 것을 막기 위하여 delay 시킴.
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) // local에서 오디오 파일을 찾는다.
i := strings.LastIndex(fileName, ".") // 오디오 파일의 확장자를 찾는다.
audioFileName8K := filepath.Join(os.TempDir(), fileName[:i]) + fmt.Sprintf("_%d.%s", SAMPLE_RATE, "wav") //결과 확장자를 wav 파일로 설정
// //FFmpeg을 통해 음성 파일 변환
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) // 변환이 완료할 때까지 block
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
}
API 사용
예제 코드는 공식 사용 예제를 참고합니다.
앞의 인증 관련 코드는 똑같기에 생략합니다.
스트리밍 STT를 위한 Stub 모듈 설치
go get -u github.com/vito-ai/go-genproto
다음과 같이 사용할 수 있습니다.
//main.go
import (
pb "github.com/vito-ai/go-genproto/vito-openapi/stt"
)
gRPC Client 생성 및 연결 설정
grpc.DialOption
을 설정합니다.grpc.NewClient
를 생성하고 서버와의 커넥션 연결상태를 체크합니다.- metadata에 Token 값을 넣음으로서 인증 설정을 완료합니다.
// 스트리밍 gRPC 예제
// 1.
var dialOpts []grpc.DialOption
//TLS certificate nil로 해서 header를 통한 authority 진행
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")))
// 2.
conn, err := grpc.NewClient(ServerHost, dialOpts...)
if err != nil {
log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()
// Server 상태 체크
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
conn.WaitForStateChange(ctx, connectivity.Ready)
// 3.
md := metadata.Pairs("authorization", fmt.Sprintf("%s %v", "bearer", result.Token))
ctx = context.Background()
newCtx := metautils.NiceMD(md).ToOutgoing(ctx) //metautils를 통해 요청 보낼때 header에 bearer 인증 반복적으로 보냄
STT 스트림 시작 및 Config 전달
pb.NewOnlineDecoderClient
을 통해 스트림을 위한 client를 생성합니다.client.Decode
를 통해 스트림 통신을 관리하는 스트림 클라이언트를 선언합니다.- 서버에 STT Config를 보내 스트림 정보를 전달합니다.
- 인코딩 지원 방식 참고 : https://developers.rtzr.ai/docs/stt-streaming
// 1.
client := pb.NewOnlineDecoderClient(conn)
// 2.
stream, err := client.Decode(newCtx)
if err != nil {
log.Printf("Failed to create stream: %v\\n", err)
log.Fatal(err)
}
// 3.
// 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,
},
},
}); err != nil {
log.Fatal(err)
}
실시간 스트리밍
실시간 스트리밍은 3부분으로 나눠 살펴보겠습니다.
- STT에 사용할 오디오 파일을 엽니다.
앞서 정의한 FileStreamer
를 이용하여 파일을 엽니다. 파일을 안전하게 읽고 종료하기 위하여 WaitGroup
을 사용합니다.
// AudioFile을 열기
streamingFile, err := OpenAudioFile(audioFile)
if err != nil {
log.Fatal(err)
}
defer streamingFile.Close()
//Wait Group을 걸어 안전하게 종료
var wg sync.WaitGroup
wg.Add(1)
- 스트림 과정 중 전송 과정입니다.
양방향 통신을 위하여 고루틴을 생성합니다. 파일에서 데이터를 순차적으로 읽고 서버에 전달합니다. 최종적으로 다 읽었을 시 연결을 끊고, WaitGroup.Done()
을 호출합니다.
go func() {
//파일을 다 읽으면 wg을 종료
defer wg.Done()
//파일을 쓰기 위한 buf 생성
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
}
}
}()
- 스트림 과정 중 수신 과정 입니다.
서버로부터 결과를 스트림으로 받아옵니다. 서버에서 받은 결과를 출력합니다. 이 과정은 Send가 끝날때 까지 받아옵니다.
_, 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()
RTZR STT SDK 사용
일반 STT와 마찬가지로 스트리밍 STT SDK를 통해 편하게 구현할 수 있습니다. 다음은 위와 똑같은 예제의 STT SDK를 이용한 코드입니다.
일반 STT SDK와 같이 환경변수를 설정하고 사용해야합니다.
NewStreamingClient
스트리밍 STT를 사용할 Client를 선언합니다. 파라미터로는 context.Context
*option.ClientOption
이 필요합니다.
- context.Context - Client를 만들기 위해 context를 사용합니다.
- *option.ClientOption - 선택적 파라미터입니다. 환경변수 설정이 끝났으면
nil
을 넘겨줍니다.ClientId
,ClientSecret
,Endpoint
,TokenURL
을 선택적으로 줄 수 있습니다.
ctx := context.Background()
client, err := speech.NewStreamingClient(ctx, nil)
if err != nil {
log.Println(err)
return
}
StreamingRecognize
Client를 바탕으로 스트림 연결을 설정합니다. 파라미터로 context.Context
가 필요합니다.
stream, err := client.StreamingRecognize(ctx)
if err != nil {
log.Println(err)
return
}
Config 전달 및 스트리밍 시작
만들어진 스트림을 통해 Config를 전송합니다. 이후 실시간 스트리밍을 시작합니다. 실시간 스트리밍 코드는 API 사용 예제와 같아 생략합니다.
if err := stream.Send(&pb.DecoderRequest{
StreamingRequest: &pb.DecoderRequest_StreamingConfig{
StreamingConfig: &pb.DecoderConfig{
SampleRate: int32(SAMPLE_RATE), // 필수
Encoding: pb.DecoderConfig_LINEAR16, //필수
UseItn: &True,
},
},
}); err != nil {
log.Fatal(err)
}
마무리
오늘은 Go 언어로 리턴제로의 일반 STT, 스트리밍 STT 예제를 만들어 보았습니다. Go 언어를 이용하면 쉽게 STT를 이용한 서비스를 구현할 수 있습니다. 특히 리턴제로에서 제공하는 STT SDK를 사용하면 그 과정은 훨씬 쉬워졌습니다. 이를 활용하여 다양한 서비스에서 STT를 사용해보세요.
리턴제로의 STT API는 Go 언어만이 아닌 다양한 언어로 사용할 수 있습니다. 다른 언어에 대한 예제는 아래의 링크를 들어가시면 확인하실 수 있습니다.
튜토리얼의 코드는 아래에서 모두 확인하실 수 있습니다.