iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

3

今天要製作語音機器人,我會使用 Azure 的 「Speech To Text」 服務將用戶的語音訊息轉成文字,讓 Line Bot 可以支援語音操作。

開使之前

這篇內容會接續之前介紹的東西,想了解完整過程的讀者可以先看。
[Day02] 使用 C# 建立 LINE Bot 聊天機器人 - LineMessagingApi


新增 Cognitive Services 認知服務

開啟 Azure 後新增 「Cognitive Services 認知服務」,該服務被歸類在 AI + 機器學習 分類下。

https://ithelp.ithome.com.tw/upload/images/20200121/20106865WuzyOOartc.jpg

接著選擇 「語音」 項目。

https://ithelp.ithome.com.tw/upload/images/20200121/2010686576b63Wd3Up.jpg

定價層記得要選免費的 「F0」

https://ithelp.ithome.com.tw/upload/images/20200121/20106865gEYqIKHElJ.jpg

要記住 「Key」「端點」 之後程式中會用到。

https://ithelp.ithome.com.tw/upload/images/20200121/20106865RYs7eyADxk.jpg

收費方式可以參考: 認知服務定價 — 語音服務


程式部分

需要安裝的套件 (Nuget):

  • Microsoft.CognitiveServices.Speech
  • NAudio

修改 appsettings.json 新增參數。

  • speechKey: 密鑰
  • speechDomain: 地區

完整的 appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "LineBot": {
    "channelSecret": "...",
    "accessToken": "...",
    "speechKey": "...",
    "speechDomain": "eastasia"
  }
}

調整 LineBotConfig.cs

public class LineBotConfig
{
    public string channelSecret { get; set; }
    public string accessToken { get; set; }
    public string speechKey { get; set; }
    public string speechDomain { get; set; }
}

調整 Startup.cs

services.AddSingleton<LineBotConfig, LineBotConfig>((s) => new LineBotConfig
{
    channelSecret = Configuration["LineBot:channelSecret"],
    accessToken = Configuration["LineBot:accessToken"],
    speechKey = Configuration["LineBot:speechKey"],
    speechDomain = Configuration["LineBot:speechDomain"]
});

在 LineBotController 內初始化 SpeechConfig 物件。

services.AddSingleton<LineBotConfig, LineBotConfig>((s) => new LineBotConfig
{
    channelSecret = Configuration["LineBot:channelSecret"],
    accessToken = Configuration["LineBot:accessToken"],
    speechKey = Configuration["LineBot:speechKey"],
    speechDomain = Configuration["LineBot:speechDomain"]
});

完整的 LineBotController.cs

[Route("api/linebot")]
public class LineBotController : Controller
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly HttpContext _httpContext;
    private readonly LineBotConfig _lineBotConfig;
    private readonly ILogger _logger;

    public LineBotController(IServiceProvider serviceProvider,
        LineBotConfig lineBotConfig,
        ILogger<LineBotController> logger)
    {
        _httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
        _httpContext = _httpContextAccessor.HttpContext;
        _lineBotConfig = lineBotConfig;
        _logger = logger;
    }

    [HttpPost("run")]
    public async Task<IActionResult> Post()
    {
        try
        {
            //lineMessagingClient
            var events = await _httpContext.Request.GetWebhookEventsAsync(_lineBotConfig.channelSecret);
            var lineMessagingClient = new LineMessagingClient(_lineBotConfig.accessToken);

            //SpeechConfig
            var speechConfig = SpeechConfig.FromSubscription(_lineBotConfig.speechKey, _lineBotConfig.speechDomain);

            var lineBotApp = new LineBotApp(lineMessagingClient, speechConfig);
            await lineBotApp.RunAsync(events);
        }
        catch (Exception ex)
        {
            _logger.LogError(JsonConvert.SerializeObject(ex));
        }
        return Ok();
    }
}

最後調整 LineBotApp.cs 建後式,設定部分就完成了。

...
private readonly SpeechConfig _speechConfig;
public LineBotApp(LineMessagingClient lineMessagingClient, SpeechConfig speechConfig)
{
    ...
    _speechConfig = speechConfig;
}

將語音訊息轉成文字

接下來就開始今天的重點,將 Line Bot 接收到的語音訊息轉成文字。

語音服務只支援 wav 格式的檔案,所以需要先使用 NAudioaac 轉檔。

要注意 Line 的錄音檔,不同平台會使用不同格式,Andriod 的是 .aac 格式。

//將 AAC 轉成 WAV
using (var reader = new MediaFoundationReader(originalFilePath))
using (var pcmStream = new WaveFormatConversionStream(
    //需為 16KHz
    new WaveFormat(16000, 1), reader))
{
    WaveFileWriter.CreateWaveFile(filePath, pcmStream);
}

接著設定語言和聲音名稱,聲音可以選擇男生或女生。

//設定語言為繁體中文
_speechConfig.SpeechRecognitionLanguage = "zh-TW";

//設定語音名稱
//zh-TW-Yating-Apollo、zh-TW-HanHanRUS、zh-TW-Zhiwei-Apollo
_speechConfig.SpeechSynthesisVoiceName ="zh-TW-Yating-Apollo";

語音說明: 語音服務的語言和區域支援

最後將語音送出取得分析結果,這邊使用的 RecognizeOnceAsync 只支援 15 秒內的語音,要注意一下。

//將語音轉為文字
using (var audioInput = AudioConfig.FromWavFileInput(filePath))
using (var recognizer = new SpeechRecognizer(_speechConfig, audioInput))
{
    //語音超過 15 秒需改用 StartContinuousRecognitionAsync 方法
    var result = await recognizer.RecognizeOnceAsync();
    //識別成功
    if (result.Reason == ResultReason.RecognizedSpeech)
    {
        await _messagingClient.ReplyMessageAsync(ev.ReplyToken,
            new List<ISendMessage>
            {
                new TextMessage(result.Text)
            });
    }
    //識別失敗
    else
    {
        await _messagingClient.ReplyMessageAsync(ev.ReplyToken,
            new List<ISendMessage>
            {
                new TextMessage("語音識別失敗!!")
            });
    }
}

詳細用法可以參考文件: Quickstart: Recognize speech from an audio file

完整程式:

protected override async Task OnMessageAsync(MessageEvent ev)
{
    switch (ev.Message.Fix())
    {
        case AudioEventMessage audioMessage:
        {
            if (audioMessage.ContentProvider.Type != ContentProviderType.Line)
                break;

            //轉換後的語音檔路徑
            var guid = Guid.NewGuid().ToString();
            var filePath = $@"D:\home\site\wwwroot\audio-{guid}.wav";
            var originalFilePath = $@"D:\home\site\wwwroot\audio-{guid}.aac";

            //儲存檔案
            using (var stream = 
                await _messagingClient.GetContentStreamAsync(audioMessage.Id))
            using (var fs = File.Create(originalFilePath))
            {
                stream.CopyTo(fs);
            }

            //將 AAC 轉成 WAV
            using (var reader = new MediaFoundationReader(originalFilePath))
            using (var pcmStream = new WaveFormatConversionStream(
                //WAV 需為 16KHz
                new WaveFormat(16000, 1), reader))
            {
                WaveFileWriter.CreateWaveFile(filePath, pcmStream);
            }

            //設定語言為繁體中文
            _speechConfig.SpeechRecognitionLanguage = "zh-TW";

            //設定語音名稱
            //zh-TW-Yating-Apollo、zh-TW-HanHanRUS、zh-TW-Zhiwei-Apollo
            _speechConfig.SpeechSynthesisVoiceName = "zh-TW-Yating-Apollo";
            
            //將語音轉為文字
            using (var audioInput = AudioConfig.FromWavFileInput(filePath))
            using (var recognizer = new SpeechRecognizer(_speechConfig, audioInput))
            {
                //語音超過 15 秒需改用 StartContinuousRecognitionAsync 方法
                var result = await recognizer.RecognizeOnceAsync();

                //識別成功
                if (result.Reason == ResultReason.RecognizedSpeech)
                {
                    await _messagingClient.ReplyMessageAsync(ev.ReplyToken,
                        new List<ISendMessage>
                        {
                            new TextMessage(result.Text)
                        });
                }
                //識別失敗
                else
                {
                    await _messagingClient.ReplyMessageAsync(ev.ReplyToken,
                        new List<ISendMessage>
                        {
                            new TextMessage("語音識別失敗!!")
                        });
                }
            }
        }
        break;
    }
}

結果:

https://ithelp.ithome.com.tw/upload/images/20200121/20106865akmLOXqMUt.jpg


將文字訊息轉成語音

也可以反向將文字訊息轉成語音。

需要另外安裝 (Nuget):

  • TagLibSharp
  • MediaToolkit.NetCore

首先設定語言。

//設定語言為繁體中文
_speechConfig.SpeechSynthesisLanguage = "zh-TW";

接著將文字轉成語音。

//將文字轉成語音
var result = null as SpeechSynthesisResult;
using (var fileOutput = AudioConfig.FromWavFileOutput(originalFilePath))
using (var synthesizer = new SpeechSynthesizer(_speechConfig, fileOutput))
{
    result = await synthesizer.SpeakTextAsync(textMessage.Text);
}

詳細用法可以參考文件: Quickstart: Synthesize speech into an audio file

LINE 的 Audio 訊息需要提供時間長度,我使用 TagLib 套件取得檔案的 Duration 屬性。

//讀取語音時間
var tfile = TagLib.File.Create(originalFilePath);
var duration = (int)tfile.Properties.Duration.TotalMilliseconds;

最後將語音回覆給用戶,因為 Line 不支援 wav 所以和上面一樣要先轉檔。

一開始我使用 NAudio 轉檔。

using (var reader = new WaveFileReader(originalFilePath))
{
    MediaFoundationEncoder.EncodeToAac(reader, filePath);
}

結果出現下面錯誤,Google 後發現 NAudio 需要電腦有 AAC 編碼器才能轉檔,但 API 是架在 App Service 上,找很久找不到在雲端安裝編碼器的方法,所以放棄此法。

No suitable AAC encoders available

這邊歡迎有經驗的大大分享,網路上討論這部分的資料很少

山不轉路轉,只好找看看有沒有別的套件可以用,最後我選擇了 MediaToolkit.NetCore,它是 FFmpeg.NET 封裝,還蠻有名的用的人很多,不過底層是呼叫 ffmpeg.exe,實務上不知道會不會遇到一些奇怪的問題就是。

下載 ffmpeg.exe 放在網站根目錄。
連結: https://www.ffmpeg.org/

免費方案的 App Service 只支援 32 位元的程式,下載的時候需要注意一下

//將 WAV 轉成 AAC
var inputFile = new MediaFile (originalFilePath);
var outputFile = new MediaFile(filePath);
using (var engine = new Engine($@"D:\home\site\wwwroot\ffmpeg.exe"))
{
    engine.Convert(inputFile, outputFile);
}

await _messagingClient.ReplyMessageAsync(ev.ReplyToken,
    new List<ISendMessage>
    {
        new AudioMessage(
            $"https://ibottestapi.azurewebsites.net/audio-{guid}.aac", 
            duration)
    });

完整程式:

protected override async Task OnMessageAsync(MessageEvent ev)
{
    switch (ev.Message.Fix())
    {
        case TextEventMessage textMessage:
        {
            //設定語言為繁體中文
            _speechConfig.SpeechSynthesisLanguage = "zh-TW";

            //轉換後的語音檔路徑
            var guid = Guid.NewGuid().ToString();
            var filePath = $@"D:\home\site\wwwroot\wwwroot\audio-{guid}.aac";
            var originalFilePath = $@"D:\home\site\wwwroot\wwwroot\audio-{guid}.wav";

            //將文字轉成語音
            var result = null as SpeechSynthesisResult;
            using (var fileOutput = AudioConfig.FromWavFileOutput(originalFilePath))
            using (var synthesizer = new SpeechSynthesizer(_speechConfig, fileOutput))
            {
                result = await synthesizer.SpeakTextAsync(textMessage.Text);
            }

            //轉換成功
            if (result.Reason == ResultReason.SynthesizingAudioCompleted)
            {
                //讀取語音時間
                var tfile = TagLib.File.Create(originalFilePath);
                var duration = (int)tfile.Properties.Duration.TotalMilliseconds;

                //此法需要 aac 編碼器
                //using (var reader = new WaveFileReader(originalFilePath))
                //{
                //    MediaFoundationEncoder.EncodeToAac(reader, filePath);
                //}

                //將 WAV 轉成 AAC
                var inputFile = new MediaFile (originalFilePath);
                var outputFile = new MediaFile(filePath);
                using (var engine = new Engine($@"D:\home\site\wwwroot\ffmpeg.exe"))
                {
                    engine.Convert(inputFile, outputFile);
                }

                await _messagingClient.ReplyMessageAsync(ev.ReplyToken,
                    new List<ISendMessage>
                    {
                        new AudioMessage(
                            $"https://ibottestapi.azurewebsites.net/audio-{guid}.aac", 
                            duration)
                    });
            }
            //轉換失敗
            else
            {
                await _messagingClient.ReplyMessageAsync(ev.ReplyToken,
                    new List<ISendMessage>
                    {
                        new TextMessage("語音轉換失敗!!")
                    });
            }
        }
        break;
    }
}

結果:

https://ithelp.ithome.com.tw/upload/images/20200121/20106865MsI4Izux2x.jpg


結語

寫完這篇後發現,原來 Audio 的世界這麼博大精深,平時聽到的 mp3、wav、m4a 這些都是超複雜的東西,加上網路上這方面的資訊真的不多,只能自己摸索亂試,從錯誤中學習,好險最後有試出來,不然這篇就要夭折了。

最後來看看美中不足的地方,轉檔部分我本來想全用 Stream 處理,這樣才不會產生垃圾檔案,但套件好像不支援,所以如果用在實務上,需要另外寫一支程式定時刪除資料夾中過期的檔案。

今天就到這裡,感謝大家觀看。 (´・ω・`)


參考文章

調用Azure認知服務進行大音頻文件語音識別
體驗 Microsoft Azure 語音服務(C#)


上一篇
[Day16] LINE Bot 的邀請處理 - Webhook Event
下一篇
[Day18] 如何在 LINE Bot 免費推播訊息 - LINE Notify
系列文
Line Bot 心得分享 LineMessagingApi + LUIS + BotFramework27

尚未有邦友留言

立即登入留言