【System.Speech】音声認識で議事録作成


## はじめに

突然ですが、皆さんの会社では頻繁に会議が行われているでしょうか？
小規模なものから大規模なもの、内容も多種多様ですが、
その会議と切っても切れないのが・・・そう、**議事録** です。

膨大な情報を記録するには相応のタイピング速度（手書き速度）が要求され、
様々な意見が飛び交う中では要点を押さえて記録するのも一苦労。
作成した議事録を他の人が確認（添削）する場合もあり、作成・確認コストも馬鹿になりません。

この議事録、もっと「楽に」、そして「正確に」作成できたら・・・
と思ったことはありませんか？
**私はあります。**


## 対応方針

何が大変なのか、は前述した通り。
なら、**会議中の会話を文字としてアウトプットできるもの**があれば・・
それを整形するだけで簡単に、かつ情報漏れの無い議事録ができるはず！
・・・という流れから、**「議事録作成ツール」**を作成する運びとなりました。


## 音声認識技術について

まずは議事録作成ツールの肝となる**音声認識**について調査しました。

音声認識の簡単な仕組みですが、
**音の波長を一つの単語とした音の情報である「音響モデル」**
**単語の組合わせで言葉となりえる情報である「言語モデル」**
この二つを組合わせて、音を言葉として認識する模様。
どちらのモデルも、サンプルデータを豊富にすることで精度が向上できそうです。

続いて実装に向けて調べてみたところ、APIも中々充実しているようです。
以下はAPIの一例です。

**・Julius**
　　オープンソースの高性能な汎用大語彙連続音声認識エンジン。
　　多少精度に不安がありそう。日本語ドキュメントが豊富。
**・音声認識API SDK for Android v2.1.2.1 **
　　名称無し。音声技術「SpeechRec」を使用しており、精度は高め。
**・Google Cloud Speech API**
　　ディープラーニングのニューラルネットワークアルゴリズムを利用。
　　機械学習サービスも利用可能で、高精度な認識が可能。
　　※ただし有料（1年無料トライアルがあります）


色々方法はありそうですが、今回は**System.Speech**を使ってみます。
**※C#で開発します。**

## System.Speech って？

音声認識をサポートする.NET Frameworkクラス ライブラリの名前空間で、
Windowsならデフォルトで搭載されています。
似たようなもので**Microsoft.Speech**がありますが、
こちらはサーバー向けとなっていて、**System.Speech**より精度が落ちるようです。
※**System.Speech**はデスクトップ向けとなってます。


## 実装（サンプル）

実際に**System.Speech**を使って実装してみました。
以下はメインロジックです。

```Form1.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Speech.Recognition;

namespace Speech
{
    public partial class Form1 : Form
    {
        // -------------------------------------------------------------------
        // フィールド
        // -------------------------------------------------------------------

        /// <summary>
        /// 音声認識エンジン
        /// </summary>
        private SpeechRecognitionEngine Engine;


        // -------------------------------------------------------------------
        // コンストラクタ
        // -------------------------------------------------------------------

        public Form1()
        {
            InitializeComponent();

            // 音声認識の設定
            StartRecognition();

            // 音声入力のチェックボックス
            this.checkBoxVoiceInput.Checked = true;
        }


        // -------------------------------------------------------------------
        // イベントハンドラ
        // -------------------------------------------------------------------

        /// <summary>
        /// 音声認識のイベントハンドラ。
        /// </summary>
        /// <param name="sender">イベントソース</param>
        /// <param name="e">イベントデータ</param>
        private void SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            // 生データを表示
            string recognitionWord = e.Result.Text;
            this.label.Text = "認識結果：" + recognitionWord;

            if (e.Result.Confidence >= 0.5)
            {
                if (recognitionWord == "音声認識ON")
                {
                    this.checkBoxVoiceInput.Checked = true;
                }
                else if (recognitionWord == "音声認識OFF")
                {
                    this.checkBoxVoiceInput.Checked = false;
                }
                else
                {
                    if (this.checkBoxVoiceInput.Checked)
                    {
                        // 音声認識データをテキストボックスに反映
                        this.textBox.Text += recognitionWord;
                    }
                }
            }
        }

        /// <summary>
        /// 保存ボタンのイベントハンドラ。
        /// </summary>
        /// <param name="sender">イベントソース</param>
        /// <param name="e">イベントデータ</param>
        private void buttonSave_Click(object sender, EventArgs e)
        {
            var dialog = new SaveFileDialog();
            dialog.Filter = "すべてのファイル(*.*)|*.*";
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                CreateFile(dialog.FileName, this.textBox.Text, false);
            }
        }

        /// <summary>
        /// クリアボタンのイベントハンドラ。
        /// </summary>
        /// <param name="sender">イベントソース</param>
        /// <param name="e">イベントデータ</param>
        private void buttonClear_Click(object sender, EventArgs e)
        {
            this.textBox.Text = null;
        }


        // -------------------------------------------------------------------
        // メソッド
        // -------------------------------------------------------------------

        /// <summary>
        /// 音声認識の設定
        /// </summary>
        private void StartRecognition()
        {
            try
            {
                // 音声認識エンジンの設定
                this.Engine = new SpeechRecognitionEngine(Application.CurrentCulture);

                // 既存のオーディオデバイスをデフォルトの入力とする
                this.Engine.SetInputToDefaultAudioDevice();

                // イベント登録
                this.Engine.SpeechRecognized += new EventHandler<SpeechRecognizedEventArgs>(SpeechRecognized);

                string grammarPath = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "Grammar.txt");
                if (File.Exists(grammarPath))
                { // 文法ファイルが存在する場合
                    var choices = new Choices();
                    foreach (string line in ReadFile(grammarPath, "#"))
                    {
                        choices.Add(line);
                    }
                    var grammar = new Grammar(choices.ToGrammarBuilder());
                    this.Engine.LoadGrammar(new Grammar(choices.ToGrammarBuilder()));
                }
                else
                {
                    this.Engine.LoadGrammarAsync(new DictationGrammar());
                }

                this.Engine.RecognizeAsync(RecognizeMode.Multiple);
            }
            catch (Exception)
            {
                // 音声認識の設定に失敗
                MessageBox.Show("音声認識の設定に失敗しました。", "音声認識", MessageBoxButtons.OK,
                    MessageBoxIcon.None, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly);

                // チェックボックス(音声入力)を非活性
                this.checkBoxVoiceInput.Enabled = false;

                // 音声認識エンジンのオブジェクトを解放
                this.Engine.Dispose();

                // 音声認識エンジンを初期化
                this.Engine = null;
            }
        }

        /// <summary>
        /// ファイル読込み。
        /// </summary>
        /// <param name="path">ファイルパス</param>
        /// <param name="comment">コメント文字</param>
        /// <returns>読込み結果リスト</returns>
        public static List<string> ReadFile(string path, string comment)
        {
            //コメント以外の行を取得
            var lines = File.ReadAllLines(path, Encoding.Default)
                .Where(line => !line.StartsWith(comment)).ToList();

            return lines;
        }

        /// <summary>
        /// ファイル作成。
        /// </summary>
        /// <param name="path">ファイルパス</param>
        /// <param name="data">書き込みデータ</param>
        /// <param name="appendFlg">追記可否フラグ(true:追記,false:上書き)</param>
        public static void CreateFile(string path, string data, bool appendFlg)
        {
            // ファイルを作成するフォルダが存在するかチェック
            // 存在しない場合は、フォルダを作成
            string dirPath = Path.GetDirectoryName(path);
            if (!Directory.Exists(dirPath))
            {
                Directory.CreateDirectory(dirPath);
            }

            //指定ファイルに内容を書き込む
            if (appendFlg)
            {
                //ファイルの末尾に書き加える
                File.AppendAllText(path, data, Encoding.Default);
            }
            else
            {
                //ファイルを上書きする
                File.WriteAllText(path, data, Encoding.Default);
            }

            return;
        }
    }
}
```

## 動かしてみた

実装したサンプルを立ち上げたら、
後はマイクを接続して話してみると・・・
文法定義用ファイル（Grammer.txt）に定義した内容に限定はされますが、
話した内容（音声）が文字として出力されました！
※事前に文法定義用ファイルに認識させたい単語、文章の登録が必要です。

### 文法定義用ファイル例
```Grammer.txt
私
俺
は
が
で
には
社員
です。
でした。
ます。
ました。
あめんぼ赤いなあいうえお
```

### 動作画面例

![2018-02-16_17h02_11.png](https://qiita-image-store.s3.amazonaws.com/0/186615/b22ded50-f2d2-410d-949f-e742f26868a9.png)


実際に動かしてみた感じ、
連続した単語や文章の認識精度にまだまだ改良の余地がありそうです。

## 所感

文法定義用ファイルの充実（学習）、音声認識の精度向上など
課題は残ったものの、議事録作成ツールの取っ掛かりとしては上々かなと。

ゆくゆくは個人判別（同時に喋っても誰がなにを話したか判別して出力）や、
多言語対応とか出来たらなあ、と考えてます。（遠い・・・）