人工智能历经多年演进,昔日高门槛的图像与语音识别任务,如今已有成熟的开源框架可供免费使用,只要花点时间,就可以零成本部署。本文以语音识别为例,看如何高效的将语音识别功能集成至C#系统中,后续大家可以继续完善扩展,去处理如语音指令、语音交互、字幕生成、会议纪要分析、语音翻译等相关任务。

本文项目在笔记本电脑上用cpu就可以自己动手轻松实现,所有代码均已开源,仅需关注 萤火初芒 公众号回复AISharp即可查看仓库地址,供学习交流使用,无套路。
一、环境配置基础
语音识别的方案有很多,windows系统本身也自带有语音识别的方案(System.Speech.Recognition),但是效果查强人意。既要简单好用,又要功能强大效果好,我们选择基于Whisper(MIT,https://github.com/openai/whisper)的Whisper.Net(MIT,https://github.com/sandrohanea/whisper.net)来实现。

Whisper 是2022年OpenAI发布的一个通用语音识别模型。它基于大量多样化的音频数据进行训练,同时也是一个多任务模型,能够执行多语言语音识别、语音翻译和语言识别任务。项目创建完成后直接在nuget拉取Whisper.net(1.9.0)和Whisper.net.Runtime(1.9.0)即可。
另外为了实现语音交互,还要在nuget拉取NAudio(2.2.1)(MIT,https://github.com/naudio/NAudio),以实现通过麦克风设备对声音的捕获。
二、核心代码实现
2.1 四行核心代码实现语音转文本
上面的控制台demo的main函数代码如下:
private static readonly string OutFile = "d:\record.wav"; //临时输出文件 // 读取json配置文件,nuget拉取LumConfig,往期文章有介绍 private static LumConfigManager con = new LumConfigManager("model.conf"); private static void Main() { // 1. 加载模型 // 输入模型地址,model文件夹下面提供了一个tiny版模型可以直接用 var wm = new WhisperManager((string)con.Get("modelPath")); while (true) { bool started = false; Console.WriteLine("按 Space 开始/停止录音"); while (true) { var key = Console.ReadKey(true).Key; if (key == ConsoleKey.Spacebar) { if (started) { // 2. 开始录音 StopRec(); break; } else { // 3. 结束录音 StartRec(); } started=!started; } } // 4. 转文本 var res = wm.RunAsync(OutFile).GetAwaiter().GetResult(); Console.WriteLine(res); } }
代码通过循环控制来实现反复读取语音再转换为文本的动作。其实核心代码只有四行,分别是加载模型、开始录音、停止录音、语音转文本。后面我们将对其一一拆解。
2.2 麦克风录制声音
麦克风的录制很简单,我们只需要完成2个动作,一个是开始录制声音,另一个是停止录制声音。WaveFileWriter方法两个重载,可以选择把声音录制到文件,也可以录制到流(stream)中。以录制到文件为例具体代码如下:
private static void StartRec() { waveIn = new WaveInEvent { DeviceNumber = 0, // 默认麦克风 WaveFormat = new WaveFormat(16000, 1), // 44.1kHz 单声道 BufferMilliseconds = 100 }; writer = new WaveFileWriter(OutFile, waveIn.WaveFormat); // 数据到达事件 waveIn.DataAvailable += (s, e) => { writer.Write(e.Buffer, 0, e.BytesRecorded); }; // 录音停止事件(负责释放 writer) waveIn.RecordingStopped += (s, e) => { writer?.Dispose(); writer = null; waveIn?.Dispose(); waveIn = null; stop.Set(); }; waveIn.StartRecording(); Console.WriteLine(">>> 正在录音……"); } private static void StopRec() { Console.WriteLine("<<< 停止录音"); waveIn?.StopRecording(); // 触发 RecordingStopped 事件 // 停止录制时,流数据的处理不一定已经完成 // 我们额外使用一个信号量来实现简单的同步 stop.WaitOne(); }
2.3 语音转文字的实现
尽管Whisper.Net已经封装好了所有相关功能,但是我们最好额外写一个WhisperManager类来高效的反复使用他。具体代码如下:
internal class WhisperManager:IDisposable { WhisperFactory whisperFactory; WhisperProcessor processor; public WhisperManager(string path) { whisperFactory = WhisperFactory.FromPath(path); processor = whisperFactory.CreateBuilder() .WithLanguage("zh") .WithPrompt("以下是,简体中文普通话的句子。") // 否则大概率会输出繁体中文 .WithThreads(16) .Build(); } public async Task<string> RunAsync(string path) { try { var sb = new StringBuilder(); using var fileStream = File.OpenRead(path); // 异步获取识别后的分段结果 await foreach (var seg in processor.ProcessAsync(fileStream)) { var str = $"{seg.Start}->{seg.End}: {seg.Text}rn"; sb.AppendLine(str); var per = (fileStream.Position / fileStream.Length*100).ToString("N2"); } return sb.ToString(); } catch (Exception ex) { return ex.Message; } } public void Dispose() { processor.Dispose(); whisperFactory.Dispose(); } }
Whisper重要基础参数配置:
WithLanguage,指定输入语音的语言,如zh、en、ja、auto(自动);
WithPrompt,输出提示词,这里与大语言模型不同,只能说引导模型生成,比如“以下是一个访谈。”。此处需要与指定语言一致,否则可能会强行切换,50~200 个字符足够,最好出现"标点符号"(不然可能会无标点)、"口语词、专有名词"(适配语境)。
另外还有很多与线程、输出相关的控制,大伙可根据需要自行研究了解。
2.4 模型、输入与输出
c#使用的Whisper模型是.bin的二进制格式的,现在可以在https://huggingface.co/ggerganov/whisper.cpp/tree/main下载。本项目里用到的是好久以前下载的ggml-model-whisper-tiny.bin,74mb,速度很快但效果中等。
本项目对Whisper的输入用的是wav格式的文件,且采样率必须为16kHz,否则会报错。如果导入音频时采样频率不对,可以用NAudio通过下代码进行转换:
public void Execute() { //using (Mp3FileReader reader = new Mp3FileReader("D:\Documents\Temp\WhisperDesktop\上午对接结构录音.mp3")) using (AudioFileReader reader = new AudioFileReader("D:\Documents\Temp\WhisperDesktop\录音.m4a")) { // 16kHz, 16bit,单声道 var newFormat = new WaveFormat(16000, 16, 1); using (var conversionStream = new WaveFormatConversionStream(newFormat, reader)) { WaveFileWriter.CreateWaveFile("D:\Documents\Temp\WhisperDesktop\录音.wav", conversionStream); } } }
Whisper输出是分多个段(IAsyncEnumerable
public class SegmentData { public string Text { get; } 文本 public TimeSpan Start { get; } public TimeSpan End { get; } public float MinProbability { get; } public float MaxProbability { get; } public float Probability { get; } public string Language { get; } public WhisperToken[] Tokens { get; } public float NoSpeechProbability { get; } }
打印的结果示意:
00:00:00->00:00:15: 一二三四五,上山打老虎,老虎没打到,打到了小松鼠。
三、最后
- 在使用tiny模型下,整体速度和质量还是基本可以的,如果追求更好的效果则考虑使用更大模型,但与之对内存的需求及运算时间会相应大幅增加。
- 在转换时,语音越长,转换速度越慢,因此建议在转换前,主动对长语音进行分段。尽管这样可能导致上下文丢失造成一定程度的不精确,但却能显著提高转换速度。
- 语音识别是支持多语言混合的。但翻译的功能,试了下,失灵时不灵,可能和模型本身及大小有关。
感谢您的阅读,本案例及更加完整丰富的机器学习模型案例的代码已全部开源,新朋友们可以关注公众号回复AISharp查看仓库地址,本期相关代码在仓库下面的Voic文件夹里,在model文件夹内可以找到案例使用的74mb大小的ggml-model-whisper-tiny.bin小模型,大家可以自行尝试。
