Go 언어에서 음성인식 API 사용하기

이번 튜토리얼에서는 Go 언어로 일반 STT 방식과 스트리밍 STT 방식의 예제 코드를 API를 사용한 경우와 SDK를 사용한 2가지 방식으로 보여줍니다.

Go 언어에서 음성인식 API 사용하기

by Kevin

리턴제로의 음성인식 모델은 우수한 성능을 제공하여 STT 사용에 있어 우선적으로 고려되는 모델입니다. 리턴제로는 개발 가이드 Developer 사이트에서 우수한 음성인식 모델의 OpenAPI를 사용하는 방법을 제공합니다.

음성인식 API 시작하기 | RTZR STT OpenAPI
RTZR STT OpenAPI는 눈으로 보는 통화 VITO, 회의내용을 자산으로 만드는 Callabo, AI 숏폼 서비스 를 서비스 하는 리턴제로의 우수한 음성인식 기능을 API로 제공합니다.

이번 튜토리얼에서는 Go 언어로 STT OpenAPI를 직접 사용하고, 복잡한 과정을 크게 줄인 RTZR STT SDK를 사용하여 2가지 경우(일반 STT, 스트리밍 STT)를 비교하겠습니다.

1. 프로젝트 세팅

시작하기에 앞서 프로젝트 환경은 다음과 같습니다.

  • Ubuntu 22.04.4 LTS
  • go1.23.0 linux/amd64

리턴제로 인증 키 발급

리턴제로의 STT API를 사용하기 위해선 Client 키를 발급받아야 합니다.

아래의 순서대로 진행하여 Client 키를 발급합니다.

  1. 리턴제로 Developer 사이트 회원가입
  2. MY 콘솔에 입장
  3. 애플리케이션 추가 후 API 연동에 필요한 SECRET 정보 발급
Client Secret은 분실 시 재발급만 가능하니 반드시 별도 저장이 필요합니다.

Go 프로젝트 만들기

이제 예제를 수행하기 위하여 Go 프로젝트를 만듭니다.

  1. 새로운 폴더를 만듭니다.
  mkdir stt_sample && cd stt_sample 
  1. Go 모듈을 시작합니다.
  go mod init example.com/stt_sample

이제 프로젝트 세팅은 끝났습니다. 본격적으로 API를 사용해보겠습니다.


2. 일반 STT

리턴제로의 일반 STT API는 다음과 같은 순서로 진행 됩니다.

여기서 주의할 점은 2가지 입니다.

💡
1. JWT 의 인증 만료 기간은 6시간입니다.
계속해서 사용을 하기 원하면 토큰을 갱신하는 코드가 필요합니다.

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)

함수에 대해 자세히 알아보겠습니다.

  1. 파일을 업로드하기 위한 multipart.NewWriter를 생성합니다.
  var buf bytes.Buffer
  // 파일 업로드를 위한 multipart writer 생성
  writer := multipart.NewWriter(&buf)
  1. 음성 파일을 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)
  }
  1. 음성 파일과 함께 보낼 Config를 작성합니다.

리턴제로의 STT API 에서는 STT를 위한 유용한 기능들을 제공합니다. 아래 링크에서 자세히 확인하실 수 있습니다.

음성인식 모델 | RTZR STT OpenAPI
사용할 음성인식 모델을 설정하는 기능입니다. 제공하는 모델의 종류는 기본 모델인 sommers 와 리턴제로가 직접 파인튜닝한 OpenAPI의 whisper 가 있습니다.

이제 원하는 조건에 따라 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()
  1. 마지막으로 헤더에 인증 토큰을 넣고 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 방식 사용을 권장합니다. 스트리밍으로 데이터를 주고받는 방식은 다음과 같습니다.

리턴제로 스트리밍 STT 진행 방식

gRPC

gRPC는 Google에서 개발한 Remote Procedure Call 프레임워크입니다. gRPC는 Protocol Buffer와 HTTP/2 기반의 전송방식을 이용하여 효율적으로 통신합니다.

클라이언트가 gRPC API를 이용하기 위해선, 함수명이나 인자, 반환 값 등에 대한 데이터형이 정의된 Protocol Buffer .proto IDL 파일을 컴파일하여, “Stub”을 생성해야 합니다.

💡
Go 언어의 경우 "Stub"을 모듈로써 받을 수 있어 컴파일 과정이 생략됩니다.

파일 스트리머 설정

예제를 위하여 실시간 음성을 받아올 수 없는 상황이기 때문에 음성 파일을 실시간처럼 만들어주는 FileStreamer를 정의합니다.

이를 위하여 ffmpeg 을 사용합니다.

🚧
튜토리얼 예제를 위해 필요하지만, 파일의 경우에는 앞선 일반 STT 모델 사용을 권장합니다.
  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 - GRPC | RTZR STT OpenAPI
본 문서는 스트리밍 STT 중에서 GRPC로 구현하는 방식에 대한 가이드를 제공합니다.

앞의 인증 관련 코드는 똑같기에 생략합니다.

스트리밍 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 생성 및 연결 설정

  1. grpc.DialOption 을 설정합니다.
  2. grpc.NewClient를 생성하고 서버와의 커넥션 연결상태를 체크합니다.
  3. 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 전달

  1. pb.NewOnlineDecoderClient을 통해 스트림을 위한 client를 생성합니다.
  2. client.Decode를 통해 스트림 통신을 관리하는 스트림 클라이언트를 선언합니다.
  3. 서버에 STT Config를 보내 스트림 정보를 전달합니다.
실시간 스트리밍에서는 보내는 음성의 EncodingSampleRate를 지정해야합니다.
  // 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부분으로 나눠 살펴보겠습니다.

  1. 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)
  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
          }
      }
  }()
  1. 스트림 과정 중 수신 과정 입니다.

서버로부터 결과를 스트림으로 받아옵니다. 서버에서 받은 결과를 출력합니다. 이 과정은 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 언어만이 아닌 다양한 언어로 사용할 수 있습니다. 다른 언어에 대한 예제는 아래의 링크를 들어가시면 확인하실 수 있습니다.

Tutorial - 기업을 위한 음성 AI - 리턴제로 blog

튜토리얼의 코드는 아래에서 모두 확인하실 수 있습니다.

go-tutorial/go_stt_example at main · vito-ai/go-tutorial
Contribute to vito-ai/go-tutorial development by creating an account on GitHub.

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to 기업을 위한 음성 AI - 리턴제로 blog.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.