top of page
  • nakaji

[C#] Azure Cognitive Service で日本語音声の文字起こし

更新日:2023年2月25日

TLDR(要約)

  • 複数人が話している音声ファイルから、話者を区別してテキスト変換するプログラムを作成

  • 音声ファイルの形式がけっこう制約ありなので、変換などの前処理が必要

  • 文字起こしの精度は良好だが、話者の区別は精度に課題があり

ちなみにAWSやGCPではなくAzureを使った理由は、ちょうど使用する環境があっただけで特に理由は無いです。

 

Azureを使った文字起こしの概要

公式のドキュメントにある図が分かりやすかったので引用:

各要素ごとに簡単に説明すると、


Device

音声を録音するデバイス、つまりマイクのこと。マルチチャンネル音声である必要があり、普段我々が使ってるPCの内蔵マイクや外付けUSBのマイクだと未対応。僕の環境にもそういうマイクは無かったので、今回はこの部分は予め録音した音声ファイルをマルチチャンネル形式に変換して使いました。


Speech SDK

文字起こしをするプログラム本体。今回はサンプルコードで提供されているC#を使って作成。図を見ると分かるが、SDKからAzureのサービスを呼ぶため、Azureのアカウントが必要。


User Enrollment

事前に会話に参加する話者の声紋を作成・登録し、話者を区別する際に用いる。ここの処理はスキップしても文字起こしを実現することは可能で、その場合はアウトプットが、「ゲスト1:(話した内容)、ゲスト2:(話した内容)」と匿名表記になる。


Conversation Transcription Service

Azure側で提供している音声認識サービス。SDK経由で呼ばれている。音声の時間に応じた利用料金が請求されるので破産しないように注意。2021年4月時点では、月5時間までなら無料で使える:


Output:プログラムで処理した結果の出力。音声認識なので文字起こししたテキストが出力され、加えてどの話者が話したか示すラベル(Speaker ID)が付与される。


動かすまでに最低限必要なこと

音声ファイルの用意/フォーマット変換

英語と日本語で認識精度も変わってくると思うので、音声ファイルは各言語で1つずつ用意。


音声のフォーマットについてはサンプルコードをそのまま使おうとすると、以下の形式に合わせる必要がある。

  • サンプリングレート:16kHz

  • ビットレート:16bit

  • チャンネル数:8ch

  • 形式:WAVE

もしかしたらドキュメントを読み込めば上記以外でも文字起こしが出来るかもしれないが、そこまでのパワーは無かったので、今回は素直に前処理を行った:

変換についてはAudacity(フリーソフト)を用いて実施。以下のような感じでモノラル音声開いて、無音のトラックを7個追加⇒Exportして、無理やり8ch音声を作成。

※サンプリングレート、ビットレートもAudacityで変換可能



Azure Cognitive Service のインスタンス作成

Azureのサービスを使うため、アカウントと音声認識を処理してくれるインスタンスが必要。

Azureでアカウント登録が終わった後に、Azureポータルから「Cognitive Service」と検索⇒Createで作成。

ここで一点注意なのがインスタンスを作成するロケーションで、アジア地域はまだ対応していない。

centralus、eastasia、eastus、westeurope のいずれかで作成が必要のため、Central USで作成。

上記画面から確認できる Subscription Key(※)はプログラムで使用する際に必要なので控えておく。

※漏れると他人にAI機能を勝手に使われるリスクがあるため、管理は注意


プログラム作成

上記にC#のサンプルコードが提供されているが、いくつか不十分な点があるので修正を行った。

認識対象の音声言語を指定

デフォルトだと設定が英語になっており、日本語音声でそのまま動かすと結果が空で返ってくる。以下のコードを設定部分で書いておく:

config.SpeechRecognitionLanguage = "ja-JP";

音声ファイルをアップロード⇒文字起こしを開始をする処理

サンプルコードだと、音声ファイルをアップロードする箇所と文字起こしをする箇所がそれぞれコードが分かれている。アップロードした音声をどうやって文字起こしするプログラムに渡すのかがよく分からなかった。。

Github上でコード検索して、似たようなことをやってそうなコードを組み合わせて以下のようなメソッドを作成した。

private static async Task<string> UploadAudioAndStartRemoteTranscription(string key, string region)
{
    AudioStreamFormat audioStreamFormat;
    var config = SpeechConfig.FromSubscription(key, region);
    config.SpeechRecognitionLanguage = "ja-JP";
    //config.SpeechRecognitionLanguage = "en-US";
    config.SetProperty("ConversationTranscriptionInRoomAndOnline", "true");
    config.SetServiceProperty("transcriptionMode", "RealTimeAndAsync", ServicePropertyChannel.UriQueryParameter);
    var waveFilePullStream = OpenWavFile(@"C:\Users\xxxx\clip-8ch-16000.wav", out audioStreamFormat);
    var audioInput = AudioConfig.FromStreamInput(AudioInputStream.CreatePullStream(waveFilePullStream, audioStreamFormat))
    var meetingId = Guid.NewGuid().ToString();
    using (var conversation = await Conversation.CreateConversationAsync(config, meetingId))
    {
        using (var conversationTranscriber = TrackSessionId(new ConversationTranscriber(audioInput)))
        {
            await conversationTranscriber.JoinConversationAsync(conversation)
            var user1 = User.FromUserId("User1");
            await conversation.AddParticipantAsync(user1)
            var user2 = User.FromUserId("User2");
            await conversation.AddParticipantAsync(user2)
            var result = await GetRecognizerResult(conversationTranscriber, meetingId);
        }
    }
    return meetingId;
}

なお、声紋を作成⇒事前に登録する処理は上記には含んでいないが、以下のような音声ファイルから声紋情報をJSON形式で返却するメソッドを作成すれば使える。

private static async Task<string> GetVoiceSignatureString(string filename)
{
    var subscriptionKey = "xxxxxx";
    var region = "centralus"
    byte[] fileBytes = File.ReadAllBytes(filename);
    var content = new ByteArrayContent(fileBytes);
    var client = new HttpClient();
    client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", subscriptionKey);
    client.DefaultRequestHeaders.Add("language", "ja-jp");
    var response = await client.PostAsync($"https://signature.{region}.cts.speech.microsoft.com/api/v1/Signature/GenerateVoiceSignatureFromByteArray?enableTranscription=true&language=ja-JP", content);
    var jsonData = await response.Content.ReadAsStringAsync();
    var result = JsonConvert.DeserializeObject<VoiceSignature>(jsonData);
    return JsonConvert.SerializeObject(result.Signature);
}

が、声紋作成で使っているAPIは日本語音声の場合、nullしか返ってこないので未だ利用できる状態では無さそうだった。

request:
curl -X POST "https://signature.centralus.cts.speech.microsoft.com/api/v1/Signature/GenerateVoiceSignatureFromFormData?language=ja-JP&enableTranscription=true" -H  "accept: application/json" -H  "Content-Type: multipart/form-data" -H "Ocp-Apim-Subscription-Key: ${KEY}" -F "file=@japanese1.wav;type=audio/wav"

response:
(null)

※英語音声はちゃんと返ってくるので使い方に間違いは無さそう

request:
 curl -X POST "https://signature.centralus.cts.speech.microsoft.com/api/v1/Signature/GenerateVoiceSignatureFromFormData?language=en-US&enableTranscription=true" -H  "accept: application/json" -H  "Content-Type: multipart/form-data" -H "Ocp-Apim-Subscription-Key: ${KEY}" -F "file=@english1.wav;type=audio/wav"

response:
{"Status":"OK","Signature":{"Version":"0","Tag":"BbzpFgUU56qoGh4RFjTJUEjciblc0ZQ/3wiLqPt/R+0=","Data":"bRLtfCikL52jhduAC+HycdZdDzv4o/Rycn7c2U9jKqXOd5zVGu9pz5Su2i46G4+4JVMt8VbPzkI7KCi7nNKL/KJCT27jFTtkSNBJka1yJS8ArNkTS4cxwZq7jt5N5UC0ZSUgAyy2ZBpNkDRiJ5/mQIr65cE2X3Mbx6T68xgPpzEFHK24VtImEyew/zfkPb3qyeK5KW3RB+0mglSGQWGGtK9wNv6kNOOWepPMT4eOrNoXSqr0WxYMvcwGc2WK/B2Y6baCCgiP7kaLoLfs3b4eODDIQRgEOScsT0vcv/Qa3b4C/PkIllbdT/6MxcmNjgWyQf76dL5aSlPPs3fxN32EhaPkaupkahK9uRNq12HjF2fAnWtoX7r0LnfCMFgV0tdoYpKEAZLbStHB5QlFEcU7UrG5ite6vJve/0GU/qTNM3YeWDHP8xFcB7xnj+Oz/3XqMoshOUKHQsC4BdyW8Qf0XbfgmOOGWsAsLVtOv/ZyxaUPri+YVWWMC0irJnTplCg0xcXh/+Euq3RVo+di6EFObtErUU7Afo7cFxf+aDj1O/VBxrF8D1x/XafEOzCaXcvh4fD+OBA5+r1U2MMY2PIDr2CMbGTHZtni/hEZaagBPBsbXRRWO2fRW4Xe+qF6qk1Ge1pMXuEgaXkz3/qdCVdBLGwK6w6uZ4LeLzZeemdaD4RJTMMZIRuzTuzhtkldV8Yd"},"Transcription":"this is the test of the recognition can you find out who i am this is the test of the recognition can you find out who i am too this is zach can you find out who i am this is zach can you find out who i am."}

実行結果

日本語音声

日本語音声をインプットに、実際にプログラムを動かして出力された結果:

SessionId: 7dbfb64784b4459fb094150260c11ff3
200
200
200
Id = ab86dec8-e415-4737-81aa-60d9a62abce6
500000, RecognizedSpeech, Guest_0, 500000, 00:00:05.0100000
{"UserId":"Guest_0","ResultId":"500000","reason":3,"Text":"なんか、当時加入されたきっかけとか、当時のことって覚えていらっしゃいますか?","DurationInTicks":50100000,"OffsetInTicks":500000}

62900000, RecognizedSpeech, Guest_0, 62900000, 00:00:10.3600000
{"UserId":"Guest_0","ResultId":"62900000","reason":3,"Text":"そうですね。会社でですかね。みんななんか1斉に案内があって、みんな入っているので入ったほうがいいかな。","DurationInTicks":103600000,"OffsetInTicks":62900000}

音声⇒テキスト変換の観点では、かなり正確に文字起こしされている。ただし、話者の区別は出来ておらず、UserIdがGuest_0と同じ話者と区別されている(実際は別の人が喋っている)。

英語音声

出力された結果:

SessionId: f785311b11c44f49ab59f45a31cc66ca
200
200
200
200
Id = d043ddc5-f33e-425a-894d-2fd123cd8918
This is the test of the recognition., 4500000, RecognizedSpeech, Guest_0, 4500000, 00:00:02.0500000
{"UserId":"Guest_0","ResultId":"4500000","reason":3,"Text":"This is the test of the recognition.","DurationInTicks":20500000,"OffsetInTicks":4500000}

Can you find out who I am?, 25100000, RecognizedSpeech, Guest_0, 25100000, 00:00:01.5700000
{"UserId":"Guest_0","ResultId":"25100000","reason":3,"Text":"Can you find out who I am?","DurationInTicks":15700000,"OffsetInTicks":25100000}

This is the test of the recognition., 40900000, RecognizedSpeech, Guest_0, 40900000, 00:00:01.8500000
{"UserId":"Guest_0","ResultId":"40900000","reason":3,"Text":"This is the test of the recognition.","DurationInTicks":18500000,"OffsetInTicks":40900000}

Can you find out who I am too?, 59500000, RecognizedSpeech, Guest_0, 59500000, 00:00:01.8700000
{"UserId":"Guest_0","ResultId":"59500000","reason":3,"Text":"Can you find out who I am too?","DurationInTicks":18700000,"OffsetInTicks":59500000}

This is Zack., 78900000, RecognizedSpeech, Guest_0, 78900000, 00:00:00.9700000
{"UserId":"Guest_0","ResultId":"78900000","reason":3,"Text":"This is Zack.","DurationInTicks":9700000,"OffsetInTicks":78900000}

Can you find out who I am?, 88900000, RecognizedSpeech, Guest_0, 88900000, 00:00:01.4700000
{"UserId":"Guest_0","ResultId":"88900000","reason":3,"Text":"Can you find out who I am?","DurationInTicks":14700000,"OffsetInTicks":88900000}

This is Zack, can you find out who I am?, 103900000, RecognizedSpeech, Guest_0, 103900000, 00:00:02.1700000
{"UserId":"Guest_0","ResultId":"103900000","reason":3,"Text":"This is Zack, can you find out who I am?","DurationInTicks":21700000,"OffsetInTicks":103900000}

POV is not there., 127450000, RecognizedSpeech, Unidentified, 127450000, 00:00:00.9500000
{"UserId":"Unidentified","ResultId":"127450000","reason":3,"Text":"POV is not there.","DurationInTicks":9500000,"OffsetInTicks":127450000}

男性⇒最後に女性が話す音声だが、最後のテキストのUserIdがGuest_0と区別されており別々の話者と一応は認識できている。

まとめ

話者の区別に関する精度はまだまだ

日本語の会話で、誰が何を話しているかを区別するのは未だまだ難しそう。声紋を事前に登録しておけば区別の精度は上がるかもしれないが、日本語だと少なくとも声紋作成のAPIは未だ利用は出来るレベルでは無い。

Microsoftに期待だが、日本語関連のIssueは後回しにされてる気もするので、日本語サポートにはもう少し時間はかかりそう。。


音声ファイルの作成方法には改善の余地あり

今回、モノラルの音声ファイルを無理やり8chに変換して用意したので、そもそもAzure側が期待しているデータでは無い可能性が大いにある。おそらくマルチチャンネルを前提としているのは、各チャンネルで別々の話者の音声がある程度分離されて録音されていることを想定し、話者の区別を行っているのではと思われる。

どうやってそのようなマルチチャンネルの音声を用意するかだが、調べた限りは以下の商品が良さそう。


Azure Kinect DK

7マイクアレイに加え、深度センサや加速度センサなど様々なセンサを搭載したデバイス。公式ドキュメントでもこの商品やRooboが推されているので、Azureで素直にやるとしたら第一候補かなと思う。


FR1000 (NTTアドバンステクノロジー)

NTT-ATが開発している高指向性のマイク。上記の製品仕様では2指向(ステレオ)と書いてあるが、拡張のソフトウェアモジュールを利用することで、12指向までは分離して録音できるとのこと

モジュールの形式は、.dll (Windows) または.so (Linux)とのこと。いくつか商用利用の事例・実績もありそうなので、ユースケースに合ってれば候補と考えられる。


以上です!

bottom of page