[Spring] Google Speech-to-Text로 오디오 파일을 스크립트로 변환하기
Google Cloud Platform에서 제공하는 Speech-to-Text API를 활용하여 10초 이내의 오디오 파일을 스크립트로 변환합니다
내 프로젝트 환경
JVM 17.0.6
Gradle 7.6
Springboot 3.0.2
시작하기 전에
프로젝트에서 Speech-to-Text 사용 설정
1. 프로젝트 만들기
기존 프로젝트를 선택하거나 새 프로젝트를 만듭니다.
프로젝트 만들기를 선택합니다
프로젝트 이름, 조직을 입력하고 만들기를 선택합니다
프로젝트가 생성되었습니다
2. 결제 계정 등록
좌측 상단의 햄버거 버튼을 누르면 결제 개요를 볼 수 있습니다
결제 개요에서 모든 상태 확인을 선택합니다
계정 관리를 선택합니다
저는 만들어둔 결제 계정이 있습니다. 결제 계정이 하나도 없다면 생성하고 프로젝트에 연결해줍니다.
3. 서비스 계정 만들기
사용자가 만드는 애플리케이션은 서비스 계정을 사용해 인증된 API 호출을 수행합니다.
서비스 계정을 검색하여 들어갑니다
만들어둔 프로젝트를 선택합니다
서비스 계정 만들기를 선택합니다
서비스 계정 ID는 고유한 이름으로 설정합니다. 액세스 권한은 선택사항입니다.
4. 서비스 계정의 JSON 키 만들기
키 관리를 선택합니다
새 키 만들기를 선택합니다
JSON 형식의 비공개 키를 만들면 자동으로 컴퓨터에 저장됩니다
5. 개발 환경에 인증 환경 변수 설정
프로젝트와 연결한 서비스 계정의 JSON키로 GOOGLE_APPLICATION_CREDENTIALS를 설정합니다.
KEY_PATH를 위에서 받은 .json 키의 경로로 설정합니다.
이 변수는 현재 셀 세션에만 적용됩니다. 매번 프로젝트를 시작할 때 마다 변수를 자동으로 설정되게 하고 싶다면 셀 시작 파일에 변수를 설정해야 합니다. 보통 mac의 terminal은 ~/.bash_profile, iterm은 ~/.zshrc에 환경변수를 설정합니다.
export GOOGLE_APPLICATION_CREDENTIALS="{KEY_PATH}"
+인증 정보가 없어 테스트가 실패할 때
인텔리제이에 환경 변수를 등록하여 인텔리제이에서 테스트를 실행할 때 애플리케이션에 사용자 인증 정보를 제공할 수 있습니다.
shift * 2(검색 도구 열기) → Run/Debug Configuration → Enviroment variables
User enviroment variables에 환경 변수를 추가해줍니다.
클라이언트 라이브러리를 사용하여 음성을 텍스트로 변환하기
클라이언트 라이브러리 설치
build.gradle.kts
빌드 툴이 다르다면 여기를 참고하여 라이브러리를 설치하세요
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
// Google Speech To Text
implementation(platform("com.google.cloud:libraries-bom:26.1.4"))
implementation("com.google.cloud:google-cloud-speech")
implementation("com.google.protobuf:protobuf-java:3.21.12")
implementation("com.fasterxml.jackson.core:jackson-databind")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
오디오 -> 텍스트 변환 요청하기
SpeechToTextService
import com.google.cloud.speech.v1.*;
import com.google.protobuf.ByteString;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
@RequiredArgsConstructor
@Service
public class SpeechToTextService {
private final Logger logger = LoggerFactory.getLogger(SpeechToTextService.class);
public String transcribe(MultipartFile audioFile) throws IOException {
if (audioFile.isEmpty()) {
throw new IOException("Required part 'audioFile' is not present.");
}
// 오디오 파일을 byte array로 decode
byte[] audioBytes = audioFile.getBytes();
// 클라이언트 인스턴스화
try (SpeechClient speechClient = SpeechClient.create()) {
// 오디오 객체 생성
ByteString audioData = ByteString.copyFrom(audioBytes);
RecognitionAudio recognitionAudio = RecognitionAudio.newBuilder()
.setContent(audioData)
.build();
// 설정 객체 생성
RecognitionConfig recognitionConfig =
RecognitionConfig.newBuilder()
.setEncoding(RecognitionConfig.AudioEncoding.FLAC)
.setSampleRateHertz(44100)
.setLanguageCode("en-US")
.build();
// 오디오-텍스트 변환 수행
RecognizeResponse response = speechClient.recognize(recognitionConfig, recognitionAudio);
List<SpeechRecognitionResult> results = response.getResultsList();
if (!results.isEmpty()) {
// 주어진 말 뭉치에 대해 여러 가능한 스크립트를 제공. 0번(가장 가능성 있는)을 사용한다.
SpeechRecognitionResult result = results.get(0);
return result.getAlternatives(0).getTranscript();
} else {
logger.error("No transcription result found");
return "";
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
SttController
import {PROJECT_PATH}.service.SpeechToTextService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@Slf4j
@RestController
@RequestMapping("stt")
public class SttRestController {
@Autowired
private SpeechToTextService sttService;
/**
* 녹음 파일을 받아서 텍스트로 변환하여 반환
*
* @param audioFile 오디오 파일
* @return 녹음 파일을 변환한 텍스트
*/
@PostMapping(value = "/audio", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> handleAudioMessage(@RequestParam("audioFile") MultipartFile audioFile) throws IOException {
String transcribe = sttService.transcribe(audioFile);
return ResponseEntity.ok().body(transcribe);
}
}
실행
POSTMAN API 테스트
./gradlew bootrun // 프로젝트 실행
SttControllerTest
MockMvc로 스프링 MVC 테스트를 해보았습니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.io.FileInputStream;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SttRestControllerTest {
private final Logger logger = LoggerFactory.getLogger(SttRestControllerTest.class);
@Autowired
/**
* 웹 API 테스트할 때 사용
* 스프링 MVC 테스트의 시작점
* HTTP GET,POST 등에 대해 API 테스트 가능
* */
private MockMvc mockMvc;
@Test
@DisplayName("STT CONTROLLER : /stt/audio")
public void testHandleAudioMessage() throws Exception {
//given
// Create a mock audio file
final String fileName = "test-audio";
final String fileType = "flac";
final String filePath = "src/test/resources/" + fileName + "." + fileType;
FileInputStream audioFileInputStream = new FileInputStream(filePath);
MockMultipartFile audioFile = new MockMultipartFile(
"audioFile",
fileName + "." + fileType,
fileType,
audioFileInputStream);
// Set userSeq and slideIdx values
Long userSeq = 1234L;
Integer slideIdx = 5;
//when
//then
// Call the controller endpoint
MvcResult result = mockMvc.perform(multipart("/stt/audio")
.file(audioFile)
.andExpect(status().isOk())
.andReturn();
// Verify the response
String responseBody = result.getResponse().getContentAsString();
assertNotNull(responseBody);
}
}
두번째 테스트가 위에 작성된 테스트입니다.
후기
Speech-to-Text 모델을 편하게 써볼 수 있어서 졸업 프로젝트에 잘 활용했습니다. 비교해 본 결과, 녹음된 음성을 다시 녹음하여 인식시키는 것보다 직접 말하는 것을 녹음하여 인식시키는 것이 인식률이 더 좋았습니다.
하지만 문제점도 있었습니다. 아래 사진을 보시면, 3초의 짧은 파일에 대한 스크립트를 얻기 까지 2초 정도의 시간이 걸렸고 이것이 실시간 자막 기능에서 응답 지연으로 이어지는 문제가 발생하였습니다.
또한, 한국어와 영어가 섞인 문장의 경우 영어를 제외하고 인식하는 문제가 있었습니다. 한국어/영어 혼합 오디오 데이터를 구하게 되면 모델 조정을 추가로 시도해 보려고 합니다.
참고
[Google Cloud Docs] Speech-to-Text 설정
[Google Cloud Docs] 빠른 시작: 클라이언트 라이브러리를 사용하여 음성을 텍스트로 변환하기
[Google Cloud Docs] 서비스 계정 개요
[번역] PATH (MacOS) : Mac OS에서 PATH 환경 변수 모범 사례
iterm .zshrc 환경변수 설정