Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일

(시리즈 글이 2개 있습니다.)
.NET Framework: 300. C#으로 만드는 음성인식/TTS 프로그램
; https://www.sysnet.pe.kr/2/0/1228

닷넷: 2363. C# - Whisper.NET Library를 이용해 음성을 텍스트로 변환 및 번역하는 예제
; https://www.sysnet.pe.kr/2/0/14013




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




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 9/15/2025]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... [136]  137  138  139  140  141  142  143  144  145  146  147  148  149  150  ...
NoWriterDateCnt.TitleFile(s)
1746정성태9/18/201428412.NET Framework: 461. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기 [6]파일 다운로드1
1745정성태9/17/201426681개발 환경 구성: 237. 리눅스 Integration Services 버전 업그레이드 하는 방법 [1]
1744정성태9/17/201434934.NET Framework: 460. GetTickCount / GetTickCount64와 0x7FFE0000 주솟값 [4]파일 다운로드1
1743정성태9/16/201424343오류 유형: 238. 설치 오류 - Failed to get size of pseudo bundle
1742정성태8/27/201431063개발 환경 구성: 236. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 [2]
1741정성태8/26/201424770.NET Framework: 459. GetModuleHandleEx로 알아보는 .NET 메서드의 DLL 모듈 관계파일 다운로드1
1740정성태8/25/201436549.NET Framework: 458. 닷넷 GC가 순환 참조를 해제할 수 있을까요? [2]파일 다운로드1
1739정성태8/24/201430234.NET Framework: 457. 교착상태(Dead-lock) 해결 방법 - Lock Leveling [2]파일 다운로드1
1738정성태8/23/201426139.NET Framework: 456. C# - CAS를 이용한 Lock 래퍼 클래스파일 다운로드1
1737정성태8/20/201422676VS.NET IDE: 93. Visual Studio 2013 동기화 문제
1736정성태8/19/201429317VC++: 79. [부연] CAS Lock 알고리즘은 과연 빠른가? [2]파일 다운로드1
1735정성태8/19/201422096.NET Framework: 455. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 - 두 번째 이야기
1734정성태8/13/201423651오류 유형: 237. Windows Media Player cannot access the file. The file might be in use, you might not have access to the computer where the file is stored, or your proxy settings might not be correct.
1733정성태8/13/201429866.NET Framework: 454. EmptyWorkingSet Win32 API를 사용하는 C# 예제파일 다운로드1
1732정성태8/13/201438613Windows: 99. INetCache 폴더가 다르게 보이는 이유
1731정성태8/11/201430554개발 환경 구성: 235. 점(.)으로 시작하는 파일명을 탐색기에서 만드는 방법
1730정성태8/11/201425891개발 환경 구성: 234. Royal TS의 터미널(Terminal) 연결에서 한글이 깨지는 현상 해결 방법
1729정성태8/11/201421738오류 유형: 236. SqlConnection - The requested Performance Counter is not a custom counter, it has to be initialized as ReadOnly.
1728정성태8/8/201434503.NET Framework: 453. C# - 오피스 파워포인트(Powerpoint) 파일을 WinForm에서 보는 방법파일 다운로드1
1727정성태8/6/201424781오류 유형: 235. SignalR 오류 메시지 - Counter 'Messages Bus Messages Published Total' does not exist in the specified Category. [2]
1726정성태8/6/201423097오류 유형: 234. IIS Express에서 COM+ 사용 시 SecurityException - "Requested registry access is not allowed" 발생
1725정성태8/6/201425255오류 유형: 233. Visual Studio 2013 Update3 적용 후 Microsoft.VisualStudio.Web.PageInspector.Runtime 모듈에 대한 FileNotFoundException 예외 발생
1724정성태8/5/201429881.NET Framework: 452. .NET System.Threading.Thread 개체에서 Native Thread Id를 구하는 방법 - 두 번째 이야기 [1]파일 다운로드1
1723정성태7/29/201462584개발 환경 구성: 233. DirectX 9 예제 프로젝트 빌드하는 방법 [3]파일 다운로드1
1722정성태7/25/201424663오류 유형: 232. IIS 500 Internal Server Error - NTFS 암호화된 폴더에 웹 애플리케이션이 위치한 경우
1721정성태7/24/201428413.NET Framework: 451. 함수형 프로그래밍 개념 - 리스트 해석(List Comprehension)과 순수 함수 [2]
... [136]  137  138  139  140  141  142  143  144  145  146  147  148  149  150  ...