멈추지 않고 끈질기게

[Unreal] 최종 프로젝트 1일차 - STT 구현 본문

포트폴리오

[Unreal] 최종 프로젝트 1일차 - STT 구현

sam0308 2024. 5. 11. 13:37

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.

※ 해당 포스팅은 청년취업사관학교 교육 과정의 최종 프로젝트에 관한 내용을 포함하고 있습니다.

 

※ 해당 포스팅은 Unreal 5.4.1 버전을 기준으로 작성되었습니다.

 

 

 

0. 서론

 최종 프로젝트에서 STT(Speech to Text)를 사용하기로 하여 우선적으로 구현하기로 했습니다. 언리얼에서 STT를 사용하는 방법을 찾아보았더니 대부분 플러그인을 사용하는 방식이라, 일단 플러그인 없이 구현할 수 없는지 찾아보았습니다.

 

 찾아보니 Whisper라는 오픈 AI사에서 제공하는 STT 모듈이 있었습니다. 테스트해보니 한국어도 인식하며, 정확도도 높은 편이라 만족스러웠습니다. 다만 파이썬에서 실행해야 하고, 저장된 음성 파일이 필요했습니다. 그래서 다음과 같은 방식으로 언리얼에서 STT를 구현할 수 있지 않을까 하고 계획해보았습니다.

 

  1. 우선 언리얼에서 마이크 입력을 받아 음성 파일로 저장한다.
  2. 저장된 음성 파일의 경로를 웹통신으로 전달하여 파이썬에서 Whisper를 통해 텍스트를 뽑아낸다.
  3. 결과물(텍스트)을 다시 웹통신을 통해 언리얼에서 받는다.

 

1. 마이크로 음성 저장

 이 부분은 다행히 쉬운 예시가 있어서 금방 구현할 수 있었습니다. 

https://www.youtube.com/watch?v=uxCsaF1faaA

영상 1. 언리얼 음성 녹음

 

 다만 한가지 아쉬운 점이라면, 해당 내용을 C++에서 작성하려고 했더니 빌드 에러가 나서 결국 블루프린트로 했다는 점입니다. AuidoCapture 모듈을 Build.cs 파일에 추가했는데도 계속 '확인할 수 없는 외부 참조입니다' 이슈가 발목을 잡아 결국 영상 내용 그대로 블루프린트로 구현했습니다. 해당 내용에 대해서는 나중에 좀 더 찾아봐야 할 듯 싶습니다.

 

 

 

2. Whisper를 통한 텍스트 검출

 이것도 찾아보니 사용법은 어렵지 않게 찾을 수 있었으나, 제가 한가지를 건너 뛰는 바람에 다소 시간 낭비가 있었습니다.

https://tech.osci.kr/openai-whisper%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EC%9D%8C%EC%84%B1-%EC%9D%B8%EC%8B%9D/

 

OpenAI Whisper와 함께하는 음성 인식 - 오픈소스컨설팅 테크블로그

여러분들은 업무를 하면서 ChatGPT를 사용하시나요? ChatGPT는 OpenAI에서 GPT를 기반으로 개발된 대화형 인공지능 모델로, 업무를 하며 사용하다 보면 AI가 인간에게 도움을 줄 수 있다는 것을 체감하

tech.osci.kr

 

분명히 파일 경로를 제대로 입력했는데도 인식을 못해서 다시 찾아보니 ffmpeg라는 모듈을 설치를 안해서 발생하는 이슈였습니다. 가이드를 따라할 때는 하나라도 놓치지 않고 제대로 따라해야겠습니다.

 

 그리고 사용 방법이 터미널에서 명령어를 입력하는 방식이라, 전달받은 파일 경로를 이용하기 위해 파이썬 코드에서 터미널 명령어를 실행하는 방법을 찾았습니다. os.system을 통해 터미널 명령어를 파이썬에서 정의하는 방법이 있어서 해결하였습니다.

import os, sys

def stt(name:str, path:str):
    terminal_command = f"whisper {path} --language Korean --model small --output_format txt" 
    os.system(terminal_command)

 

 

3. 웹통신

 언리얼과 연동하기 위해 웹통신을 구현할 필요가 있었습니다. 다행히 언리얼에서 Http 통신하는 방법을 배운 적이 있고, 파이썬에서는 fastapi를 통해 손쉽게 웹 서버를 열 수 있어서 생각보다는 어렵지 않았습니다.

 

 다음은 파이썬 쪽 웹서버 코드입니다. 1.언리얼에서 파일명과 경로를 전달하면 해당 음성 파일을 읽어들인 뒤, 2.whisper를 이용한 검출 결과를 파일명.txt로 저장합니다. 3.해당 파일을 읽어들여 텍스트로 저장하고 4.요청이 오면 반환하는 방식입니다.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import os, sys

app = FastAPI()

## 요청 양식 설정
## BaseModel 상속받은 클래스 생성
class STTData(BaseModel):
    file_name : str
    file_path : str

latest_speech = ""

def stt(name:str, path:str):
    terminal_command = f"whisper {path} --language Korean --model small --output_format txt" 
    os.system(terminal_command)

    save_path = f'{name}.txt'
    with open(save_path, 'r', encoding='UTF-8') as file:
        text = file.read()
        return text

@app.post("/post-speech")
async def post_speech(data:STTData):
    try:
        global latest_speech
        latest_speech = stt(data.file_name, data.file_path)
        return latest_speech
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"잘못된 경로 정보입니다.: {str(e)}")

@app.get("/get-speech")
async def get_text():
    global latest_speech
    return latest_speech

 

 다만 파일을 읽어오는 부분에서 인코딩을 지정해주지 않는 바람에 통신이 실패하여, 이를 잡는 데 애를 좀 먹었습니다. 앞으로는 파일 읽어오는 부분에서는 인코딩을 주의해야겠습니다.

 

 다음은 언리얼 쪽 웹통신 코드입니다. 대부분 예전에 사용한 코드를 복사한 것이고, 요청 양식만 주의해서 수정하였습니다. 저장한 음성 파일 이름과 경로를 전달하고, 전달이 끝나면 바로 결과를 요청하여 받아온 뒤 로그로 출력하는 식으로 구성하였습니다.

void AHttpActor::SendSpeech(const FString& SpeechFileName, const FString& SpeechFilePath)
{
	const FString& FullURL = BaseURL + EndPoint_SendSpeech;

	// HTTP Request
	TSharedRef<IHttpRequest> HttpRequest = FHttpModule::Get().CreateRequest();
	HttpRequest->SetVerb(TEXT("POST"));
	HttpRequest->SetURL(FullURL);
	HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
	HttpRequest->OnProcessRequestComplete().BindUObject(this, &AHttpActor::SendSpeechComplete);

	// 양식 주의할 것(웹 서버쪽의 양식과 정확하게 일치해야 함)
	FString JsonBody = FString::Printf(TEXT("{\"file_name\": \"%s\",\"file_path\" : \"%s\"}"), *SpeechFileName, *SpeechFilePath);
	HttpRequest->SetContentAsString(JsonBody);

	HttpRequest->ProcessRequest();

	UE_LOG(LogTemp, Warning, TEXT("Send speech to %s"), *FullURL);
}

void AHttpActor::SendSpeechComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully)
{
	if (bConnectedSuccessfully)
	{
		UE_LOG(LogTemp, Warning, TEXT("Send speech complete"));
		ReqTextFromSpeech();
	}
	else
	{
		if (Request->GetStatus() == EHttpRequestStatus::Succeeded)
		{
			UE_LOG(LogTemp, Warning, TEXT("Response Failed...%d"), Response->GetResponseCode());
		}
	}
}

void AHttpActor::ReqTextFromSpeech()
{
	const FString& FullURL = BaseURL + EndPoint_GetText;

	// HTTP Request
	TSharedRef<IHttpRequest> HttpRequest = FHttpModule::Get().CreateRequest();
	HttpRequest->SetVerb(TEXT("GET"));
	HttpRequest->SetURL(FullURL);
	HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
	HttpRequest->OnProcessRequestComplete().BindUObject(this, &AHttpActor::ReqTextFromSpeechComplete);

	HttpRequest->ProcessRequest();
	UE_LOG(LogTemp, Warning, TEXT("Req to %s"), *FullURL);
}

void AHttpActor::ReqTextFromSpeechComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully)
{
	if (bConnectedSuccessfully)
	{
		const FString& ResultText = Response->GetContentAsString();
		UE_LOG(LogTemp, Warning, TEXT("ReqTest Complete : %s"), *ResultText);
	}
	else
	{
		if (Request->GetStatus() == EHttpRequestStatus::Succeeded)
		{
			UE_LOG(LogTemp, Warning, TEXT("Response Failed...%d"), Response->GetResponseCode());
		}
	}
}

 

 

다음은 언리얼에서 직접 테스트 한 결과입니다(원문: 안녕하세요, stt 테스트 중입니다). stt -> spt로 알파벳 하나 틀린 것을 제외하고는 꽤 괜찮은 인식율을 보여주고 있습니다. 통신 시간을 고려하여 whisper에서 small 모델을 사용한 것 치고는 만족스러운 결과였습니다.

사진 1. STT 테스트 결과