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
전체코드
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}