이번에는 유니티에서 구글 STT(Speech To Text) 사용하는 법에 대해서 알아보도록 하겠습니다.
구글 클라우드 라이브러리 설치 및 API 키 발급 / 유니티 XR(메타 퀘스트 2)에서 STT구현 두 가지의 과정으로 설명하도록 하겠습니다.
구글 클라우드 라이브러리 설치 및 API 키 발급
우선 Google Cloud 서비스에 대해서 로그인이 되어 있어야 합니다! 이 작업에 대해선 다른 글에서도 많이 소개되어 있어서 로그인하는 작업은 건너뛰겠습니다.
로그인 하시면 바로 Speech to text가 나오는 것을 알 수 있습니다.
API 및 서비스 창에서 사용자 인증 정보를 누른 후 사용자 인증 정보 만들기를 누르시면 API키를 발급할 수 있는 것을 알 수 있습니다.
API 키를 누르시면 이런 창을 볼 수 있는데 이렇게 API키마다 제한사항을 두는 것이 좋습니다.
그리고 옆에 추가정보 창이 나올텐데 이건 나중에 쓰일 API Key 정보 이므로 정보를 노출시키면 안 됩니다!
유니티 XR(메타 퀘스트 2)상에서 STT구현
구현하기에 앞서 전체적인 코드 설명은 다음과 같습니다.
메타 퀘스트 2에 있는 마이크를 인식 / 마이크를 이용한 음성 녹음(Audioclip) 생성 -> 음성 녹음(Audioclip) WAV파일로 변경 -> 음성 녹음 파일 구글 STT사이트로 전송 -> 결과 파일 JSON으로 가져옴 -> SimpleJSON를 이용한 transscript 추출
직접 해본 결과 마이크에서 인식하고 음성 녹음을 파일을 생성할 때 구글 STT는 WAV파일에서도 지정된 형식만을 지원하므로 그 형식을 꼭 지켜줘야 한다는 것을 알게 되었습니다. WAV파일로 녹음하고 보내야 결과 값을 받을 수 있습니다.
메타 퀘스트에서 마이크를 사용하기 위해서는 몇 가지 과정이 필요합니다.
이 파일에 들어가서 마이크 선언을 위해서 코드를 추가해줍시다.
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<!-- 마이크 권한 추가 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
위에서 바로 주석 처리되어 있는 마이크 권한 추가하는 코드만 넣어주시면 됩니다.
메타 퀘스트 2에 있는 마이크를 인식
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MicrophoneInput : MonoBehaviour
{
// Right Controller의 트랜스폼 (XR Origin의 Right Controller 참조)
public Transform ControllerTransform; // 오른쪽 컨트롤러의 트랜스폼 참조
private AudioClip record; // 녹음된 오디오 클립
private AudioSource aud; // 오디오 소스 컴포넌트
public int recordingDuration = 3; // 녹음 시간 (초)
public SpeechToText speechToText; // SpeechToText 클래스의 인스턴스
void Start()
{
// AudioSource 컴포넌트 가져오기
aud = GetComponent<AudioSource>();
if (aud == null)
{
Debug.LogError("AudioSource 컴포넌트가 필요합니다."); // AudioSource가 없을 경우 오류 로그 출력
}
// SpeechToText 인스턴스를 찾기
speechToText = FindObjectOfType<SpeechToText>();
if (speechToText == null)
{
Debug.LogError("SpeechToText 인스턴스가 없습니다. 씬에 추가했는지 확인하세요."); // SpeechToText 인스턴스가 없을 경우 오류 로그 출력
}
}
// WAV 파일 생성 메소드
public byte[] CreateWavFile(byte[] audioData)
{
byte[] header = CreateWavHeader(44100, 1, audioData.Length / 2); // WAV 헤더 생성
byte[] wavFile = new byte[header.Length + audioData.Length]; // 헤더와 오디오 데이터 결합
System.Buffer.BlockCopy(header, 0, wavFile, 0, header.Length); // 헤더 복사
System.Buffer.BlockCopy(audioData, 0, wavFile, header.Length, audioData.Length); // 오디오 데이터 복사
return wavFile; // 생성된 WAV 파일 반환
}
// WAV 헤더 생성 메소드
private byte[] CreateWavHeader(int sampleRate, int channels, int samplesCount)
{
int byteRate = sampleRate * channels * 2; // 바이트 레이트 계산 (16 bits = 2 bytes)
int blockAlign = channels * 2; // 블록 정렬 계산
int subChunk2Size = samplesCount * blockAlign; // 데이터 크기 계산
int chunkSize = 36 + subChunk2Size; // 청크 크기 계산
byte[] header = new byte[44]; // 44바이트의 WAV 헤더
// WAV 헤더 필드 설정
System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes("RIFF"), 0, header, 0, 4);
System.Buffer.BlockCopy(System.BitConverter.GetBytes(chunkSize), 0, header, 4, 4);
System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes("WAVE"), 0, header, 8, 4);
System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes("fmt "), 0, header, 12, 4);
System.Buffer.BlockCopy(System.BitConverter.GetBytes(16), 0, header, 16, 4); // Subchunk1Size (16 for PCM)
System.Buffer.BlockCopy(System.BitConverter.GetBytes((short)1), 0, header, 20, 2); // AudioFormat (PCM)
System.Buffer.BlockCopy(System.BitConverter.GetBytes((short)channels), 0, header, 22, 2); // NumChannels
System.Buffer.BlockCopy(System.BitConverter.GetBytes(sampleRate), 0, header, 24, 4); // SampleRate
System.Buffer.BlockCopy(System.BitConverter.GetBytes(byteRate), 0, header, 28, 4); // ByteRate
System.Buffer.BlockCopy(System.BitConverter.GetBytes((short)blockAlign), 0, header, 32, 2); // BlockAlign
System.Buffer.BlockCopy(System.BitConverter.GetBytes((short)16), 0, header, 34, 2); // BitsPerSample
System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes("data"), 0, header, 36, 4);
System.Buffer.BlockCopy(System.BitConverter.GetBytes(subChunk2Size), 0, header, 40, 4); // Subchunk2Size
return header; // 생성된 헤더 반환
}
// 인터넷 연결 상태 확인 메소드
void CheckInternetConnection()
{
bool isConnected = Application.internetReachability != NetworkReachability.NotReachable;
if (isConnected)
{
Debug.Log("인터넷에 연결되어 있습니다."); // 인터넷 연결 시 로그 출력
}
else
{
Debug.LogWarning("인터넷에 연결되어 있지 않습니다."); // 인터넷 연결 없음 경고 로그 출력
}
}
void Update()
{
// 오른쪽 컨트롤러의 A 버튼을 클릭했을 때 녹음을 시작합니다.
if (OVRInput.GetDown(OVRInput.Button.One)) // A 버튼 클릭
{
CheckInternetConnection(); // 인터넷 연결 확인
RecSnd(); // 녹음 시작
Debug.Log("녹음 시작");
}
// B 버튼 클릭 시 녹음 종료 및 재생
if (OVRInput.GetDown(OVRInput.Button.Two)) // B 버튼 클릭
{
StopSnd(); // 녹음 종료
PlaySnd(); // 녹음된 클립 재생
Debug.Log("녹음 종료 및 재생");
}
}
// 녹음 시작 메소드
public void RecSnd()
{
// 마이크 장치가 있는지 확인
if (Microphone.devices.Length == 0)
{
Debug.LogError("마이크 장치가 없습니다."); // 마이크가 없을 경우 오류 로그 출력
return;
}
// 이미 녹음 중인지 확인
if (Microphone.IsRecording(Microphone.devices[0].ToString()))
{
Debug.LogWarning("이미 녹음 중입니다."); // 중복 녹음 경고
return;
}
// 녹음 시작 (지정된 시간, 44100Hz)
record = Microphone.Start(Microphone.devices[0].ToString(), false, recordingDuration, 44100);
aud.clip = record; // AudioSource에 클립 할당
Debug.Log("녹음 시작됨");
}
// 녹음 종료 메소드
public void StopSnd()
{
// 녹음 중인지 확인
if (!Microphone.IsRecording(Microphone.devices[0].ToString()))
{
Debug.Log("StopSnd에 진입을 하지 못하고 있습니다."); // 녹음 중이 아닐 때 경고 로그 출력
return; // 녹음 중이 아닐 때는 종료
}
Microphone.End(Microphone.devices[0].ToString()); // 녹음 종료
if (record == null)
{
Debug.LogError("녹음된 AudioClip이 없습니다."); // 녹음된 클립이 없을 경우 오류 로그 출력
return;
}
Debug.Log("녹음이 종료되었습니다. AudioClip이 생성되었습니다."); // 녹음 종료 로그 출력
// 녹음된 AudioClip을 확인
Debug.Log("녹음된 AudioClip: " + record.name);
// SpeechToText 인스턴스가 null인지 확인
if (speechToText != null)
{
// 녹음된 AudioClip을 byte[]로 변환하고 Google에 전송
byte[] audioData = AudioClipToWav.ClipToWav(record); // WAV 형식으로 변환
byte[] wavFileData = CreateWavFile(audioData); // WAV 파일 생성
StartCoroutine(speechToText.SendAudioToGoogle(wavFileData)); // SendAudioToGoogle 호출
}
else
{
Debug.LogError("SpeechToText 인스턴스가 null입니다. 할당을 확인하세요."); // SpeechToText 인스턴스가 없을 경우 오류 로그 출력
}
Debug.Log("녹음 종료");
}
// 녹음된 클립 재생 메소드
public void PlaySnd()
{
if (aud.clip != null)
{
aud.Play(); // 녹음된 클립 재생
Debug.Log("재생 중: " + aud.clip.name); // 재생 중인 클립 이름 로그 출력
}
else
{
Debug.LogError("녹음된 클립이 없습니다."); // 녹음된 클립이 없을 경우 오류 로그 출력
}
}
// 녹음된 오디오 클립 반환 메소드
public AudioClip GetAudioClip()
{
return record; // 녹음된 오디오 클립 반환
}
}
음성 녹음(Audioclip) WAV파일로 변경
using UnityEngine;
public class AudioClipToWav
{
public static byte[] ClipToWav(AudioClip clip)
{
if (clip == null)
{
Debug.LogError("AudioClip is null.");
return null;
}
int samples = clip.samples * clip.channels;
float[] data = new float[samples];
clip.GetData(data, 0);
// WAV 파일의 총 바이트 수 계산
byte[] byteArray = new byte[samples * 2 + 44]; // 2바이트는 16비트 PCM 포맷
System.Text.Encoding.ASCII.GetBytes("RIFF").CopyTo(byteArray, 0);
System.BitConverter.GetBytes(byteArray.Length - 8).CopyTo(byteArray, 4);
System.Text.Encoding.ASCII.GetBytes("WAVE").CopyTo(byteArray, 8);
System.Text.Encoding.ASCII.GetBytes("fmt ").CopyTo(byteArray, 12);
System.BitConverter.GetBytes(16).CopyTo(byteArray, 16); // fmt chunk의 크기
System.BitConverter.GetBytes((short)1).CopyTo(byteArray, 20); // PCM 포맷
System.BitConverter.GetBytes((short)clip.channels).CopyTo(byteArray, 22); // 채널 수
System.BitConverter.GetBytes(44100).CopyTo(byteArray, 24); // 샘플 레이트를 44100으로 설정
System.BitConverter.GetBytes(44100 * 2 * clip.channels).CopyTo(byteArray, 28); // 바이트 레이트
System.BitConverter.GetBytes((short)(2 * clip.channels)).CopyTo(byteArray, 32); // 블록 정렬
System.Text.Encoding.ASCII.GetBytes("data").CopyTo(byteArray, 36); // data 태그
System.BitConverter.GetBytes(samples * 2).CopyTo(byteArray, 40); // 데이터 크기
// PCM 데이터를 16비트 형식으로 변환하여 배열에 추가
for (int i = 0; i < data.Length; i++)
{
short temp = (short)(data[i] * short.MaxValue);
byteArray[44 + i * 2] = (byte)(temp & 0xff);
byteArray[44 + i * 2 + 1] = (byte)((temp >> 8) & 0xff);
}
return byteArray;
}
}
음성 녹음 파일 구글 STT사이트로 전송 / 결과 파일 JSON으로 가져옴 / SimpleJSON를 이용한 transscript 추출
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using SimpleJSON; // SimpleJSON 라이브러리 추가
public class SpeechToText : MonoBehaviour
{
public string apiKey;
public IEnumerator SendAudioToGoogle(byte[] audioData)
{
if (audioData == null || audioData.Length == 0)
{
Debug.LogError("Audio data is null or empty.");
yield break;
}
string url = $"https://speech.googleapis.com/v1/speech:recognize?key={apiKey}";
string audioBase64 = System.Convert.ToBase64String(audioData);
if (string.IsNullOrEmpty(audioBase64))
{
Debug.LogError("Audio Base64 is empty.");
yield break;
}
var requestData = new SpeechRecognitionRequest
{
config = new RecognitionConfig
{
encoding = "LINEAR16",
sampleRateHertz = 44100,
languageCode = "en-US"
},
audio = new RecognitionAudio
{
content = audioBase64
}
};
string jsonData = JsonUtility.ToJson(requestData);
using (UnityWebRequest request = UnityWebRequest.PostWwwForm(url, jsonData))
{
request.method = "POST";
request.SetRequestHeader("Content-Type", "application/json");
request.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(jsonData));
request.downloadHandler = new DownloadHandlerBuffer();
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.LogError("Error: " + request.error);
Debug.LogError("Response: " + request.downloadHandler.text);
}
else
{
// JSON 응답에서 transcript 추출
string responseText = request.downloadHandler.text;
var jsonResponse = JSON.Parse(responseText); // SimpleJSON으로 파싱
string transcript = jsonResponse["results"][0]["alternatives"][0]["transcript"]; // transcript 추출
Debug.Log("Transcript: " + transcript); // 추출된 transcript 출력
}
}
}
[System.Serializable]
public class SpeechRecognitionRequest
{
public RecognitionConfig config;
public RecognitionAudio audio;
}
[System.Serializable]
public class RecognitionConfig
{
public string encoding;
public int sampleRateHertz;
public string languageCode;
}
[System.Serializable]
public class RecognitionAudio
{
public string content;
}
}
위에서 받았던 API키를 여기에서 넣으시면 됩니다.
SimpleJSON 설치하기
SimpleJSON을 사용하려면 SimpleJSON GitHub에서 SimpleJSON.cs 파일을 Unity 프로젝트에 추가해 주시면 됩니다!!
저는 Asset -> Scripts 폴더를 생성한 후 거기에 분류해서 뒀습니다!
이렇게 결과가 잘 나오는 것을 알 수 있습니다.
'소프트웨어 개발 > Unity' 카테고리의 다른 글
[Unity] 오브젝트에 믹사모(Mixamo) 애니메이션 적용하기 (2) | 2024.11.20 |
---|---|
[Unity] 유니티에서 MySQL 연동하고 조회하기 (5) | 2024.11.15 |
[Unity] Unity Scenes 조명 바꾸는 법 (0) | 2024.11.15 |
[Unity] Meta Quest 2 이용한 씬 이동 (1) | 2024.10.14 |
[Unity] Meta Quest 2 VR 프로젝트 구축-2 (2) | 2024.09.25 |