C# で日本語合成音声・音声認識をやってみよう。

この記事は C# advent calendar 2011の記事です。
microsoftって音声認識・合成音声のOS組み込みをXPから初めています。
今回は、そのライブラリを使い、日本語合成音声でしゃべり、日本語の音声認識するソフトの作り方を説明したいと思います。

サンプルの使い方とソース

サンプルソース
http://rtilabs.net/files/2011_12_16/speechplatformtest_src.zip
サンプルexe
http://rtilabs.net/files/2011_12_16/speechplatformtest.zip
開発環境
windows7 64bit .Net4
依存ライブラリ
Speech Platform ver11
http://mahoro-ba.net/e1541.html
http://www.microsoft.com/download/en/details.aspx?id=27224
http://www.microsoft.com/download/en/details.aspx?id=27225



読み上げを押すと、読み上げます。
音声認識により「りんごください」と「バナナください」、「私がムスカ大佐だ」「らんらんるー」のいづれかの発音にマッチします。
ルールベースのマッチングなので、多少変なマッチでも、この4つのうちのどれかになります。

日本語合成音を手に入れよう。

windows7 64bitでは日本語合成音声は無料で公開されて来ませんでした。
しかし、今年の10月になって、ようやく無料での日本語合成音声が公開されました。


こちらからダウンロードできますが、やり方が少々めんどいので、
インストール方法は、まほろばさんのページを参考にしてください。
http://mahoro-ba.net/e1541.html


MS公式はこちらになります。
http://www.microsoft.com/download/en/details.aspx?id=27224
http://www.microsoft.com/download/en/details.aspx?id=27225



ラピュタ王家は地上に降りて二つに分かれたそうですが、MSの音声関係のライブラリも SAPI と Speech Platform という2つのライブラリに分離してしまっており、単純に利用するのは少々メンドイです。


SAPI だと日本語の合成音声はやや微妙でした。しかもwindows7 64bitだと無料で使えません。(たぶん)
逆に Speech Platform だと、10月に公開された、結構綺麗に喋る日本語合成音声 haruka があります。
ただし、SAPI と比べるとやや癖があるので、音声認識の方で苦労します。
(うちでは未だに Speech Platform で dictication が動作しません。 SAPI5.4だと動くのに。 どうすれば動くようになるんでしょう・・・)

C#での音声ライブラリと罠。

windows7では音声認識はOSに直結されました。(vistaから?)
そして、C#やその他の言語から普通に使おうとすると、実用的に使えない残念なものとなりました。


何が残念なんでしょうか?


ふつーC#でやろうとすると、System.Speech.Recognitionなどの .NETのライブラリを経由すると思います。
これは罠です。使ってはいけません

//これはダメな方のコードです

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Speech;

namespace test1234
{
    public partial class Form1 : Form
    {
        System.Speech.Recognition.SpeechRecognizer Reco = null;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            this.Reco = new  System.Speech.Recognition.SpeechRecognizer();
            var chooices = new System.Speech.Recognition.Choices();
            chooices.Add("りんごください");
            chooices.Add("バナナください");

            var gram = new System.Speech.Recognition.Grammar(chooices.ToGrammarBuilder());
            gram.Enabled = true;

            this.Reco.MaxAlternates = 1;
            this.Reco.LoadGrammar(gram);
            this.Reco.SpeechRecognized += SpeechRecognized;


            this.Reco.Enabled = true;
        }
        void SpeechRecognized(object sender, System.Speech.Recognition.RecognitionEventArgs e)
        {
            Console.WriteLine(e.Result.Text);
        }
    }
}

奴が来る

このようなコードを使って、実行してみましょう。
何が起きるでしょうか?

おや、頼んでもいないのに変なウィンドウが登場しました。
しかも、ウィンドウ登場時に一定秒間OSが固まりましたネ。
さらに、こいつ動作が鈍く、たまにOS全体を巻き込んでフリーズしたりクラッシュしたりしてくれます。
さらに、音声認識を利用していると、長時間時間が経つと勝手に認識を打ち切って、スリープモードみたいな感じになるという、えっ!?という動作をしてくれます。
まったく使い物になりません。



なんということでしょう。
windows xp とかだとこんなことはなかったのに。


windows7 (もしくはvista)から仕様が改悪されてしまいました。
microsoftはなんでこんなことひどい事をするのか・・・・
(もし、これをどうにか出来る方法をご存知の人は教えてください)

.NETバージョンは使い物にならない。COMを使おう。

音声認識ライブラリはCOMインターフェースを実装されています。
そして、大きく2つのインターフェースをもっていて、一つはSpSharedRecognizerでさっきの奴を呼んでしまう方法、もうひとつはSpInprocRecognizerでアプリ内で閉じてくれる方法です。

SpSharedRecognizer 奴が来る
SpInprocRecognizer アプリ内で閉じて幸せ


SpInprocRecognizerでアプリ内で閉じてくれる方法だと、初期化もほぼ一瞬ですし、音声認識エンジンがおかしくなったとしてもアプリだけが落ち、OSを不安定にもしません。しかも、長時間稼働させても勝手にオフになるなどのおかしな動作もしません。ちゃんと動いてくれるライブラリです。これは使わない手はありません。



(windows7音声認識エンジンを改悪したチームは何でこんなコトしたの?)



.NETの音声認識・合成音声ライブラリを使うと、前者のアレな方を呼んでしまいます。
そこで、今回は、COMを利用して、アプリ内で閉じてみます。


Visual StudioC# を使うと COM も簡単に操作できるので楽ちんです。
試しにやってみましょう。


Visual Studioでメニューの「プロジェクト」から「参照の追加」を選択します。

次に、 「COM」 のタブを選択し、 Microsoft Speech Object Library を選択します。
もし、 Microsoft Speech Object Library が2つある場合、今回は バージョンが11.0 の方を入れます。

以上で、参照設定への追加は終わりです。
あとは、これを利用したコードを書くだけです。
ライブラリは、 SpeechLib の名前空間の下に展開されています。

合成音声で読みあげてみよう。

SpeechLib.SpVoice クラスの Speak メソッドを呼び出すと、コンピュータが喋ってくれます。
これをコードにすると、こんな感じです。(抜粋です。)

//using System.Speech;  //こっちは邪悪なので使ってはならぬ。
using SpeechLib; //for ver11

namespace speechplatformtest
{
    public partial class Form1 : Form
    {
        //合成音声ライブラリの読み込み
        private SpeechLib.SpVoice VoiceSpeeach = null;

        public Form1()
        {
            //合成音声エンジンを初期化する.
            this.VoiceSpeeach = new SpeechLib.SpVoice();
            //合成音声エンジンで日本語を話す人を探す。(やらなくても動作はするけど、念のため)
            bool hit = false;
            foreach (SpObjectToken voiceperson in this.VoiceSpeeach.GetVoices())
            {
                string language = voiceperson.GetAttribute("Language");
                if (language == "411")
                {//日本語を話す人だ!
                    this.VoiceSpeeach.Voice = voiceperson; //君に読みあげて欲しい
                    hit = true;
                    break;
                }
            }
            if (!hit)
            {
                MessageBox.Show("日本語合成音声が利用できません。\r\n日本語合成音声 MSSpeech_TTS_ja-JP_Haruka をインストールしてください。\r\n");
            }
        }

        //読み上げボタン
        private void YondeneButton_Click(object sender, EventArgs e)
        {
            if (this.VoiceSpeeach.Status.RunningState == SpeechRunState.SRSEIsSpeaking)
            {
                //現在話し中..
                return;
            }

            this.VoiceSpeeach.Speak(StringComboBox.Text , SpeechVoiceSpeakFlags.SVSFlagsAsync | SpeechVoiceSpeakFlags.SVSFIsXML);
        }

ピッチや速度・音量の調整もできます。
しかし、これまた不思議仕様がありまして、合成音声クラスには、ピッチの調整はあれど速度を調整するプロパティがありません。

//抜粋。
    public interface ISpeechVoice
    {
        [DispId(5)]
        int Rate { get; set; }     //ピッチレート

        [DispId(6)]
        int Volume { get; set; }   //音量
        
        
        //あれ? 話し速度を変える speed がないよ・・・・
        //気になる人は探してみよう。
    }


どうしてそういう設定にしたのか激しく謎ですが、こうなっています。
クラスのプロパティはこうなっていますが、音声読み上げの文字列の中にxml形式で制御できるようになっています。


具体的にはこんな感じで、指定ができます。

こんにちは世界。貴方の予想に反して、この音声が聞こえるでしょうか?
こんにちは世界。<volume level="10">大きな声で、こんにちは世界。</volume>元の声で、こんにちは世界。
こんにちは世界。<rate speed="5">早目に、こんにちは世界。</rate><rate speed="-5">遅めに、こんにちは世界。</rate>元の速度で、こんにちは世界。
こんにちは世界。<pitch middle="5">ピッチをあげて、こんにちは世界。</pitch><pitch middle="-5">ピッチを下げて、こんにちは世界。</pitch>ピッチを元に戻して、こんにちは世界。
こんにちは世界。<silence msec="500"/>こんにちは世界。沈黙をつくれる。怨念がおんねん<silence msec="500"/>


詳しい仕様はmicrosoftのサイト御覧ください。
http://msdn.microsoft.com/en-us/library/ee431815(v=vs.85).aspx


一部指定しても動作しないパラメータがあります。
サンプルのコンボボックスにいくつか登録してあるので、読み上げ本を押すと、どんな感じになるのかわかります。


音声認識させてみよう。


次に音声認識させてみます。
こちらは、windows7とかだとOSに標準で組み込まれています。
しかし、先ほどの「お話くださいウィンドウ」の罠があるので、こちらもCOMを経由して触るのが得策です。


りんごとばななの聞き取りをやってみましょう。

//using System.Speech;  //こっちは邪悪なので使ってはならぬ。
using SpeechLib; //for ver11

namespace speechplatformtest
{
    public partial class Form1 : Form
    {
        //音声認識オブジェクト
        private SpeechLib.SpInProcRecoContext RecognizerRule = null;
        //音声認識のための言語モデル
        private SpeechLib.ISpeechRecoGrammar RecognizerGrammarRule = null;
        //音声認識のための言語モデルのルールのトップレベルオブジェクト.
        private SpeechLib.ISpeechGrammarRule RecognizerGrammarRuleGrammarRule = null;

        public Form1()
        {
            //ルール認識 音声認識オブジェクトの生成
            this.RecognizerRule = new SpeechLib.SpInProcRecoContext();
            hit = false;
            foreach (SpObjectToken recoperson in this.RecognizerRule.Recognizer.GetRecognizers()) //'Go through the SR enumeration
            {
                string language = recoperson.GetAttribute("Language");
                if (language == "411")
                {//日本語を聴き取れる人だ
                    this.RecognizerRule.Recognizer.Recognizer = recoperson; //君に聞いていて欲しい
                    hit = true;
                    break;
                }
            }
            if (!hit)
            {
                MessageBox.Show("日本語認識が利用できません。\r\n日本語音声認識 MSSpeech_SR_ja-JP_TELE をインストールしてください。\r\n");
            }

            //マイクから拾ってね。
            this.RecognizerRule.Recognizer.AudioInput = this.CreateMicrofon();

            //音声認識イベントで、デリゲートによるコールバックを受ける.

            //認識の途中
            this.RecognizerRule.Hypothesis +=
                delegate(int streamNumber, object streamPosition, SpeechLib.ISpeechRecoResult result)
                {
                    string strText = result.PhraseInfo.GetText(0, -1, true);
                    this.HypothesisTextBox.Text = strText;
                };
            //認識完了
            this.RecognizerRule.Recognition +=
                delegate(int streamNumber, object streamPosition, SpeechLib.SpeechRecognitionType srt, SpeechLib.ISpeechRecoResult isrr)
                {
                    string strText = isrr.PhraseInfo.GetText(0, -1, true);
                    this.RecognitionTextBox.Text = strText;
                };
            //ストリームに何かデータが来た(?)
            this.RecognizerRule.StartStream +=
                delegate(int streamNumber, object streamPosition)
                {
                    this.HypothesisTextBox.Text = "";
                    this.RecognitionTextBox.Text = "";
                };
            //認識失敗
            this.RecognizerRule.FalseRecognition +=
                delegate(int streamNumber, object streamPosition, SpeechLib.ISpeechRecoResult isrr)
                {
                    this.RecognitionTextBox.Text = "--ERROR!--";
                };

//            this.RecognizerRule.CmdMaxAlternates = 0;

            //言語モデルの作成
            this.RecognizerGrammarRule = this.RecognizerRule.CreateGrammar(0);

            this.RecognizerGrammarRule.Reset(0);
            //言語モデルのルールのトップレベルを作成する.
            this.RecognizerGrammarRuleGrammarRule = this.RecognizerGrammarRule.Rules.Add("TopLevelRule",
                SpeechRuleAttributes.SRATopLevel | SpeechRuleAttributes.SRADynamic);
            //文字列の追加.
            this.RecognizerGrammarRuleGrammarRule.InitialState.AddWordTransition(null, "りんごください");
            this.RecognizerGrammarRuleGrammarRule.InitialState.AddWordTransition(null, "バナナください");
            this.RecognizerGrammarRuleGrammarRule.InitialState.AddWordTransition(null, "らんらんるー");
            this.RecognizerGrammarRuleGrammarRule.InitialState.AddWordTransition(null, "私がムスカ大佐だ");

            //ルールを反映させる。
            this.RecognizerGrammarRule.Rules.Commit();

            //音声認識開始。(トップレベルのオブジェクトの名前で SpeechRuleState.SGDSActive を指定する.)
            this.RecognizerGrammarRule.CmdSetRuleState("TopLevelRule", SpeechRuleState.SGDSActive);
        }

        //マイクから読み取るため、マイク用のデバイスを指定する.
        // C++ だと SpCreateDefaultObjectFromCategoryId ヘルパーがあるんだけど、C#だとないんだなこれが。
        private SpeechLib.SpObjectToken CreateMicrofon()
        {
            SpeechLib.SpObjectTokenCategory objAudioTokenCategory = new SpeechLib.SpObjectTokenCategory();
            objAudioTokenCategory.SetId(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech Server\v11.0\AudioInput", false);
            SpeechLib.SpObjectToken objAudioToken = new SpeechLib.SpObjectToken();
            objAudioToken.SetId(objAudioTokenCategory.Default, @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech Server\v11.0\AudioInput", false);
            return objAudioToken;
        }
    }
}


イベントがデリゲートで実装されているので、C#との相性は抜群です。
余談ですが、C++からやると、EventObject や WindowMessage通知などなどの手法が選択できます。
C#だとそーゆーのはなしで、デリゲートで十分カバーできますし、旧来の手法よりも使いやすくて素晴らしい感じです。

正規表現音声認識

もっと、複雑な認識を SAPI XML形式で書くことができます。

<?xml version="1.0" encoding="UTF-8"?>
<GRAMMAR>
<RULE name="S" toplevel="ACTIVE">
  <L>
    <P>りんごください</P>
    <P>バナナください</P>
  </L>
</RULE>
</GRAMMAR>

共通部分の、くださいをくくりだして、以下のようにもいけるはずです。

<?xml version="1.0" encoding="UTF-8"?>
<GRAMMAR>
<RULE name="S" toplevel="ACTIVE">
  <L>
    <P>りんご</P>
    <P>バナナ</P>
  </L>
  <L>
    <P>ください</P>
  </L>
</RULE>
</GRAMMAR>


SAPI XMLは一応簡単にかけるのですが、長くなると結構めんどいです。


なんつーか、正規表現音声認識のマッチパターンがかければいいのに・・・って思いませんか?
私もそう思ったので作って見ました。



正規表現で音声認識です。


私の正規表現音声認識ライブラリを使うと、りんごとばななにマッチすることをいかのように書けます。

(りんご|バナナ)ください

まとめ

C#で 合成音声・音声認識 を利用する方法について説明しました。
COMなので、C++や、その他の言語からも利用できます。
ただしwindows縛りになってしまいますけどね。


うちではこれを利用して家電制御アプリを使っています。(こっちの実装はC++ですけど)
家電を音声でコントロールする。
http://d.hatena.ne.jp/rti7743/20110828/1314546712
イオナズンして家電制御
http://d.hatena.ne.jp/rti7743/20110830/1314663041


近年、合成音声・音声認識はやっと実用化され、身近な存在になりつつあります。
siriに代表されるような、音声エージェントも登場しましたし、さらにブレークしていくものだと思います。


音声でコンピュータとコミニュケーションをとれる未来が来たらきっと楽しいと思います。
音声認識や合成音声をガチで作ろうとすると難しい数式の嵐になると思いますが、無料でつかえるライブラリがあるわけですから、こいつらをうまく活用して、楽しい応用アプリがたくさん生まれれば未来はもっと楽しくなるでしょう。