Spring Boot에서 VITO 음성인식 API 사용하기

프로덕트에 음성인식 기능을 추가하면 더욱 멋있어 질 것 같습니다. 뛰어난 성능을 자랑하는 VITO API를 내 서버에서 사용하여 음성인식 기능을 추가하려면 어떻게 해야 할까요? 이번 글은 Spring Boot 로 구성된 서버에서 VITO API를 사용하는 튜토리얼에 대해서 작성한 글입니다.

Spring Boot에서 VITO 음성인식 API 사용하기

by Roger


프로덕트에 음성인식 기능을 추가하면 더욱 멋있어 질 것 같습니다. 뛰어난 성능을 자랑하는 VITO API를 내 서버에서 사용하여 음성인식 기능을 추가하려면 어떻게 해야 할까요?

이번 글은 Spring Boot 로 구성된 서버에서 VITO API를 사용하는 튜토리얼에 대해서 작성한 글입니다.


VITO API 사용 구조

클라이언트는 서비스 서버에 음성파일 전사를 요청하고 서비스 서버에서 VITO 서버로 API 호출 및 응답 처리 후 클라이언트로 다시 반환하는 구조로 설계하려고 합니다.
음성 파일 전사 요청후 바로 변환 결과를 주는 것이 아니라 transcribe_id를 받고 이 id를 기반으로 전사가 끝날때까지 polling 방식으로 요청하여 결과를 얻을 수 있습니다.


홈페이지에서 애플리케이션 등록하기

VITO API를 사용하기 위해선 VITO Developers에 회원가입 후 콘솔에서 애플리케이션을 등록해야 합니다. 아래와 같이 등록 후 CLIENT ID, CLIENT SECRET 값을 잘 저장해 두어야 합니다.

Controller 구성하기

함수 한개 한개를 세부적으로 살펴보기 전에 Controller의 모습을 보도록 하겠습니다.

@RestController
@RequestMapping("/api/tutorial")
public class TutorialController {
    private final TutorialService tutorialService;

    public TutorialController(TutorialService tutorialService) {
        this.tutorialService = tutorialService;
    }

    @PostMapping("/transcribe")
    public void transcribe(){
        tutorialService.requestTranscribe();
    }

    @PostMapping("/websocket")
    public void websocketTranscribe() throws UnsupportedAudioFileException, IOException, InterruptedException {
        tutorialService.transcribeWebSocket();
    }
}
저장된 파일을 전부 변환한뒤에 결과를 반환하는 requestTranscribe()
WebSocket을 통해 실시간 음성변환을 해주는 transcribeWebSocket()

TutorialService.java에서 위 두가지 함수를 구현하도록 하겠습니다.

Token 발급 받기

VITO OPEN API는 6시간을 만료시간으로 가지는 Token을 사용합니다. 아래의 함수를 통해 accessToken을 갱신합니다. 이때 Spring WebClient를 사용하여 VITO token 발급 API를 호출합니다.

...
    @Value("${vito.client_id}")
    String client_id;

    @Value("${vito.client_secret}")
    String client_secret;
...

public String getAccessToken(){
        WebClient webClient = WebClient.builder()
                .baseUrl("https://openapi.vito.ai")
                .build();



        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("client_id", client_id);
        formData.add("client_secret", client_secret);


        String response = webClient
                .post()
                .uri("/v1/authenticate")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .body(BodyInserters.fromFormData(formData))
                .retrieve()
                .bodyToMono(String.class)
                .block();

        log.info(response);
        JSONObject jsonObject = new JSONObject(response.toString());
        return jsonObject.getString("access_token");

Transcribe id 받기 (전사 요청)

클라이언트는 Spring 서버에 파일과 함께 전사를 요청하면 서버는 VITO 서버에 전사를 요청후 받아오는 작업을 거치게 됩니다. 아래와 같이 두 단계를 거쳐 전사 결과를 확인할 수 있습니다.

POST를 통해 VITO API 에 파일 전사 요청 → transcribe_id 반환
transcribe_id를 통해 Polling 으로 전사 완료까지 지속 확인

POST 요청 한 뒤에 Transcribe id 받아오기

파일과 함께 POST 요청을 통해 받은 transcribe_id를 활용하여 GET을 통해 요청합니다.

public void transcribeFile(MultipartFile multipartFile) throws IOException, InterruptedException {

        accessToken = getAccessToken();
        WebClient webClient = WebClient.builder()
                .baseUrl("https://openapi.vito.ai/v1")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, String.valueOf(MediaType.MULTIPART_FORM_DATA))
                .defaultHeader(HttpHeaders.AUTHORIZATION, "bearer " + accessToken)
                .build();

        Path currentPath = Paths.get("");
        File file = new File(currentPath.toAbsolutePath().toString() + "/"+multipartFile.getOriginalFilename());
        multipartFile.transferTo(file);

        MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder();
        multipartBodyBuilder.part("file", new FileSystemResource(file));
        multipartBodyBuilder.part("config", "{}");

        // POST 요청 보내기
        String response = null;
        try{
            response = webClient.post()
                    .uri("/transcribe")
                    .body(BodyInserters.fromMultipartData(multipartBodyBuilder.build()))
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();
        }catch (WebClientResponseException e){
            log.error(String.valueOf(e));
        }

        JSONObject jsonObject = new JSONObject(response.toString());

        try{
            if(jsonObject.getString("code").equals("H0002")){
                log.info("accessToken 만료로 재발급 받습니다");
                accessToken = getAccessToken();
                response = webClient.post()
                        .uri("/transcribe")
                        .body(BodyInserters.fromMultipartData(multipartBodyBuilder.build()))
                        .retrieve()
                        .bodyToMono(String.class)
                        .block();
            }
        }catch (JSONException e){
            log.info("code 확인 불가 오류 catch");
            log.info(e.toString());
        }

        log.info("transcribe 요청 id : " + jsonObject.getString("id"));

        stopPolling = false;
        Thread.sleep(10);
        transcribeId = jsonObject.getString("id");
        startPolling();
    }

Polling 방식을 사용해서 지속적으로 확인하기

전사 요청후 바로 결과를 기대하기는 어렵기 때문에 5초뒤에 결과를 확인하고 이후에 5초 주기로 전사 완료 여부를 확인합니다.

public void startPolling() throws InterruptedException {
      log.info("Polling 함수 첫 시작");
      Thread.sleep(5000);
      while (!stopPolling) {
            log.info("while polling 시작 반복중");
            WebClient webClient = WebClient.builder()
                    .baseUrl("https://openapi.vito.ai/v1")
                    .defaultHeader(HttpHeaders.AUTHORIZATION, "bearer " + accessToken)
                    .build();


            String uri = "/transcribe/" + transcribeId;
            String response = webClient.get()
                    .uri(uri)
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();



            JSONObject jsonObject = new JSONObject(response.toString());
            // status 확인하여 폴링 중단 여부 결정
            if (jsonObject.getString("status").equals("completed")) {
                stopPolling = true;
            }

            try {
                Thread.sleep(5000); // 폴링 주기 (5초)를 설정
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            log.info("while polling 끝 반복중");
        }
        log.info("폴링함수 끝");
    }
  • 결과 log (아래와 같이 나오면 정상적으로 Polling 을 진행한 것입니다)

Streaming API(WebSocket) 사용하여 실시간으로 보내고 받아오기

저장된 파일로부터 전사를 요청하여 결과를 받을 수 있지만 실시간으로 전사가 필요한 경우 Streaming API를 사용할 수 있습니다.

FileInputStream을 통해 파일을 바이트 단위로 불러옵니다.
홈페이지에 있는 개발 가이드라인은 encoding 형식을 Linear16으로 지정하여 AudioInputStream 을 통해 파일을 읽어왔지만 encoding을 WAV로 지정시에는 FileInputStream 을 사용해야 실제 파일 encoding 형식을 wav로 인식합니다. 따라서 본 튜토리얼에서는 FileInputStream 을 사용합니다.

wav파일 전송시에는 헤더에서 sample_rate와 channel을 자동으로 읽을 수 있기 때문에 어떠한 값을 주어도 정확한 결과를 받을 수 있습니다.

public void transcribeWebSocketFile(MultipartFile multipartFile) throws IOException, InterruptedException {
        Logger logger = Logger.getLogger(VitoSttWebSocketClient.class.getName());
        OkHttpClient client = new OkHttpClient();
        String token = getAccessToken();

        HttpUrl.Builder httpBuilder = HttpUrl.get("<https://openapi.vito.ai/v1/transcribe:streaming>").newBuilder();
        httpBuilder.addQueryParameter("sample_rate", "44100");
        httpBuilder.addQueryParameter("encoding", "WAV");
        httpBuilder.addQueryParameter("use_itn", "true");
        httpBuilder.addQueryParameter("use_disfluency_filter", "true");
        httpBuilder.addQueryParameter("use_profanity_filter", "true");

        String url = httpBuilder.toString().replace("https://", "wss://");

        Request request = new Request.Builder()
                .url(url)
                .addHeader("Authorization", "Bearer " + token)
                .build();

        VitoWebSocketListener webSocketListener = new VitoWebSocketListener();
        WebSocket vitoWebSocket = client.newWebSocket(request, webSocketListener);

        FileInputStream fis = null;
        Path currentPath = Paths.get("");
        File file = new File(currentPath.toAbsolutePath().toString() + "/"+multipartFile.getOriginalFilename());
        multipartFile.transferTo(file);
        try {
            fis = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            System.exit(1);
        }

        byte[] buffer = new byte[1024];
        int readBytes;
        while ((readBytes = fis.read(buffer)) != -1) {
            boolean sent = vitoWebSocket.send(ByteString.of(buffer, 0, readBytes));
            if (!sent) {
                logger.log(Level.WARNING, "Send buffer is full. Cannot complete request. Increase sleep interval.");
                System.exit(1);
            }
            Thread.sleep(0, 100);
        }
        fis.close();
        vitoWebSocket.send("EOS");

        webSocketListener.waitClose();
        client.dispatcher().executorService().shutdown();

    }

개선여지

실 서비스에서 VITO API를 사용하기 위해서 추가로 고려해야 할 사항들 몇가지는 다음과 같습니다.

  • Token 재발급 API를 매번 호출하는 대신 Expire 시간확인 후 Expire 처리 (Redis와 같은 별도 DB 사용)
  • File Size에 따른 적절한 Polling 주기 설정
  • API 응답 Parsing
  • 상황에 맞는 Exception 처리

참조

음성인식 API 시작하기 | VITO STT OpenAPI
WAV - Waveform Audio File Format

전체코드

GitHub - vito-ai/java-spring-sample
Contribute to vito-ai/java-spring-sample development by creating an account on GitHub.

application-prod.yml 에서 VITO_CLIENT_ID, VITO_CLIENT_SECRET을 본인의 값으로 넣어야 합니다.

spring:
  config:
    name: secondtutorial
    activate:
      on-profile: prod
  
  servlet:
    multipart:
      max-file-size: 200MB

vito:
  client_id: ${VITO_CLIENT_ID}
  client_secret: ${VITO_CLIENT_SECRET}

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.