由于如鵬網(wǎng)工作需要要對大語音文件(長度超過5分鐘)進(jìn)行“語音轉(zhuǎn)文字”的語音識別知染,試了百度和科大訊飛的接口,對大語音文件的識別都支持的不好陆爽,本來想找開源的語音識別項(xiàng)目胶逢,發(fā)現(xiàn)都要自己去做數(shù)據(jù)集的訓(xùn)練,不僅麻煩俗扇,而且訓(xùn)練不夠的話就會識別準(zhǔn)確度太低硝烂。最后試了微軟的Azure認(rèn)知服務(wù)(CognitiveServices Speech),感覺非常好用铜幽。
1滞谢、首先要到azure去申請一個(gè)賬號,azure上提供了免費(fèi)的試用賬號除抛,具體怎么申請很簡單狮杨,我就不說了。
2到忽、新建一個(gè).Net項(xiàng)目
Azure認(rèn)知服務(wù)對于.Net橄教、Java、C++喘漏、Python护蝶、js等主流語言都有支持。我這里用.Net舉例子陷遮,其他語言用法點(diǎn)擊上圖中的【快速入門指南】
Nuget安裝SDK:Install-Package Microsoft.CognitiveServices.Speech
3滓走、首先是一個(gè)工具類Helper.cs
它的作用是把大wav音頻文件轉(zhuǎn)換為“音頻拉流”PullAudioInputStreamCallback
這個(gè)代碼是從Azure的GitHub官方例子中Copy出來的。
using Microsoft.CognitiveServices.Speech.Audio;
using System.Diagnostics;
using System.IO;
namespace Demo
{
? ? public class Helper
? ? {
? ? ? ? public static AudioConfig OpenWavFile(string filename)
? ? ? ? {
? ? ? ? ? ? BinaryReader reader = new BinaryReader(File.OpenRead(filename));
? ? ? ? ? ? return OpenWavFile(reader);
? ? ? ? }
? ? ? ? public static AudioConfig OpenWavFile(BinaryReader reader)
? ? ? ? {
? ? ? ? ? ? AudioStreamFormat format = readWaveHeader(reader);
? ? ? ? ? ? return AudioConfig.FromStreamInput(new BinaryAudioStreamReader(reader), format);
? ? ? ? }
? ? ? ? public static BinaryAudioStreamReader CreateWavReader(string filename)
? ? ? ? {
? ? ? ? ? ? BinaryReader reader = new BinaryReader(File.OpenRead(filename));
? ? ? ? ? ? // read the wave header so that it won't get into the in the following readings
? ? ? ? ? ? AudioStreamFormat format = readWaveHeader(reader);
? ? ? ? ? ? return new BinaryAudioStreamReader(reader);
? ? ? ? }
? ? ? ? public static AudioStreamFormat readWaveHeader(BinaryReader reader)
? ? ? ? {
? ? ? ? ? ? // Tag "RIFF"
? ? ? ? ? ? char[] data = new char[4];
? ? ? ? ? ? reader.Read(data, 0, 4);
? ? ? ? ? ? Trace.Assert((data[0] == 'R') && (data[1] == 'I') && (data[2] == 'F') && (data[3] == 'F'), "Wrong wav header");
? ? ? ? ? ? // Chunk size
? ? ? ? ? ? long fileSize = reader.ReadInt32();
? ? ? ? ? ? // Subchunk, Wave Header
? ? ? ? ? ? // Subchunk, Format
? ? ? ? ? ? // Tag: "WAVE"
? ? ? ? ? ? reader.Read(data, 0, 4);
? ? ? ? ? ? Trace.Assert((data[0] == 'W') && (data[1] == 'A') && (data[2] == 'V') && (data[3] == 'E'), "Wrong wav tag in wav header");
? ? ? ? ? ? // Tag: "fmt"
? ? ? ? ? ? reader.Read(data, 0, 4);
? ? ? ? ? ? Trace.Assert((data[0] == 'f') && (data[1] == 'm') && (data[2] == 't') && (data[3] == ' '), "Wrong format tag in wav header");
? ? ? ? ? ? // chunk format size
? ? ? ? ? ? var formatSize = reader.ReadInt32();
? ? ? ? ? ? var formatTag = reader.ReadUInt16();
? ? ? ? ? ? var channels = reader.ReadUInt16();
? ? ? ? ? ? var samplesPerSecond = reader.ReadUInt32();
? ? ? ? ? ? var avgBytesPerSec = reader.ReadUInt32();
? ? ? ? ? ? var blockAlign = reader.ReadUInt16();
? ? ? ? ? ? var bitsPerSample = reader.ReadUInt16();
? ? ? ? ? ? // Until now we have read 16 bytes in format, the rest is cbSize and is ignored for now.
? ? ? ? ? ? if (formatSize > 16)
? ? ? ? ? ? ? ? reader.ReadBytes((int)(formatSize - 16));
? ? ? ? ? ? // Second Chunk, data
? ? ? ? ? ? // tag: data.
? ? ? ? ? ? reader.Read(data, 0, 4);
? ? ? ? ? ? Trace.Assert((data[0] == 'd') && (data[1] == 'a') && (data[2] == 't') && (data[3] == 'a'), "Wrong data tag in wav");
? ? ? ? ? ? // data chunk size
? ? ? ? ? ? int dataSize = reader.ReadInt32();
? ? ? ? ? ? // now, we have the format in the format parameter and the
? ? ? ? ? ? // reader set to the start of the body, i.e., the raw sample data
? ? ? ? ? ? return AudioStreamFormat.GetWaveFormatPCM(samplesPerSecond, (byte)bitsPerSample, (byte)channels);
? ? ? ? }
? ? }
? ? /// <summary>
? ? /// Adapter class to the native stream api.
? ? /// </summary>
? ? public sealed class BinaryAudioStreamReader : PullAudioInputStreamCallback
? ? {
? ? ? ? private System.IO.BinaryReader _reader;
? ? ? ? /// <summary>
? ? ? ? /// Creates and initializes an instance of BinaryAudioStreamReader.
? ? ? ? /// </summary>
? ? ? ? /// <param name="reader">The underlying stream to read the audio data from. Note: The stream contains the bare sample data, not the container (like wave header data, etc).</param>
? ? ? ? public BinaryAudioStreamReader(System.IO.BinaryReader reader)
? ? ? ? {
? ? ? ? ? ? _reader = reader;
? ? ? ? }
? ? ? ? /// <summary>
? ? ? ? /// Creates and initializes an instance of BinaryAudioStreamReader.
? ? ? ? /// </summary>
? ? ? ? /// <param name="stream">The underlying stream to read the audio data from. Note: The stream contains the bare sample data, not the container (like wave header data, etc).</param>
? ? ? ? public BinaryAudioStreamReader(System.IO.Stream stream)
? ? ? ? ? ? : this(new System.IO.BinaryReader(stream))
? ? ? ? {
? ? ? ? }
? ? ? ? /// <summary>
? ? ? ? /// Reads binary data from the stream.
? ? ? ? /// </summary>
? ? ? ? /// <param name="dataBuffer">The buffer to fill</param>
? ? ? ? /// <param name="size">The size of data in the buffer.</param>
? ? ? ? /// <returns>The number of bytes filled, or 0 in case the stream hits its end and there is no more data available.
? ? ? ? /// If there is no data immediate available, Read() blocks until the next data becomes available.</returns>
? ? ? ? public override int Read(byte[] dataBuffer, uint size)
? ? ? ? {
? ? ? ? ? ? return _reader.Read(dataBuffer, 0, (int)size);
? ? ? ? }
? ? ? ? /// <summary>
? ? ? ? /// This method performs cleanup of resources.
? ? ? ? /// The Boolean parameter <paramref name="disposing"/> indicates whether the method is called from <see cref="IDisposable.Dispose"/> (if <paramref name="disposing"/> is true) or from the finalizer (if <paramref name="disposing"/> is false).
? ? ? ? /// Derived classes should override this method to dispose resource if needed.
? ? ? ? /// </summary>
? ? ? ? /// <param name="disposing">Flag to request disposal.</param>
? ? ? ? protected override void Dispose(bool disposing)
? ? ? ? {
? ? ? ? ? ? if (disposed)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? return;
? ? ? ? ? ? }
? ? ? ? ? ? if (disposing)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? _reader.Dispose();
? ? ? ? ? ? }
? ? ? ? ? ? disposed = true;
? ? ? ? ? ? base.Dispose(disposing);
? ? ? ? }
? ? ? ? private bool disposed = false;
? ? }
}
4帽馋、編寫主體識別代碼:
using Microsoft.CognitiveServices.Speech;
using Microsoft.CognitiveServices.Speech.Audio;
using System;
using System.Threading.Tasks;
namespace Demo
{
? ? class SpeechToTestMain
? ? {
? ? ? ? static void Main(string[] args)
? ? ? ? {
? ? ? ? ? ? T1().Wait();
? ? ? ? ? ? Console.WriteLine("ok");
? ? ? ? ? ? Console.ReadKey();
? ? ? ? }
? ? ? ? static async Task T1()
? ? ? ? {
? ? ? ? ? ? var file = @"E:\1.wav";
? ? ? ? ? ? var config = SpeechConfig.FromSubscription("這里填寫在第一步中拿到的密鑰", "westus");
? ? ? ? ? ? //通過設(shè)置config.SpeechRecognitionLanguage屬性設(shè)定識別的語言搅方,默認(rèn)是英文
? ? ? ? ? ? // config.OutputFormat = OutputFormat.Detailed;是讓 recognizer.Recognized 中可以通過var best = e.Result.Best();
? ? ? ? ? ? //拿到一句話的多個(gè)識別形式:比如數(shù)字是寫成3還是three
? ? ? ? ? ? var stopRecognition = new TaskCompletionSource<int>();
? ? ? ? ? ? //不要用AudioConfig.FromWavFileInput,因?yàn)樗麩o法處理大wav文件
? ? ? ? ? ? using (var pushStream = AudioInputStream.CreatePushStream())
? ? ? ? ? ? using (var audioInput = AudioConfig.FromStreamInput(pushStream))
? ? ? ? ? ? using (var recognizer = new SpeechRecognizer(config, audioInput))
? ? ? ? ? ? {
? ? ? ? ? ? ? ? recognizer.Recognized += (s, e) =>
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? if (e.Result.Reason == ResultReason.RecognizedSpeech)
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? Console.WriteLine($"RECOGNIZED: Text={e.Result.Text} Duration={e.Result.Duration} OffsetInTicks={e.Result.OffsetInTicks}");
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? else if (e.Result.Reason == ResultReason.NoMatch)
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? Console.WriteLine($"NOMATCH: Speech could not be recognized.");
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? };
? ? ? ? ? ? ? ? recognizer.Canceled += (s, e) =>
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? Console.WriteLine($"CANCELED: Reason={e.Reason}");
? ? ? ? ? ? ? ? ? ? if (e.Reason == CancellationReason.Error)
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? Console.WriteLine($"CANCELED: ErrorCode={e.ErrorCode}");
? ? ? ? ? ? ? ? ? ? ? ? Console.WriteLine($"CANCELED: ErrorDetails={e.ErrorDetails}");
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? stopRecognition.TrySetResult(0);
? ? ? ? ? ? ? ? };
? ? ? ? ? ? ? ? recognizer.SessionStarted += (s, e) =>
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? Console.WriteLine("\nSession started event.");
? ? ? ? ? ? ? ? };
? ? ? ? ? ? ? ? recognizer.SessionStopped += (s, e) =>
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? Console.WriteLine("\nSession stopped event.");
? ? ? ? ? ? ? ? ? ? stopRecognition.TrySetResult(0);
? ? ? ? ? ? ? ? };
? ? ? ? ? ? ? ? // Starts continuous recognition. Uses StopContinuousRecognitionAsync() to stop recognition.
? ? ? ? ? ? ? ? await recognizer.StartContinuousRecognitionAsync().ConfigureAwait(false);
? ? ? ? ? ? ? ? // open and read the wave file and push the buffers into the recognizer
? ? ? ? ? ? ? ? using (BinaryAudioStreamReader reader = Helper.CreateWavReader(file))
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? byte[] buffer = new byte[1000];
? ? ? ? ? ? ? ? ? ? while (true)
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? var readSamples = reader.Read(buffer, (uint)buffer.Length);
? ? ? ? ? ? ? ? ? ? ? ? if (readSamples == 0)
? ? ? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? ? ? break;
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? ? pushStream.Write(buffer, readSamples);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? pushStream.Close();
? ? ? ? ? ? ? ? // Waits for completion.
? ? ? ? ? ? ? ? // Use Task.WaitAny to keep the task rooted.
? ? ? ? ? ? ? ? Task.WaitAny(new[] { stopRecognition.Task });
? ? ? ? ? ? ? ? // Stops recognition.
? ? ? ? ? ? ? ? await recognizer.StopContinuousRecognitionAsync().ConfigureAwait(false);
? ? ? ? ? ? }
? ? ? ? }
? ? }
}
可以看到在recognizer.Recognized事件中绽族,我們可以拿到識別出來的一段段的話姨涡,一個(gè)比較長的語音會分為多次觸發(fā)recognizer.Recognized事件識別出來。e.Result.Text屬性是識別出來的文本吧慢,e.Result.Duration是識別出來的這段話的長度涛漂,e.Result.OffsetInTicks是識別出來這句話在整個(gè)音頻中的位置,可以用?TimeSpan.FromTicks(e.Result.OffsetInTicks)轉(zhuǎn)換為TimeSpan類型。
5匈仗、注意:包括Azure認(rèn)知服務(wù)在內(nèi)的幾乎所有語音識別引擎都只支持wav文件瓢剿,不支持mp3等格式的文件。而且需要注意的是wav碼率必須是16000悠轩,否則OffsetInTicks時(shí)間將會不準(zhǔn)確间狂。
可以用NAudio這個(gè)組件把mp3轉(zhuǎn)換為wav文件,NAudio是全托管代碼火架,不像ffmpeg是單獨(dú)運(yùn)行一個(gè)進(jìn)程鉴象,無論是調(diào)試還是其他的都很麻煩。
下面是使用NAudio進(jìn)行mp3轉(zhuǎn)為wav的代碼何鸡,再次強(qiáng)調(diào):碼率必須是16000.
using (Mp3FileReader reader = new Mp3FileReader(mp3file))
using (WaveStream pcmStream = new WaveFormatConversionStream(new WaveFormat(16000, 1),reader))
{
WaveFileWriter.CreateWaveFile(wavFile, pcmStream);
}