C#で音声ファイルから文字起こし（Azure、プロキシ、時刻出力、一括、SpeechToText）


# はじめに

*こんにちは、のんびりエンジニアのたっつーです。*
*[今回の記事](https://www.fast-system.jp/charp-azure-speech-to-text-proxy/)を紹介しているのでよろしければ見てください。*

最近は、音声ファイルから文字起こし（Speech to Text）をする機会が多かったので、Azureを使ったサンプルソースコードをご紹介させていただきます。

# 使い方

## Azure Speech Service で キー／リージョン を取得

まずは、Azure クラウドを使用するので、
Azure Portal で Speech Services を作成して、***Key / Region*** を取得します。
Region は[こちら](https://docs.microsoft.com/ja-jp/azure/cognitive-services/speech-service/regions)から正確な値を取得してください。
![https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_149921_45db6915-0dd8-0041-e608-f438f7d9d5ff.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227880/eef75ab2-449c-348b-b746-64fb9858cb87.jpeg)

例）
const string KEY = “32文字の英数字”;
const string REGION = “japaneast”;

## プロジェクトの作成

Visual Studio 2019 で、新規で***「コンソールアプリ（.NET Framework）」を選択***して、***「SpeechToTextSample」***でプロジェクトを作成します。
※プロジェクト名は任意です。
![image-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227880/18a464a8-cacc-c788-4e55-30a8edd39e3e.png)

## デバッグ構成の変更

今回は、「Microsoft.CognitiveServices.Speech」のライブラリを使うのですが、このライブラリを使う時には、***デバッグ構成「Any CPU」だとうまく動かないので、x64/x84 にする***。CPUアーキテクチャを変更しておきましょう。

64bit環境→x64
32bit環境→x84 

![debug-add.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227880/790c3e77-8c02-b0f2-2c87-930cf502e3d6.gif)

## Azure Speech ライブラリを追加

Azure Speechサービスを、C#で使えるようにライブラリを追加します。
メニューから、「ツール > NuGet パッケージマネージャー > ソリューションのNuGetパッケージ管理」でNuGet管理マネージャーを起動してください。

次に、「Microsoft Speech」で検索を行い、「Micorosoft.Cognitive.Services.Speech」ライブラリをインストールしてください。

![image-9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227880/06a229a8-176e-1ec8-55e1-73676a6b5d63.png)

## ソースコードの追加

次に、Azure にアクセスして、SpeechToTextを実行してくれるクラス、「AzureSpeechToText.cs」でソースコードを追加してください。![image-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227880/76b411de-62b8-cd27-380e-1323f5db6396.png)

```C#:AzureSpeechToText.cs
using Microsoft.CognitiveServices.Speech;
using Microsoft.CognitiveServices.Speech.Audio;
using System;
using System.Text;
using System.Threading.Tasks;

namespace SpeechToTextSample
{
    public class AzureSpeechToText
    {
        const string KEY = "32文字の英数字";
        const string REGION = "japaneast";
        const string LOCALE = "ja-JP";

        const string PROXY_HOST = "";
        const int PROXY_PORT = 0;

        public async Task<string> RecognizeAsync(string wavefile)
        {
            // Console.WriteLine($"RecognizeAsync: waveFile={wavefile}");

            // Azure情報の設定
            var config = SpeechConfig.FromSubscription(KEY, REGION);
            config.SpeechRecognitionLanguage = LOCALE;

            // Proxyの設定
            if (!string.IsNullOrEmpty(PROXY_HOST))
                config.SetProxy(PROXY_HOST, PROXY_PORT);

            // 文字起こし開始
            var result = await RecognizeStartAsync(config, wavefile);

            return result;
        }

        private async Task<string> RecognizeStartAsync(SpeechConfig config, string wavefile)
        {
            var sb = new StringBuilder();
            var stopRecognition = new TaskCompletionSource<int>();

            // SpeechToTextの開始
            using (var audioInput = AudioConfig.FromWavFileInput(wavefile))
            {
                using (var recognizer = new SpeechRecognizer(config, audioInput))
                {
                    // recognizer.Recognizing += (s, e) =>
                    // {
                    //     Console.WriteLine($"RECOGNIZING: Text={e.Result.Text}");
                    // };

                    recognizer.Recognized += (s, e) =>
                    {
                        if (e.Result.Reason == ResultReason.RecognizedSpeech)
                        {
                            var time = TimeSpan.FromSeconds(e.Result.OffsetInTicks / 10000000).ToString(@"hh\:mm\:ss");
                            var text = $"{time} {e.Result.Text}\n";
                            Console.Write(text);
                            sb.Append(text);
                        }
                        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}");
                            Console.WriteLine($"CANCELED: Did you update the subscription info?");
                            // ret = null;
                        }

                        stopRecognition.TrySetResult(0);
                    };

                    // recognizer.SessionStarted += (s, e) =>
                    // {
                    //     Console.WriteLine("\n    Session started event.");
                    // };

                    // recognizer.SessionStopped += (s, e) =>
                    // {
                    //     Console.WriteLine("\n    Session stopped event.");
                    //     Console.WriteLine("\nStop recognition.");
                    //     if (string.IsNullOrEmpty(ret))
                    //         ret = "";
                    //     stopRecognition.TrySetResult(0);
                    // };

                    // Starts continuous recognition. Uses StopContinuousRecognitionAsync() to stop recognition.
                    await recognizer.StartContinuousRecognitionAsync().ConfigureAwait(false);

                    // Waits for completion.
                    // Use Task.WaitAny to keep the task rooted.
                    Task.WaitAny(new[] { stopRecognition.Task });

                    // Stops recognition.
                    await recognizer.StopContinuousRecognitionAsync().ConfigureAwait(false);
                }
            }

            return sb.ToString();
        }
    }
}
```

## パラメータの変更

該当ソースコードの、以下パラメータをご自分の環境に従って変更してください。

- KEY ： Azure Speechサービスで取得した、Keyを設定
- REGION ： Azure Speechサービスで取得した、Regionを設定（Regionはどこの地域のサーバーかを設定します）
- LOCALE ： 文字起こしする言語の設定
- PROXY_HOST ： プロキシサーバーを挟んでいる場合は、そのホスト
- PROXY_PORT  ：   プロキシサーバーを挟んでいる場合は、そのポート

```C#
    const string KEY = "32文字の英数字";
    const string REGION = "japaneast";
    const string LOCALE = "ja-JP";

    const string PROXY_HOST = "";
    const int PROXY_PORT = 0;
```

## 使い方

次に、Programクラスの Main関数を以下のように変更します。
※ Main関数を「async Task」に変更するのを忘れないように！

プログラムの概要としては、「C:\temp」にある 「～.wav」を取得して、「～.wav.txt」を出力するようなプログラムになります。

```C#:Program.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace SpeechToTextSample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var dir = @"C:\temp";
            var speechToText = new AzureSpeechToText();

            // ファイル一覧の取得
            var files = new List<string>();
            files.AddRange(Directory.GetFiles(dir, "*.wav"));

            // テキスト変換の開始
            foreach (var file in files)
            {
                Console.WriteLine("--------------------------");
                Console.WriteLine(file + " ... ");

                var result = await speechToText.RecognizeAsync(file);

                File.WriteAllText(file + ".txt", result);

                Console.WriteLine(file + " ... end");
                Console.WriteLine("");
                Console.WriteLine("");
                Console.WriteLine("");
            }
        }
    }
}
```

## 実行結果

今回は、このサイトから音声サンプルをお借りしてみましたので、気になる方は試してみてください。

実際に実行してみると、正常に変換がされている事が確認できました。
一部変換ミスが見られますが、クラウドサービスの品質が改善するのを期待したいところです。

![image-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227880/c606da04-274b-8f5d-5d0b-0a60d0cb2793.png)
![image-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227880/cfe99e38-ea5f-a7e0-8642-6f3750b18075.png)

ファイルの中身は、こんな感じになります。
当該.wavファイルの、時刻（時分秒） と変換されたテキストが対になって出力されます。

![image-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227880/18043f55-c225-0770-60c7-dd1547b470c5.png)

# 終わりに

*よければ ブログ「[Unity+AssetStoreおすすめ情報](https://www.fast-system.jp/)」の方にも色々記載しているのでぜひご参照いただければと思います。*