最近在做一个需要语音转文本功能的桌面应用时,我对比了市面上多个开源方案,最终选择了Whisper.net。这个决定不是随便做的,而是经过实际测试和性能对比后的结果。相比其他语音识别库,Whisper.net有几点特别吸引我的地方。
首先,它是基于OpenAI开源的Whisper模型,这个模型在语音识别领域的表现有目共睹。我测试过几个中文语音样本,准确率能达到90%以上,特别是对带口音的普通话识别效果比很多商业API还要好。而且它支持多种语言,只需要简单修改参数就能切换。
其次,Whisper.net专门为.NET开发者做了优化,用起来特别顺手。不像有些Python转过来的库需要折腾一大堆依赖项,它直接通过NuGet就能安装,引用起来非常方便。我在Windows 10和Windows 11上都测试过,基本没有兼容性问题。
最重要的是它能完全离线运行,这点对我的项目至关重要。有些项目需要处理敏感音频,不能上传到云端,Whisper.net完美解决了这个问题。模型文件下载到本地后,所有计算都在本机完成,数据安全性有保障。
在开始编码前,我们需要准备好开发环境。我推荐使用Visual Studio 2022,社区版就完全够用。创建一个新的Windows Forms应用项目,目标框架选.NET 6或.NET 7都可以,我个人更推荐.NET 7,因为它在性能上有一定优化。
通过NuGet包管理器安装Whisper.net非常简单。在解决方案资源管理器中右键点击项目,选择"管理NuGet程序包",搜索"Whisper.net"安装最新稳定版。我目前用的是1.0.3版本,运行非常稳定。
注意
如果你的项目需要支持x86平台,要特别注意模型文件大小。大模型在32位系统上可能会遇到内存不足的问题。
Whisper.net支持多种规模的模型,从tiny到large都有。模型越大识别精度越高,但消耗的资源也越多。经过多次测试,我发现对于中文语音识别,small模型已经能提供不错的效果,同时内存占用控制在1GB以内。
模型文件可以从Hugging Face仓库下载:
下载完成后,建议把模型文件放在项目目录下的Models文件夹中,并设置"如果较新则复制"属性,这样调试时能自动同步到输出目录。
核心功能的实现其实相当简单。首先我们需要在窗体加载时初始化Whisper工厂:
csharp复制private WhisperFactory _whisperFactory;
private WhisperProcessor _processor;
private void Form1_Load(object sender, EventArgs e)
{
// 加载模型文件
_whisperFactory = WhisperFactory.FromPath("Models/ggml-small.bin");
// 创建处理器,指定中文语言
_processor = _whisperFactory.CreateBuilder()
.WithLanguage("zh")
.Build();
}
这里有几个实用技巧:
添加一个按钮来处理用户选择的音频文件:
csharp复制private async void btnProcess_Click(object sender, EventArgs e)
{
if(string.IsNullOrEmpty(_currentAudioFile))
{
MessageBox.Show("请先选择音频文件");
return;
}
try
{
btnProcess.Enabled = false;
txtResult.Clear();
using var fileStream = File.OpenRead(_currentAudioFile);
await foreach (var result in _processor.ProcessAsync(fileStream))
{
txtResult.AppendText($"[{result.Start:hh\\:mm\\:ss}->{result.End:hh\\:mm\\:ss}] {result.Text}\r\n");
}
}
catch(Exception ex)
{
MessageBox.Show($"处理失败: {ex.Message}");
}
finally
{
btnProcess.Enabled = true;
}
}
这段代码有几个值得注意的点:
Whisper.net默认只支持WAV格式,但实际应用中用户可能有MP3等其他格式。我们可以用NAudio库先进行转码:
csharp复制private string ConvertToWav(string inputFile)
{
var outputFile = Path.Combine(
Path.GetTempPath(),
Path.GetFileNameWithoutExtension(inputFile) + ".wav");
using(var reader = new AudioFileReader(inputFile))
{
WaveFileWriter.CreateWaveFile(outputFile, reader);
}
return outputFile;
}
记得在NuGet中添加NAudio包,这个方法会把各种音频格式统一转为WAV,再交给Whisper处理。
处理大文件时,用户需要知道进度。我们可以改造处理逻辑:
csharp复制private async Task ProcessAudioWithProgress(IProgress<int> progress)
{
using var fileStream = File.OpenRead(_currentAudioFile);
long totalBytes = fileStream.Length;
long processedBytes = 0;
await foreach (var result in _processor.ProcessAsync(fileStream))
{
txtResult.AppendText($"{result.Text}\r\n");
processedBytes = fileStream.Position;
int percent = (int)((double)processedBytes / totalBytes * 100);
progress.Report(percent);
}
}
然后在界面添加一个ProgressBar控件,实时更新进度。
原始的时间戳格式可能不够友好,我们可以改进显示方式:
csharp复制private string FormatTimeSpan(TimeSpan time)
{
return $"{(int)time.TotalHours:D2}:{time.Minutes:D2}:{time.Seconds:D2}.{time.Milliseconds:D3}";
}
这样会显示成"00:01:23.456"这样的专业格式,更符合字幕文件的标准格式。
在实际开发过程中,我遇到过几个典型问题,这里分享下解决方法。
处理长音频时可能会遇到OutOfMemoryException。这时可以分段处理:
csharp复制var segmenter = new AudioSegmenter(TimeSpan.FromMinutes(5)); // 每5分钟一段
var segments = segmenter.SplitAudio(inputFile);
foreach(var segment in segments)
{
await ProcessSegment(segment);
File.Delete(segment); // 及时删除临时文件
}
AudioSegmenter可以用NAudio实现,关键是把大文件切成小段分别处理。
Whisper对中文标点的识别有时不太准确。我们可以后处理结果:
csharp复制private string FixChinesePunctuation(string text)
{
return text.Replace(".", "。")
.Replace(",", ",")
.Replace("?", "?")
.Replace("!", "!");
}
更复杂的可以引入正则表达式替换。
如果应用需要处理大量音频,可以考虑以下优化:
csharp复制// 预加载示例
_whisperFactory = WhisperFactory.FromPath("ggml-small.bin",
WhisperFactoryFlags.LoadAllToMemory);
这会增加启动时间和内存占用,但后续处理速度会快很多。
基础功能实现后,可以考虑添加一些实用扩展。
把识别结果导出为SRT字幕格式:
csharp复制private void ExportToSrt(string outputPath)
{
var lines = txtResult.Lines.Where(l => !string.IsNullOrEmpty(l));
using var writer = new StreamWriter(outputPath, false, Encoding.UTF8);
int index = 1;
foreach(var line in lines)
{
writer.WriteLine(index++);
writer.WriteLine(line.Substring(0, 29)); // 时间码部分
writer.WriteLine(line.Substring(32)); // 文本部分
writer.WriteLine();
}
}
通过NAudio捕获麦克风输入,实现实时转写:
csharp复制private WaveInEvent _waveIn;
private void StartRecording()
{
_waveIn = new WaveInEvent();
_waveIn.DataAvailable += OnAudioAvailable;
_waveIn.StartRecording();
}
private async void OnAudioAvailable(object sender, WaveInEventArgs e)
{
// 将音频数据存入内存流
// 每隔2秒发送给Whisper处理
}
结合翻译API,实现语音到文本再到目标语言的转换:
csharp复制private async Task<string> TranslateText(string text, string targetLang)
{
// 调用翻译服务API
// 返回翻译结果
}
可以在界面上添加目标语言选择框,实现一键翻译。