C# - Whisper.NET Library를 이용해 음성을 텍스트로 변환 및 번역하는 예제
고맙게도, OpenAI에서 Whisper 모델을 공개했는데요,
openai/whisper-large-v3
; https://huggingface.co/openai/whisper-large-v3
더욱 고맙게도, ^^ .NET 환경을 위한 라이브러리가 nuget에 공개돼 있어 이걸 사용하면 쉽게 음성을 텍스트로 변환할 수 있습니다.
Whisper.net
; https://www.nuget.org/packages/Whisper.net/
github에 보면, 대략적인 사용법도 알 수 있는데요,
sandrohanea/whisper.net
; https://github.com/sandrohanea/whisper.net
이걸로 간단하게 실습해 보겠습니다. (사실상 베끼는 것에 불과합니다. ^^)
우선 nuget으로부터 아래의 패키지를 설치하는 것으로 시작할 수 있습니다.
Install-Package Whisper.net.AllRuntimes
이름에서 알 수 있듯이 다양한 런타임을 한 번에 설치하는데요, AI 모델을 로컬에서 실행하기 때문에 특정 런타임을 제약할 수 있다면 개별로 선택해서 설치하는 것도 가능합니다.
// CPU만 사용하는 경우,
Install-Package Whisper.net
Install-Package Whisper.net.Runtime
// Cuda GPU를 사용하는 경우,
Install-Package Whisper.net
Install-Package Whisper.net.Runtime.Cuda.Windows
대충 감이 오시죠? ^^ 일단 위의 패키지 설치로 Whisper 라이브러리와 런타임은 설치하지만 정작 Model 파일은 별도로 다운로드해야 합니다. 재미있는 건, 이러한 모델 파일을 다운로드하는 기능도 라이브러리에서 제공한다는 점인데, 대충 아래와 같이 코딩할 수 있습니다.
using Whisper.net.Ggml;
namespace ConsoleApp1;
// Install-Package Whisper.net.AllRuntimes
internal class Program
{
static async Task Main(string[] args)
{
var modelName = "ggml-base.bin";
if (!File.Exists(modelName))
{
using var modelStream = await WhisperGgmlDownloader.Default.GetGgmlModelAsync(GgmlType.Base);
using var fileWriter = File.OpenWrite(modelName);
await modelStream.CopyToAsync(fileWriter);
}
}
}
실행하면, EXE 파일이 있는 디렉터리에 ggml-base.bin 파일이 다운로드되고, WhisperFactory 타입을 이용해 해당 모델을 로드할 수 있습니다.
using var whisperFactory = WhisperFactory.FromPath(modelName);
using var processor = whisperFactory.CreateBuilder()
.WithLanguage("auto")
.Build();
자, 그럼 이제 음성을 텍스트로 변환하는 작업이 남았는데요, Whisper.NET 라이브러리는 wave 포맷을 지원하므로 다음과 같이 ProcessAsync 메서드에 전달하는 것으로 마무리할 수 있습니다.
string waveFileName = "audio.wav";
using var fileStream = File.OpenRead(wavFileName);
await foreach (var result in processor.ProcessAsync(fileStream))
{
Console.WriteLine($"{result.Start}->{result.End}: {result.Text}");
}
실제로 결과의 품질까지 확인해 볼까요? ^^ 이에 대한 실습을 위해 한글 음성이 포함된 audio 파일을 구해야 하는데... Kaggle을 이용해 보는 것도 좋을 듯합니다.
Korean Single Speaker Speech Dataset
; https://www.kaggle.com/datasets/bryanpark/korean-single-speaker-speech-dataset
3GB 정도의 zip 파일을 해제하면 4GB 정도의 wav 파일들과 각각의 wav 음성에 대한 텍스트가 대응되는 정보를 가진 transcript.v.1.4.txt 파일이 나옵니다.
이것을 참조하면, "".\kss\1\1_0000.wav"" 경로의 파일은 "그는 괜찮은 척하려고 애쓰는 것 같았다."라는 텍스트에 해당하는 것을 알 수 있는데요, 이걸 whisper.net의 입력으로 쓰면 다음과 같은 결과가 나옵니다.
00:00:00->00:00:03.4800000: 그는 괜찮은 척하려고 S는 것 같았다.
"애쓰"라는 단어를 영문자 "S"로 인식했는데, 비록 원하는 결과와는 다르지만 어쨌든 음성 자체로는 비슷한 발음이라 나름대로 그럴싸한 결과가 나온 것 같습니다. ^^
혹시 명시적으로 언어를 지정하면 더 나은 결과가 나올까요?
using var processor = whisperFactory.CreateBuilder()
.WithLanguage("ko")
.Build();
아쉽게도 결과는 동일했습니다. 즉 "auto"로 지정한 경우 결국 "ko"로 인식한 것과 같다는 의미입니다. 참고로 이것을 "en"으로 지정하면,
00:00:00->00:00:03.5200000: I think it's okay to use it.
이번에는 아예 번역까지 해버립니다. ^^; ASR 기능에 더해 번역 기능까지 있다니 놀라울 따름입니다.
"애쓰"를 "S"로 인식한 것은 locale 문제는 아닌 것 같으니, 그렇다면 모델의 크기를 키우면 좀 더 나아지지 않을까요? ^^
Whisper.NET이 다운로드한 모델 파일(위의 코드의 경우 "ggml-base.bin")은 140MB 정도에 불과한 매우 작은 크기의 모델입니다. 실제로 해당 모델은 Hugging Face의 whisper.cpp repo에서,
ggerganov/whisper.cpp
; https://huggingface.co/ggerganov/whisper.cpp/tree/main
ggml-base.bin 파일을 다운로드한 것인데요, 따라서 그것보다 좀 더 큰 모델, 가령 "ggml-large-v1.bin" 파일을 직접 다운로드하거나, 코드에서 그 모델을 다운로드하도록 바꾸면,
var modelName = "ggml-large-v1.bin";
if (!File.Exists(modelName))
{
using var modelStream = await WhisperGgmlDownloader.Default.GetGgmlModelAsync(GgmlType.LargeV1);
using var fileWriter = File.OpenWrite(modelName);
await modelStream.CopyToAsync(fileWriter);
}
음성 인식뿐만 아니라 번역까지도 더 정확해집니다.
// "ko"인 경우,
00:00:00->00:00:03.5000000: 그는 괜찮은 척 하려고 애쓰는 것 같았다.
// "en"인 경우,
00:00:00->00:00:03.5000000: He seemed to be trying to act okay.
오호~~~ 이제야 좀 쓸만하군요. ^^ (물론, 그만큼의 GPU 메모리와 연산 시간이 필요하다는 것은 감안해야 합니다.)
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
본문의 테스트를 "
Korean Single Speaker Speech Dataset"의 wav 파일들을 대상으로 직접 테스트하면 이런 오류가 발생합니다.
Unhandled exception. Whisper.net.Wave.NotSupportedWaveException: Only 16KHz sample rate is supported.
at Whisper.net.Wave.WaveParser.InternalInitialize(Boolean useAsync, CancellationToken cancellationToken)
at Whisper.net.Wave.WaveParser.GetAvgSamplesAsync(CancellationToken cancellationToken)
at Whisper.net.WhisperProcessor.ProcessAsync(Stream waveStream, CancellationToken cancellationToken)+MoveNext()
at Whisper.net.WhisperProcessor.ProcessAsync(Stream waveStream, CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
...[생략]...
즉, 16KHz 샘플링 레이트만 지원한다는 건데요, 어쩔 수 없습니다. ^^ ffmpeg 등을 이용해 다음과 같이 (원본은 44KHz이므로) 16KHz로 변환해 두어야 합니다.
ffmpeg -i .\kss\1\1_0000.wav -ar 16000 1_0000_16khz.wav
혹은, NAudio 등의 라이브러리를 이용해 wav 파일의 포맷을 16KHz로 변환하면 될 텐데요, 이에 대해서는 whisper.net의 예제에도 잘 나와 있으니 참고하시고!
NAudio integration for resampled wav (whisper.net/examples/NAudioResampleWav/Program.cs)
; https://github.com/sandrohanea/whisper.net/blob/main/examples/NAudioResampleWav/Program.cs
덧붙이면 wav 포맷뿐만 아니라 mp3 등의 포맷도 결국 원시 스트림만 얻을 수 있다면 whisper.net에서 사용할 수 있습니다. 그리고 그에 대한 예제도 제공하므로 ^^ 쉽게 코딩할 수 있을 것입니다.
NAudio integration for mp3 (whisper.net/examples/NAudioMp3/Program.cs)
; https://github.com/sandrohanea/whisper.net/blob/main/examples/NAudioMp3/Program.cs
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]