次の方法で共有


音声認識

.NET デスクトップ アプリと音声認識

James McCaffrey

コード サンプルのダウンロード

音声で動作するパーソナル アシスタントの Windows Phone Cortana (そして同様の機能を持つ、フルーツ会社の「名前を言ってはいけないあの人」) の登場により、ソフトウェア開発では音声に対応するアプリがますます重要視されています。今回は、Windows コンソール アプリ、Windows フォーム アプリ、Windows Presentation Foundation (WPF) アプリで、音声認識と音声合成の使用を開始する方法を取り上げます。

Windows Phone アプリ、ASP.NET Web アプリ、Windows ストア アプリ、Windows RT アプリ、Xbox Kinect にも音声機能を追加できますが、今回説明する手法とは異なります。

今回の内容を簡単に把握するには、図 1図 2 に示す 2 つのデモ プログラムのスクリーンショットを見てみるのが一番です。図 1 では、コンソール アプリを起動すると、アプリが即座に「I am awake」というフレーズを発しています。もちろん、この記事を読んでもデモの音声は聞こえないので、デモ プログラムではコンピューターが発話した内容をテキストにして表示します。次に、ユーザーが「Speech on」というコマンドを発話しています。デモ プログラムは、認識したテキストを表示し、プログラム内部で要求を聞いて答える機能を有効にして、2 つの数字を加算しています。

コンソール アプリでの音声認識と音声合成
図 1 コンソール アプリでの音声認識と音声合成

Windows フォーム アプリでの音声認識
図 2 Windows フォーム アプリでの音声認識

ユーザーはアプリに対して、1 と 2 を加算し、その後 2 と 3 を加算するように要求しています。アプリは発話されたこれらのコマンドを認識し、音声で答えています。後半では、これよりも便利に音声認識を使用する方法を説明します。

続いて、ユーザーは「Speech off」というコマンドを発話しています。このコマンドによって、数字を加算するコマンドの聞き取り機能が無効になりますが、音声認識が完全に無効になるわけではありません。このコマンドにより、次に発話した 1 と 2 の加算コマンドが無視されています。最後に、ユーザーは再び音声を有効にし、意味が通らない「Klatu barada nikto」というコマンドを発話しています。アプリはこれを、音声認識を完全に無効にしてアプリを終了するコマンドとして認識しています。

図 2 は、音声に対応するダミーの Windows フォーム アプリです。このアプリは音声コマンドを認識しますが、音声では答えません。アプリの初回起動時は、[Speech On] チェック ボックスがオフになっています。つまり、音声認識は有効になっていません。ユーザーは [Speech On] チェック ボックスをオンにして、「Hello」と発話しています。アプリは認識した音声テキストを、アプリ下部の ListBox コントロールにそのまま出力します。

次に、ユーザーは「Set text box 1 to red」と発話しています。アプリはこれを「Set text box 1 red」と認識しています。ほぼ正解ですが、ユーザーの発話内容と完全には一致していません。図 2 では確認できませんが、アプリ上部にある TextBox コントロール内のテキストは、実際に "red" に設定されます。

次に、ユーザーが「Please set text box 1 to white」と発話しています。アプリは「set text box 1 white」と認識し、そのとおりに動作します。ユーザーが最後に「Goodbye」と発話すると、アプリはそのコマンドをそのまま出力します。しかし、[Speech On] チェック ボックスをオフにするなどの方法で Windows フォームを操作できますが、このアプリは Windows フォームを操作していません。

ここからは、必要な .NET 音声ライブラリのインストールも含め、この 2 つのデモ プログラムの作成プロセスについて順を追って説明します。今回は、少なくとも中級レベルのプログラミング スキルがあることを前提としますが、音声認識または音声合成の知識は問いません。

コンソール アプリへの音声の追加

図 1 のデモ プログラムを作成するには、Visual Studio を起動して、ConsoleSpeech という名前の新しい C# コンソール アプリを作成します。今回は Visual Studio 2010 と Visual Studio 2012 で音声を正しく使用できましたが、最近のバージョンであれば、どのバージョンでも音声は機能します。エディターにテンプレート コードが読み込まれたら、ソリューション エクスプローラー ウィンドウで、Program.cs ファイルの名前をわかりやすい ConsoleSpeechProgram.cs に変更します。これにより、Program クラスの名前が Visual Studio によって自動的に変更されます。

次に、C:\ProgramFiles (x86)\Microsoft SDKs\Speech\v11.0\Assembly にある Microsoft.Speech.dll ファイルへの参照を追加します。今回使用したホスト コンピューターにはこの DLL がなかったため、ダウンロードが必要でした。音声認識と音声合成の追加に必要なファイルのインストールは、すべてが単純な作業というわけにはいきません。インストール プロセスについては、後ほど詳しく説明します。現時点では、Microsoft.Speech.dll がコンピューターに存在しているものとします。

音声 DLL への参照を追加したら、ソース コードの先頭にある using ステートメントを、最上位レベルの System 名前空間を指定するステートメントを除いてすべて削除します。次に、Microsoft.Speech.Recognition 名前空間、Microsoft.Speech.Synthesis 名前空間、および System.Globalization 名前空間の using ステートメントを追加します。最初の 2 つの名前空間が音声 DLL に関連するものです。注: 少し紛らわしいかもしれませんが、System.Speech.Recognition と System.Speech.Synthesis という名前空間もあります。これらの違いについても後ほど説明します。Globalization 名前空間は既定で利用でき、プロジェクトに新しい参照を追加する必要はありません。

図 3 に、コンソール アプリのソース コード全体を示します。付属のコード ダウンロードでも確認できます。中心となる考え方をできるだけ明確にするために、通常行うエラー チェックはすべて削除しています。

図 3 デモ コンソール アプリのソース コード

using System;
using Microsoft.Speech.Recognition;
using Microsoft.Speech.Synthesis;
using System.Globalization;
namespace ConsoleSpeech
{
  class ConsoleSpeechProgram
  {
    static SpeechSynthesizer ss = new SpeechSynthesizer();
    static SpeechRecognitionEngine sre;
    static bool done = false;
    static bool speechOn = true;
    static void Main(string[] args)
    {
      try
      {
        ss.SetOutputToDefaultAudioDevice();
        Console.WriteLine("\n(Speaking: I am awake)");
        ss.Speak("I am awake");
        CultureInfo ci = new CultureInfo("en-us");
        sre = new SpeechRecognitionEngine(ci);
        sre.SetInputToDefaultAudioDevice();
        sre.SpeechRecognized += sre_SpeechRecognized;
        Choices ch_StartStopCommands = new Choices();
        ch_StartStopCommands.Add("speech on");
        ch_StartStopCommands.Add("speech off");
        ch_StartStopCommands.Add("klatu barada nikto");
        GrammarBuilder gb_StartStop = new GrammarBuilder();
        gb_StartStop.Append(ch_StartStopCommands);
        Grammar g_StartStop = new Grammar(gb_StartStop);
        Choices ch_Numbers = new Choices();
        ch_Numbers.Add("1");
        ch_Numbers.Add("2");
        ch_Numbers.Add("3");
        ch_Numbers.Add("4");
        GrammarBuilder gb_WhatIsXplusY = new GrammarBuilder();
        gb_WhatIsXplusY.Append("What is");
        gb_WhatIsXplusY.Append(ch_Numbers);
        gb_WhatIsXplusY.Append("plus");
        gb_WhatIsXplusY.Append(ch_Numbers);
        Grammar g_WhatIsXplusY = new Grammar(gb_WhatIsXplusY);
        sre.LoadGrammarAsync(g_StartStop);
        sre.LoadGrammarAsync(g_WhatIsXplusY);
        sre.RecognizeAsync(RecognizeMode.Multiple);
        while (done == false) { ; }
        Console.WriteLine("\nHit <enter> to close shell\n");
        Console.ReadLine();
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message);
        Console.ReadLine();
      }
    } // Main
    static void sre_SpeechRecognized(object sender,
      SpeechRecognizedEventArgs e)
    {
      string txt = e.Result.Text;
      float confidence = e.Result.Confidence;
      Console.WriteLine("\nRecognized: " + txt);
      if (confidence < 0.60) return;
      if (txt.IndexOf("speech on") >= 0)
      {
        Console.WriteLine("Speech is now ON");
        speechOn = true;
      }
      if (txt.IndexOf("speech off") >= 0)
      {
        Console.WriteLine("Speech is now OFF");
        speechOn = false;
      }
      if (speechOn == false) return;
      if (txt.IndexOf("klatu") >= 0 && txt.IndexOf("barada") >= 0)
      {
        ((SpeechRecognitionEngine)sender).RecognizeAsyncCancel();
        done = true;
        Console.WriteLine("(Speaking: Farewell)");
        ss.Speak("Farewell");
      }
      if (txt.IndexOf("What") >= 0 && txt.IndexOf("plus") >= 0)
      {
        string[] words = txt.Split(' ');
        int num1 = int.Parse(words[2]);
        int num2 = int.Parse(words[4]);
        int sum = num1 + num2;
        Console.WriteLine("(Speaking: " + words[2] + " plus " +
          words[4] + " equals " + sum + ")");
        ss.SpeakAsync(words[2] + " plus " + words[4] +
          " equals " + sum);
      }
    } // sre_SpeechRecognized
  } // Program
} // ns

using ステートメントの後のデモ コード先頭部分は次のようになっています。

namespace ConsoleSpeech
{
  class ConsoleSpeechProgram
  {
    static SpeechSynthesizer ss = new SpeechSynthesizer();
    static SpeechRecognitionEngine sre;
    static bool done = false;
    static bool speechOn = true;
    static void Main(string[] args)
    {
...

アプリの発話機能は、クラススコープの SpeechSynthesizer オブジェクトによって提供されます。SpeechRecognitionEngine オブジェクトにより、アプリは発話された語やフレーズを聞き取り、認識できるようになります。ブール値変数 "done" は、アプリ全体を終了するタイミングを決定します。ブール値変数の speechOn は、プログラムの終了コマンド以外のコマンドを聞き取るかどうかを制御します。

このコンソール アプリはキーボードから入力される値を受け取りません。そのため、アプリは常にコマンドを聞き取る状態になっています。つまり、speechOn が false の場合でも、アプリはプログラムを終了するコマンドのみは認識して実行します。他のコマンドは認識はしても無視します。

Main メソッドの冒頭は、次のようになっています。

try
{
  ss.SetOutputToDefaultAudioDevice();
  Console.WriteLine("\n(Speaking: I am awake)");
  ss.Speak("I am awake");

SpeechSynthesizer オブジェクトのインスタンスは宣言時に作成されます。シンセサイザー オブジェクトの使い方はきわめてシンプルです。SetOutputToDefaultAudioDevice メソッドは、コンピューターのスピーカーに出力を送信します (出力をファイルに送信することもできます)。Speak メソッドは、名前のとおり、文字列を受け取り、発話します。とても簡単です。

音声認識は、音声合成よりもはるかに難易度の高い作業です。Main メソッドでは、続いて認識エンジン オブジェクトを作成しています。

CultureInfo ci = new CultureInfo("en-us");
sre = new SpeechRecognitionEngine(ci);
sre.SetInputToDefaultAudioDevice();
sre.SpeechRecognized += sre_SpeechRecognized;

まず、CultureInfo オブジェクトでは、認識する言語 (ここでは米国英語) を指定します。CultureInfo オブジェクトは、using ステートメントで参照している Globalization 名前空間に含まれています。次に、SpeechRecognitionEngine コンストラクターを呼び出した後、音声入力を既定のオーディオ デバイス (ほとんどの場合はマイク) に設定します。ほとんどのノート PC にはマイクが内蔵されていますが、デスクトップ コンピューターの多くは外付けのマイク (最近の主流はヘッドセット一体型) が必要になるので注意してください。

認識エンジン オブジェクトの重要なメソッドは、SpeechRecognized イベント ハンドラーです。Visual Studio を使用している場合に「sre.SpeechRecognized +=」と入力すると、IntelliSense 機能によって瞬時にステートメントがオートコンプリートされ、"sre_SpeechRecognized" というイベント ハンドラー名が表示されます。Tab キーを押して既定の名前を採用することをお勧めします。

次に、2 つの数字を加算するコマンドの認識機能を設定します。

Choices ch_Numbers = new Choices();
ch_Numbers.Add("1");
ch_Numbers.Add("2");
ch_Numbers.Add("3");
ch_Numbers.Add("4"); // Technically Add(new string[] { "4" });
GrammarBuilder gb_WhatIsXplusY = new GrammarBuilder();
gb_WhatIsXplusY.Append("What is");
gb_WhatIsXplusY.Append(ch_Numbers);
gb_WhatIsXplusY.Append("plus");
gb_WhatIsXplusY.Append(ch_Numbers);
Grammar g_WhatIsXplusY = new Grammar(gb_WhatIsXplusY);

ここで重要なオブジェクトは、Choices コレクション、GrammarBuilder テンプレート、および基盤とする Grammar の 3 つです。今回認識用の Grammar を作成するときに、まず、認識対象の具体例をいくつか箇条書きにしました。たとえば、「What is one plus two?」や「What is three plus four?」などです。

次に、これに対応する汎用テンプレートを決めます。たとえば、「What is <x> plus <y>?」などです。このテンプレートが GrammarBuilder で、テンプレートに当てはめる具体的な値が Choices です。Grammar オブジェクトが、テンプレートとテンプレートに当てはめる選択肢をカプセル化します。

デモ プログラムでは、加算する数字を 1 ~ 4 に制限し、Choices コレクションの文字列としてそれらの数字を追加しています。効率化したアプローチを以下に示します。

string[] numbers = new string[] { "1", "2", "3", "4" };
Choices ch_Numbers = new Choices(numbers);

Choices コレクションを作成するアプローチで効率が悪い例を示したのには 2 つの理由があります。1 つは、音声に関する他の例では、一度に 1 つずつ文字列を追加するアプローチしか見つからなかったためです。もう 1 つは、読者の皆さんが一度に 1 つずつ文字列を追加しないと動作しないと考えているかもしれないためです。Visual Studio でリアルタイムに表示される IntelliSense を見ると、Add オーバーロードの 1 つに "params string[] phrases" という型のパラメーターを使用できることがわかります。params キーワードを知らなければ、Add メソッドが、(String 型の配列や単一の文字列ではなく) 文字列の配列のみを受け取るように思えたかもしれません。個人的には配列を渡すことをお勧めします。

連続した数字の Choices コレクションを作成するのは少し特別で、次のようにプログラムによるアプローチも使用できます。

string[] numbers = new string[100];
for (int i = 0; i < 100; ++i)
  numbers[i] = i.ToString();
Choices ch_Numbers = new Choices(numbers);

GrammarBuilder のスロットに設定する Choices を作成したら、デモ プログラムは次のように GrammarBuilder を作成し、基盤とする Grammar を作成します。

GrammarBuilder gb_WhatIsXplusY = new GrammarBuilder();
gb_WhatIsXplusY.Append("What is");
gb_WhatIsXplusY.Append(ch_Numbers);
gb_WhatIsXplusY.Append("plus");
gb_WhatIsXplusY.Append(ch_Numbers);
Grammar g_WhatIsXplusY = new Grammar(gb_WhatIsXplusY);

このデモ プログラムでは同様のパターンを使用して、開始と停止に関連するコマンドの Grammar を作成しています。

Choices ch_StartStopCommands = new Choices();
ch_StartStopCommands.Add("speech on");
ch_StartStopCommands.Add("speech off");
ch_StartStopCommands.Add("klatu barada nikto");
GrammarBuilder gb_StartStop = new GrammarBuilder();
gb_StartStop.Append(ch_StartStopCommands);
Grammar g_StartStop = new Grammar(gb_StartStop);

文法は非常に柔軟に定義できます。ここでは、「speech on」、「speech off」、および「klatu barada nikto」というコマンドをすべて同じ文法に分類しています。これらのコマンドに論理的な関連性があるためです。この 3 つのコマンドを、3 つの個別の文法を使用して定義することもできます。また、「speech on」コマンドと「speech off」コマンドを 1 つの文法で定義し、「klatu barada nikto」コマンドを 2 つ目の文法で定義してもかまいません。

すべての Grammar オブジェクトを作成したら、それらのオブジェクトを音声認識エンジンに渡します。これで、音声認識が有効になります。

sre.LoadGrammarAsync(g_StartStop);
sre.LoadGrammarAsync(g_WhatIsXplusY);
sre.RecognizeAsync(RecognizeMode.Multiple);

複数の文法を使用する場合は、RecognizeMode.Multiple 引数が必要です。このことは、きわめて単純なプログラムでなければ、すべてのプログラムに当てはまります。Main メソッドの最後は、次のようになっています。

...
    while (done == false) { ; }
    Console.WriteLine("\nHit <enter> to close shell\n");
    Console.ReadLine();
  }
  catch (Exception ex)
  {
    Console.WriteLine(ex.Message);
    Console.ReadLine();
  }
} // Main

奇妙に見える空の while ループは、コンソール アプリ シェルをアクティブな状態に維持します。このループは、音声認識エンジンのイベント ハンドラーにより、ブール値のクラススコープ変数 "done" が true に設定されると停止します。

認識した音声の処理

音声認識のイベント ハンドラーの先頭部分は、次のようになっています。

static void sre_SpeechRecognized(object sender,
  SpeechRecognizedEventArgs e)
{
  string txt = e.Result.Text;
  float confidence = e.Result.Confidence;
  Console.WriteLine("\nRecognized: " + txt);
  if (confidence < 0.60) return;
...

認識した実際のテキストは、SpeechRecognizedEventArgs Result.Text プロパティに格納されています。また、Result.Words コレクションを使用することもできます。Result.Confidence プロパティは、0.0 ~ 1.0 の値を保持します。この値は、発話されたテキストが、認識エンジンに関連付けられたいずれかの文法に合致する率を大まかな測定値として表したものです。デモ プログラムでは、認識したテキストの信頼度が低い場合、そのテキストを無視するようにイベント ハンドラーに指示しています。

信頼度は、文法の複雑さや、使用するマイクの品質などによって大きく変わります。たとえば、デモ プログラムで 1 ~ 4 のみを認識する必要がある場合、今回使用したコンピューターでは通常の信頼度は約 0.75 になりました。しかし、文法で 1 ~ 100 を認識する必要がある場合、この信頼度は約 0.25 に下がります。つまり、音声認識で高い結果を得るには、多くの場合、信頼度に関する実験が必要です。

次に、音声認識エンジンのイベント ハンドラーで、認識を有効または無効にします。

if (txt.IndexOf("speech on") >= 0)
{
  Console.WriteLine("Speech is now ON");
  speechOn = true;
}
if (txt.IndexOf("speech off") >= 0)
{
  Console.WriteLine("Speech is now OFF");
  speechOn = false;
}
if (speechOn == false) return;

このロジックをひと目で完全に理解するのは難しいかもしれませんが、少し調べれば意味がわかります。次に、秘密の終了コマンドを処理します。

if (txt.IndexOf("klatu") >= 0 && txt.IndexOf("barada") >= 0)
{
  ((SpeechRecognitionEngine)sender).RecognizeAsyncCancel();
  done = true;
  Console.WriteLine("(Speaking: Farewell)");
  ss.Speak("Farewell");
}

実は、音声認識エンジンでは意味が通らない語でも認識できます。Grammar オブジェクトに含まれている語がオブジェクトの組み込みの辞書にない場合、Grammar は、語義に関するヒューリスティックを使って可能な限りその語を特定しようとします。そしてほとんどの場合は、これである程度通用します。ですから、ここでは (昔の SF 映画から引用した) 正しい「klaatu」ではなく、「klatu」を使用しています。

また、認識した Grammar のテキスト (「klatu barada nikto」) 全体を処理する必要はありません。文法フレーズ (「klatu」と「barada」) を一意に特定できるだけの情報を得られれば十分です。

次に、2 つの数字を加算するコマンドを処理し、イベント ハンドラー、Program クラス、および名前空間を完成させます。

...
      if (txt.IndexOf("What") >= 0 && txt.IndexOf("plus") >= 0)
      {
        string[] words = txt.Split(' ');
        int num1 = int.Parse(words[2]);
        int num2 = int.Parse(words[4]);
        int sum = num1 + num2;
        Console.WriteLine("(Speaking: " + words[2] +
          " plus " + words[4] + " equals " + sum + ")");
        ss.SpeakAsync(words[2] + " plus " + words[4] +
          " equals " + sum);
      }
    } // sre_SpeechRecognized
  } // Program
} // ns

Results.Text のテキストでは、大文字と小文字が区別されているのがわかります (「What」と「what」)。フレーズを認識したら、特定の語を抽出します。今回は、認識するテキストが「What is x plus y」形式になっているので、「What」は words[0] に、(文字列として) 追加する 2 つの数字は words[2] と words[4] に格納されます。

ライブラリのインストール

デモ プログラムの説明では、必要な音声ライブラリがコンピューターにすべてインストールされていることを前提としました。デモ プログラムを作成して実行するには、デモ プログラムを Visual Studio で作成するための SDK、作成したデモ プログラムを実行するためのランタイム、認識言語、および合成 (発話する) 言語の 4 つのパッケージをインストールする必要があります。

SDK をインストールするには、"Speech Platform 11 SDK" をインターネットで検索します。これにより、Microsoft ダウンロード センターの正しいページにアクセスできます (図 4 参照)。[Download] (ダウンロード) ボタンをクリックすると、図 5 のオプションが表示されます。SDK には 32 ビット バージョンと 64 ビット バージョンがありますが、お使いのホスト コンピューターに関係なく、32 ビット バージョンを使用することを強くお勧めします。64 ビット バージョンは、一部のアプリとの同時使用ができません。

Microsoft ダウンロード センターの SDK インストール メイン ページ
図 4 Microsoft ダウンロード センターの SDK インストール メイン ページ

Speech SDK のインストール
図 5 Speech SDK のインストール

必要なものは x86 (32 ビット) .msi ファイル 1 つだけです。ファイルを選択して [Next] (次へ) ボタンをクリックすると、インストール プログラムを直接実行できます。音声ライブラリはインストール完了時のフィードバックがほとんどなく、インストールの成功を示すメッセージなども表示されません。

次に、音声ランタイムをインストールします。メイン ページを表示して [Next] (次へ) ボタンをクリックすると、図 6 のオプションが表示されます。

音声ランタイムのインストール
図 6 音声ランタイムのインストール

必ず SDK と同じプラットフォーム バージョン (デモ プログラムでは 11) とビット バージョン (32 (x86) または 64 (x64)) を選択します。先ほどと同様、お使いのコンピューターが 64 ビットであっても、32 ビット バージョンのインストールを強くお勧めします。

次に、認識言語をインストールします。図 7 にダウンロード ページを示します。このデモでは、MSSpeech_SR_en-us_TELE.msi (米国英語) ファイルを使用します。SR とは、音声認識 (Speech Recognition) を表し、TELE はテレフォニー (Telephony) を表します。つまり、認識言語は、電話やデスクトップのマイクといった低品質の音声入力でも動作するように設計されています。

認識言語のインストール
図 7 認識言語のインストール

最後に、音声合成用の言語と音声をインストールします。図 8 にダウンロード ページを示します。デモでは MSSpeech_TTS_en-us_Helen.msi ファイルを使用します。TTS は "Text-to-Speech" のことで、基本的には音声合成 (Speech Synthesis) と同義です。米国英語の場合は 2 つの音声を使用でき、米国以外の英語音声も用意されています。合成ファイルの作成はかなり難易度の高い作業です。いくつかの企業からその他の音声を購入し、インストールすることができます。

合成用の言語と音声のインストール
図 8 合成用の言語と音声のインストール

興味深いことに、音声認識の言語と音声合成の音声/言語はまったく異なるものでありながら、どちらも同じダウンロード ページからダウンロードできます。ダウンロード センターの UI では認識言語と合成言語の両方を確認できます。ですが、この 2 つを同時にインストールしようとすると悲惨な目に遭うため、個人的には 1 つずつインストールすることをお勧めします。

Microsoft.Speech と System.Speech の違い

音声プラットフォームは複数あるため、Windows アプリの音声認識と音声合成を始めたばかりの方は、ドキュメントを読む際に混乱しがちです。具体例を挙げると、今回のデモ プログラムで使用している Microsoft.Speech.dll ライブラリ以外に、Windows OS 組み込みの System.Speech.dll ライブラリがあります。この 2 つのライブラリは、API がほとんど同じ (まったく同じではありません) という点では似ています。そのため、インターネットで音声の使用例を検索していて、完全なプログラムではなくコード スニペットを見つけた場合、その例が System.Speech と Microsoft.Speech のどちらを示しているのか判断しにくいことがあります。

結論として、音声の使用経験が少ない方が .NET アプリに音声を追加する場合は、System.Speech ライブラリではなく Microsoft.Speech ライブラリを使用することをお勧めします。

中核的なベース コードに同じ部分があり、API が似ているとはいえ、この 2 つのライブラリは決して同じものではありません。いくつかの重要な違いを図 9 の表にまとめます。

図 9 Microsoft.Speech と System.Speech の違い

Microsoft.Speech.dll System.Speech.dll
別途インストールが必要 OS 組み込み (Windows Vista 以上)
アプリと共にパッケージ化可能 再頒布不可
Grammar の作成が必要 Grammar または自由発話のディクテーションを使用
ユーザーのトレーニングが不要 特定のユーザー向けに対してトレーニングが必要
マネージ コード API (C#) ネイティブ コード API (C++)

System.Speech DLL は OS に組み込まれているため、すべての Windows コンピューターにインストールされています。Microsoft.Speech DLL (および関連ランタイムと言語) は、コンピューターにダウンロードしてインストールする必要があります。System.Speech による音声認識は、通常ユーザーのトレーニングが必要です。このトレーニングでは、ユーザーがいくつかのテキストを読み上げ、特定のユーザーの発音を認識するようにシステムが学習します。Microsoft.Speech による音声認識は、すべてのユーザーに対してすぐに動作します。System.Speech はほとんどすべての語を認識できます (自由発話のディクテーション) が、Microsoft.Speech はプログラムで定義した Grammar に当てはまる語やフレーズのみを認識します。

Windows フォーム アプリへの音声認識の追加

Windows フォーム アプリまたは WPF アプリに音声認識と音声合成を追加するプロセスは、コンソール アプリに音声を追加するプロセスと同様です。図 2 に示すダミー デモ プログラムを作成するには、Visual Studio を起動して新しい C# の Windows フォーム アプリを作成し、WinFormSpeech という名前を付けます。

テンプレート コードが Visual Studio エディターに読み込まれたら、コンソール アプリのデモで行ったように、ソリューション エクスプローラー ウィンドウで Microsoft.Speech.dll ファイルへの参照を追加します。ソース コードの先頭で、不要な using ステートメントを削除し、System 名前空間、Data 名前空間、Drawing 名前空間、および Forms 名前空間への参照のみを残します。Microsoft.Speech.Recognition 名前空間と System.Globalization 名前空間をスコープに含めるため、2 つの using ステートメントを追加します。

Windows フォーム アプリのデモでは音声合成を使わないので、Microsoft.Speech.Synthesis ライブラリへの参照は使用しません。Windows フォーム アプリへの音声合成の追加方法は、コンソール アプリへの音声合成の追加方法とまったく同じです。

Visual Studio のデザイン ビューで、TextBox コントロール、CheckBox コントロール、および ListBox コントロールをフォーム アプリにドラッグします。CheckBox コントロールをダブルクリックすると、Visual Studio によって CheckChanged イベント ハンドラー メソッドのスケルトンが自動的に作成されます。

コンソール アプリのデモで、音声コマンドの聞き取りが即座に開始され、アプリが終了するまで継続させていたことを思い出してください。このアプローチは Windows フォーム アプリでも使えますが、ここでは CheckBox コントロールを使用して、ユーザーが音声認識の有効/無効を切り替えられるようにします。

図 10 に、デモ プログラムの Form1.cs ファイルのソース コード (部分クラスの定義) を示します。音声認識エンジン オブジェクトを宣言し、Form メンバーとしてインスタンスを作成します。Form コンストラクター内部では、SpeechRecognized イベント ハンドラーをフックし、2 つの Grammar を作成して読み込みます。

public Form1()
{
  InitializeComponent();
  sre.SetInputToDefaultAudioDevice();
  sre.SpeechRecognized += sre_SpeechRecognized;
  Grammar g_HelloGoodbye = GetHelloGoodbyeGrammar();
  Grammar g_SetTextBox = GetTextBox1TextGrammar();
  sre.LoadGrammarAsync(g_HelloGoodbye);
  sre.LoadGrammarAsync(g_SetTextBox);
  // sre.RecognizeAsync() is in CheckBox event
}

図 10 Windows フォーム アプリへの音声認識の追加

using System;
using System.Data;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.Speech.Recognition;
using System.Globalization;
namespace WinFormSpeech
{
  public partial class Form1 : Form
  {
    static CultureInfo ci = new CultureInfo("en-us");
    static SpeechRecognitionEngine sre = 
      new SpeechRecognitionEngine(ci);
    public Form1()
    {
      InitializeComponent();
      sre.SetInputToDefaultAudioDevice();
      sre.SpeechRecognized += sre_SpeechRecognized;
      Grammar g_HelloGoodbye = GetHelloGoodbyeGrammar();
      Grammar g_SetTextBox = GetTextBox1TextGrammar();
      sre.LoadGrammarAsync(g_HelloGoodbye);
      sre.LoadGrammarAsync(g_SetTextBox);
      // sre.RecognizeAsync() is in CheckBox event
    }
    static Grammar GetHelloGoodbyeGrammar()
    {
      Choices ch_HelloGoodbye = new Choices();
      ch_HelloGoodbye.Add("hello");
      ch_HelloGoodbye.Add("goodbye");
      GrammarBuilder gb_result = 
        new GrammarBuilder(ch_HelloGoodbye);
      Grammar g_result = new Grammar(gb_result);
      return g_result;
    }
    static Grammar GetTextBox1TextGrammar()
    {
      Choices ch_Colors = new Choices();
      ch_Colors.Add(new string[] { "red", "white", "blue" });
      GrammarBuilder gb_result = new GrammarBuilder();
      gb_result.Append("set text box 1");
      gb_result.Append(ch_Colors);
      Grammar g_result = new Grammar(gb_result);
      return g_result;
    }
    private void checkBox1_CheckedChanged(object sender, 
      EventArgs e)
    {
      if (checkBox1.Checked == true)
        sre.RecognizeAsync(RecognizeMode.Multiple);
      else if (checkBox1.Checked == false) // Turn off
        sre.RecognizeAsyncCancel();
    }
    void sre_SpeechRecognized(object sender, 
      SpeechRecognizedEventArgs e)
    {
      string txt = e.Result.Text;
      float conf = e.Result.Confidence;
      if (conf < 0.65) return;
      this.Invoke(new MethodInvoker(() =>
      { listBox1.Items.Add("I heard you say: " 
      + txt); })); // WinForm specific
      if (txt.IndexOf("text") >= 0 && txt.IndexOf("box") >=
        0 && txt.IndexOf("1")>= 0)
      {
        string[] words = txt.Split(' ');
        this.Invoke(new MethodInvoker(() =>
        { textBox1.Text = words[4]; })); // WinForm specific
      }
    }
  } // Form
} // ns

コンソール アプリのデモで行ったように、2 つの Grammar オブジェクトを直接作成することもできます。しかし、今回はコードをもう少し明確にするため、その処理を実行する 2 つのヘルパー メソッド、GetHelloGoodbyeGrammar と GetTextBox1TextGrammar を定義します。

Form コンストラクターで RecognizeAsync メソッドを呼び出していないことに注目してください。つまり、音声認識はアプリの起動時に即座にアクティブになるわけではありません。

GetHelloGoodbyeGrammar ヘルパー メソッドは、前述のパターンと同じパターンに従って実行します。

static Grammar GetHelloGoodbyeGrammar()
{
  Choices ch_HelloGoodbye = new Choices();
  ch_HelloGoodbye.Add("hello"); // Should be an array!
  ch_HelloGoodbye.Add("goodbye");
  GrammarBuilder gb_result =
    new GrammarBuilder(ch_HelloGoodbye);
  Grammar g_result = new Grammar(gb_result);
  return g_result;
}

Windows フォーム アプリの TextBox コントロールにテキストを設定するための Grammar オブジェクトを作成するヘルパー メソッドにも、特に目新しい点はありません。

static Grammar GetTextBox1TextGrammar()
{
  Choices ch_Colors = new Choices();
  ch_Colors.Add(new string[] { "red", "white", "blue" });
  GrammarBuilder gb_result = new GrammarBuilder();
  gb_result.Append("set text box 1");
  gb_result.Append(ch_Colors);
  Grammar g_result = new Grammar(gb_result);
  return g_result;
}

このヘルパー メソッドは、「set text box 1 red」というフレーズを認識します。ただし、ユーザーはこのフレーズを正確に発話する必要はありません。たとえば、ユーザーは「Please set the text in text box 1 to red」と言ってもかまいません。こうすると、ユーザーが Grammar のパターンに正確に従った場合よりも信頼度は下がるものの、音声認識エンジンはこのフレーズを「set text box 1 red」と認識します。言い換えれば、Grammar を作成する際、1 つのフレーズのさまざまなバリエーションを考慮しなくてもかまいません。これにより、音声認識は劇的に使いやすくなります。

CheckBox イベント ハンドラーは、次のように定義します。

private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
  if (checkBox1.Checked == true)
    sre.RecognizeAsync(RecognizeMode.Multiple);
  else if (checkBox1.Checked == false) // Turn off
    sre.RecognizeAsyncCancel();
}

Windows フォーム アプリの実行中は、音声認識エンジン オブジェクト (SRE) が常に動作しています。このオブジェクトは、ユーザーが CheckBox コントロールを切り替えたときに、RecognizeAsync メソッドと RecognizeAsyncCancel メソッドによってアクティブまたは非アクティブになります。

音声認識のイベント ハンドラーの先頭部分は、次のように定義します。

void sre_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
{
  string txt = e.Result.Text;
  float conf = e.Result.Confidence;
  if (conf < 0.65) return;
...

事実上常に使用する Result.Text プロパティと Result.Confidence プロパティのほかにも、Result オブジェクトには、Homophones や ReplacementWordUnits など便利な (ただしより高度な) プロパティがいくつか用意されているので、確認することをお勧めします。さらに、音声認識エンジンには SpeechHypothesized をはじめとする有用なイベントがいくつか組み込まれています。

イベント ハンドラーのコードの最後は、次のように記述します。

...
  this.Invoke(new MethodInvoker(() =>
    { listBox1.Items.Add("I heard you say: " + txt); }));
  if (txt.IndexOf("text") >= 0 &&
    txt.IndexOf("box") >= 0 && txt.IndexOf("1")>= 0)
  {
    string[] words = txt.Split(' ');
    this.Invoke(new MethodInvoker(() =>
    { textBox1.Text = words[4]; }));
  }
}

認識したテキストは、MethodInvoker デリゲートによってそのまま ListBox コントロールに表示します。音声認識エンジンを実行するスレッドは、Windows フォーム アプリの UI スレッドとは異なるため、次のように ListBox コントロールに直接アクセスしようとすると、

listBox1.Items.Add("I heard you say: " + txt);

失敗して例外がスローされます。MethodInvoker の代わりに、次のように Action デリゲートを使用することもできます。

this.Invoke( (Action)( () =>
  listBox1.Items.Add("I heard you say: " + txt)));

この場合は、MethodInvoker が Windows.Forms 名前空間の一部で、Windows フォーム アプリ固有のデリゲートであるため、理論上は Action デリゲートを使用するよりも MethodInvoker デリゲートを使用する方がやや効率的です。Action の方がデリゲートとしては汎用的です。今回の例では、音声認識を使用して、非常に強力かつ便利な方法で Windows フォーム アプリを完全に操作できることがわかります。

まとめ

.NET アプリの音声認識と音声合成について知りたい方は、今回示した情報が出発点になります。必要なインストールと学習のハードルを最初に乗り越えてしまえば、テクノロジ自体の習得はそれほど難しくありません。音声認識と音声合成で本当に課題になるのは、それらの機能が有効なタイミングを見極めることです。

コンソール アプリでは、ユーザーが質問してアプリが返答するという、興味を引くような双方向のダイアログを作成し、Cortana のような環境を作り出すことができます。コンピューターが発話する際は、その音声がマイクで拾われて認識されてしまう可能性があるため、少し注意が必要です。これまで何度か面白い状況に陥った経験があります。質問をすると認識してアプリが答えてくれるのですが、その際に発話された答えの音声によって別の認識イベントが発生し、笑えるような音声の無限ループにはまってしまいました。

コンソール アプリで実現できる音声のもう 1 つの使用方法は、「Launch Notepad」(メモ帳を起動) や「Launch Word」(Word を起動) などのコマンドをアプリに認識させることです。つまり、通常はマウスとキーボードを複数回操作して実行する動作を、コンソール アプリを使用してホスト コンピューターで実行できます。


Dr. James McCaffrey は、ワシントン州レドモンドにある Microsoft Research に勤務しています。これまでに、Internet Explorer、Bing などの複数のマイクロソフト製品にも携わってきました。McCaffrey 博士の連絡先は、jammc@microsoft.com (英語のみ) です。

この記事のレビューに協力してくれた Microsoft Research 技術スタッフの Rob Gruen、Mark Marron、および Curtis von Veh に心より感謝いたします。